mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge pull request #20376 from goodtrailer/smoke
Add cursor "smoke" trails
This commit is contained in:
commit
4980e53c9c
@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 251 B |
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal file
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSmoke : OsuSkinnableTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestSmoking()
|
||||
{
|
||||
addStep("Create short smoke", 2_000);
|
||||
addStep("Create medium smoke", 5_000);
|
||||
addStep("Create long smoke", 10_000);
|
||||
}
|
||||
|
||||
private void addStep(string stepName, double duration)
|
||||
{
|
||||
var smokeContainers = new List<SmokeContainer>();
|
||||
|
||||
AddStep(stepName, () =>
|
||||
{
|
||||
smokeContainers.Clear();
|
||||
SetContents(_ =>
|
||||
{
|
||||
smokeContainers.Add(new TestSmokeContainer
|
||||
{
|
||||
Duration = duration,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
|
||||
return new SmokingInputManager
|
||||
{
|
||||
Duration = duration,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.95f),
|
||||
Child = smokeContainers[^1],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("Until skinnable expires", () =>
|
||||
{
|
||||
if (smokeContainers.Count == 0)
|
||||
return false;
|
||||
|
||||
Logger.Log("How many: " + smokeContainers.Count);
|
||||
|
||||
foreach (var smokeContainer in smokeContainers)
|
||||
{
|
||||
if (smokeContainer.Children.Count != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private class SmokingInputManager : ManualInputManager
|
||||
{
|
||||
public double Duration { get; init; }
|
||||
|
||||
private double? startTime;
|
||||
|
||||
public SmokingInputManager()
|
||||
{
|
||||
UseParentInput = false;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
MoveMouseTo(ToScreenSpace(DrawSize / 2));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
const float spin_angle = 4 * MathF.PI;
|
||||
|
||||
startTime ??= Time.Current;
|
||||
|
||||
float fraction = (float)((Time.Current - startTime) / Duration);
|
||||
|
||||
float angle = fraction * spin_angle;
|
||||
float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2;
|
||||
|
||||
Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2;
|
||||
MoveMouseTo(ToScreenSpace(pos));
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSmokeContainer : SmokeContainer
|
||||
{
|
||||
public double Duration { get; init; }
|
||||
|
||||
private bool isPressing;
|
||||
private bool isFinished;
|
||||
|
||||
private double? startTime;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
startTime ??= Time.Current + 0.1;
|
||||
|
||||
if (!isPressing && !isFinished && Time.Current > startTime)
|
||||
{
|
||||
OnPressed(new KeyBindingPressEvent<OsuAction>(new InputState(), OsuAction.Smoke));
|
||||
isPressing = true;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
if (isPressing && Time.Current > startTime + Duration)
|
||||
{
|
||||
OnReleased(new KeyBindingReleaseEvent<OsuAction>(new InputState(), OsuAction.Smoke));
|
||||
isPressing = false;
|
||||
isFinished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
LeftButton,
|
||||
|
||||
[Description("Right button")]
|
||||
RightButton
|
||||
RightButton,
|
||||
|
||||
[Description("Smoke")]
|
||||
Smoke,
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
new KeyBinding(InputKey.Z, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.X, OsuAction.RightButton),
|
||||
new KeyBinding(InputKey.C, OsuAction.Smoke),
|
||||
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
SpinnerBody,
|
||||
CursorSmoke,
|
||||
ApproachCircle,
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
Position = currentFrame.Position;
|
||||
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
|
||||
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
|
||||
if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
|
||||
}
|
||||
|
||||
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
|
||||
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
state |= ReplayButtonState.Left1;
|
||||
if (Actions.Contains(OsuAction.RightButton))
|
||||
state |= ReplayButtonState.Right1;
|
||||
if (Actions.Contains(OsuAction.Smoke))
|
||||
state |= ReplayButtonState.Smoke;
|
||||
|
||||
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class DefaultSmokeSegment : SmokeSegment
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
// ISkinSource doesn't currently fallback to global textures.
|
||||
// We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins.
|
||||
Texture = textures.Get("Gameplay/osu/cursor-smoke");
|
||||
}
|
||||
}
|
||||
}
|
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal file
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class LegacySmokeSegment : SmokeSegment
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Texture = skin.GetTexture("cursor-smoke");
|
||||
}
|
||||
}
|
||||
}
|
@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorSmoke:
|
||||
if (GetTexture("cursor-smoke") != null)
|
||||
return new LegacySmokeSegment();
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.HitCircleText:
|
||||
if (!this.HasFont(LegacyFont.HitCircle))
|
||||
return null;
|
||||
|
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal file
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal file
@ -0,0 +1,366 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
|
||||
{
|
||||
private const int max_point_count = 18_000;
|
||||
|
||||
// fade anim values
|
||||
private const double initial_fade_out_duration = 4000;
|
||||
|
||||
private const double re_fade_in_speed = 3;
|
||||
private const double re_fade_in_duration = 50;
|
||||
|
||||
private const double final_fade_out_speed = 2;
|
||||
private const double final_fade_out_duration = 8000;
|
||||
|
||||
private const float initial_alpha = 0.6f;
|
||||
private const float re_fade_in_alpha = 1f;
|
||||
|
||||
private readonly int rotationSeed = RNG.Next();
|
||||
|
||||
// scale anim values
|
||||
private const double scale_duration = 1200;
|
||||
|
||||
private const float initial_scale = 0.65f;
|
||||
private const float final_scale = 1f;
|
||||
|
||||
// rotation anim values
|
||||
private const double rotation_duration = 500;
|
||||
|
||||
private const float max_rotation = 0.25f;
|
||||
|
||||
public IShader? TextureShader { get; private set; }
|
||||
public IShader? RoundedTextureShader { get; private set; }
|
||||
|
||||
protected Texture? Texture { get; set; }
|
||||
|
||||
private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
|
||||
|
||||
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
|
||||
|
||||
private float pointInterval => radius * 7f / 8;
|
||||
|
||||
private double smokeStartTime { get; set; } = double.MinValue;
|
||||
|
||||
private double smokeEndTime { get; set; } = double.MaxValue;
|
||||
|
||||
private float totalDistance;
|
||||
private Vector2? lastPosition;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders)
|
||||
{
|
||||
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
|
||||
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
LifetimeStart = smokeStartTime = Time.Current;
|
||||
|
||||
totalDistance = pointInterval;
|
||||
}
|
||||
|
||||
private Vector2 nextPointDirection()
|
||||
{
|
||||
float angle = RNG.NextSingle(0, 2 * MathF.PI);
|
||||
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
|
||||
}
|
||||
|
||||
public void AddPosition(Vector2 position, double time)
|
||||
{
|
||||
lastPosition ??= position;
|
||||
|
||||
float delta = (position - (Vector2)lastPosition).LengthFast;
|
||||
totalDistance += delta;
|
||||
int count = (int)(totalDistance / pointInterval);
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
Vector2 increment = position - (Vector2)lastPosition;
|
||||
increment.NormalizeFast();
|
||||
|
||||
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
|
||||
increment *= pointInterval;
|
||||
|
||||
if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
|
||||
{
|
||||
int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
|
||||
SmokePoints.RemoveRange(index, SmokePoints.Count - index);
|
||||
}
|
||||
|
||||
totalDistance %= pointInterval;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SmokePoints.Add(new SmokePoint
|
||||
{
|
||||
Position = pointPos,
|
||||
Time = time,
|
||||
Direction = nextPointDirection(),
|
||||
});
|
||||
|
||||
pointPos += increment;
|
||||
}
|
||||
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
lastPosition = position;
|
||||
|
||||
if (SmokePoints.Count >= max_point_count)
|
||||
FinishDrawing(time);
|
||||
}
|
||||
|
||||
public void FinishDrawing(double time)
|
||||
{
|
||||
smokeEndTime = time;
|
||||
|
||||
double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime);
|
||||
LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed;
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
protected struct SmokePoint
|
||||
{
|
||||
public Vector2 Position;
|
||||
public double Time;
|
||||
public Vector2 Direction;
|
||||
|
||||
public struct UpperBoundComparer : IComparer<SmokePoint>
|
||||
{
|
||||
public int Compare(SmokePoint x, SmokePoint target)
|
||||
{
|
||||
// By returning -1 when the target value is equal to x, guarantees that the
|
||||
// element at BinarySearch's returned index will always be the first element
|
||||
// larger. Since 0 is never returned, the target is never "found", so the return
|
||||
// value will be the index's complement.
|
||||
|
||||
return x.Time > target.Time ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class SmokeDrawNode : TexturedShaderDrawNode
|
||||
{
|
||||
protected new SmokeSegment Source => (SmokeSegment)base.Source;
|
||||
|
||||
protected double SmokeStartTime { get; private set; }
|
||||
protected double SmokeEndTime { get; private set; }
|
||||
protected double CurrentTime { get; private set; }
|
||||
|
||||
private readonly List<SmokePoint> points = new List<SmokePoint>();
|
||||
private IVertexBatch<TexturedVertex2D>? quadBatch;
|
||||
private float radius;
|
||||
private Vector2 drawSize;
|
||||
private Texture? texture;
|
||||
|
||||
// anim calculation vars (color, scale, direction)
|
||||
private double initialFadeOutDurationTrunc;
|
||||
private double firstVisiblePointTime;
|
||||
|
||||
private double initialFadeOutTime;
|
||||
private double reFadeInTime;
|
||||
private double finalFadeOutTime;
|
||||
|
||||
private Random rotationRNG = new Random();
|
||||
|
||||
public SmokeDrawNode(ITexturedShaderDrawable source)
|
||||
: base(source)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
points.Clear();
|
||||
points.AddRange(Source.SmokePoints);
|
||||
|
||||
radius = Source.radius;
|
||||
drawSize = Source.DrawSize;
|
||||
texture = Source.Texture;
|
||||
|
||||
SmokeStartTime = Source.smokeStartTime;
|
||||
SmokeEndTime = Source.smokeEndTime;
|
||||
CurrentTime = Source.Clock.CurrentTime;
|
||||
|
||||
rotationRNG = new Random(Source.rotationSeed);
|
||||
|
||||
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
|
||||
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
|
||||
|
||||
initialFadeOutTime = CurrentTime;
|
||||
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
|
||||
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
|
||||
}
|
||||
|
||||
public sealed override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
if (points.Count == 0)
|
||||
return;
|
||||
|
||||
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10);
|
||||
texture ??= renderer.WhitePixel;
|
||||
RectangleF textureRect = texture.GetTextureRect();
|
||||
|
||||
var shader = GetAppropriateShader(renderer);
|
||||
|
||||
renderer.SetBlend(BlendingParameters.Additive);
|
||||
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||
|
||||
shader.Bind();
|
||||
texture.Bind();
|
||||
|
||||
foreach (var point in points)
|
||||
drawPointQuad(point, textureRect);
|
||||
|
||||
shader.Unbind();
|
||||
renderer.PopLocalMatrix();
|
||||
}
|
||||
|
||||
protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
|
||||
? ((SRGBColour)DrawColourInfo.Colour).Linear
|
||||
: DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear;
|
||||
|
||||
protected virtual Color4 PointColour(SmokePoint point)
|
||||
{
|
||||
var color = Color4.White;
|
||||
|
||||
double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
|
||||
|
||||
if (timeDoingInitialFadeOut > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
|
||||
color.A = (1 - fraction) * initial_alpha;
|
||||
}
|
||||
|
||||
if (color.A > 0)
|
||||
{
|
||||
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
|
||||
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
|
||||
|
||||
if (timeDoingFinalFadeOut > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
|
||||
fraction = MathF.Pow(fraction, 5);
|
||||
color.A = (1 - fraction) * re_fade_in_alpha;
|
||||
}
|
||||
else if (timeDoingReFadeIn > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
protected virtual float PointScale(SmokePoint point)
|
||||
{
|
||||
double timeDoingScale = CurrentTime - point.Time;
|
||||
float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
return fraction * (final_scale - initial_scale) + initial_scale;
|
||||
}
|
||||
|
||||
protected virtual Vector2 PointDirection(SmokePoint point)
|
||||
{
|
||||
float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
|
||||
float finalAngle = initialAngle + nextRotation();
|
||||
|
||||
double timeDoingRotation = CurrentTime - point.Time;
|
||||
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
|
||||
fraction = 1 - MathF.Pow(1 - fraction, 5);
|
||||
float angle = fraction * (finalAngle - initialAngle) + initialAngle;
|
||||
|
||||
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
|
||||
}
|
||||
|
||||
private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
|
||||
|
||||
private void drawPointQuad(SmokePoint point, RectangleF textureRect)
|
||||
{
|
||||
Debug.Assert(quadBatch != null);
|
||||
|
||||
var colour = PointColour(point);
|
||||
float scale = PointScale(point);
|
||||
var dir = PointDirection(point);
|
||||
var ortho = dir.PerpendicularLeft;
|
||||
|
||||
if (colour.A == 0 || scale == 0)
|
||||
return;
|
||||
|
||||
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
|
||||
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
|
||||
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
|
||||
var localBotRight = point.Position + (radius * scale * (ortho + dir));
|
||||
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localTopLeft,
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localTopRight,
|
||||
TexturePosition = textureRect.TopRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localBotRight,
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
{
|
||||
Position = localBotLeft,
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour),
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
quadBatch?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
|
||||
|
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal file
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages smoke trails generated from user input.
|
||||
/// </summary>
|
||||
public class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
private SmokeSkinnableDrawable? currentSegmentSkinnable;
|
||||
|
||||
private Vector2 lastMousePosition;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 _) => true;
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
|
||||
|
||||
// Add initial position immediately.
|
||||
addPosition();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
if (currentSegmentSkinnable?.Drawable is SmokeSegment segment)
|
||||
{
|
||||
segment.FinishDrawing(Time.Current);
|
||||
currentSegmentSkinnable = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
lastMousePosition = e.MousePosition;
|
||||
addPosition();
|
||||
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
|
||||
|
||||
private class SmokeSkinnableDrawable : SkinnableDrawable
|
||||
{
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
public override double LifetimeStart => Drawable.LifetimeStart;
|
||||
public override double LifetimeEnd => Drawable.LifetimeEnd;
|
||||
|
||||
public SmokeSkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(component, defaultImplementation, confineMode)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
addSeekStep(3000);
|
||||
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
|
||||
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
|
||||
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15);
|
||||
AddStep("clear results", () => Player.Results.Clear());
|
||||
addSeekStep(0);
|
||||
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
|
||||
|
@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy
|
||||
[IgnoreMember]
|
||||
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
|
||||
|
||||
[JsonIgnore]
|
||||
[IgnoreMember]
|
||||
public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke);
|
||||
|
||||
[Key(3)]
|
||||
public ReplayButtonState ButtonState;
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.15.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="Sentry" Version="3.20.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
|
@ -61,8 +61,8 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||
<PropertyGroup>
|
||||
|
Loading…
Reference in New Issue
Block a user