mirror of
https://github.com/ppy/osu.git
synced 2025-03-28 09:37:23 +08:00
Implement disjoint (old style) cursor trail (#6046)
Implement disjoint (old style) cursor trail Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
commit
d571ad4a0b
128
osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
Normal file
128
osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Testing.Input;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class TestSceneCursorTrail : OsuTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestSmoothCursorTrail()
|
||||||
|
{
|
||||||
|
Container scalingContainer = null;
|
||||||
|
|
||||||
|
createTest(() => scalingContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new CursorTrail()
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set large scale", () => scalingContainer.Scale = new Vector2(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacySmoothCursorTrail()
|
||||||
|
{
|
||||||
|
createTest(() => new LegacySkinContainer(false)
|
||||||
|
{
|
||||||
|
Child = new LegacyCursorTrail()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyDisjointCursorTrail()
|
||||||
|
{
|
||||||
|
createTest(() => new LegacySkinContainer(true)
|
||||||
|
{
|
||||||
|
Child = new LegacyCursorTrail()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
|
||||||
|
Add(new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = new Vector2(0.8f),
|
||||||
|
Child = new MovingCursorInputManager { Child = createContent?.Invoke() }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[Cached(typeof(ISkinSource))]
|
||||||
|
private class LegacySkinContainer : Container, ISkinSource
|
||||||
|
{
|
||||||
|
private readonly bool disjoint;
|
||||||
|
|
||||||
|
public LegacySkinContainer(bool disjoint)
|
||||||
|
{
|
||||||
|
this.disjoint = disjoint;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Texture GetTexture(string componentName)
|
||||||
|
{
|
||||||
|
switch (componentName)
|
||||||
|
{
|
||||||
|
case "cursortrail":
|
||||||
|
var tex = new Texture(Texture.WhitePixel.TextureGL);
|
||||||
|
|
||||||
|
if (disjoint)
|
||||||
|
tex.ScaleAdjust = 1 / 25f;
|
||||||
|
return tex;
|
||||||
|
|
||||||
|
case "cursormiddle":
|
||||||
|
return disjoint ? null : Texture.WhitePixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public event Action SourceChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MovingCursorInputManager : ManualInputManager
|
||||||
|
{
|
||||||
|
public MovingCursorInputManager()
|
||||||
|
{
|
||||||
|
UseParentInput = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
const double spin_duration = 1000;
|
||||||
|
double currentTime = Time.Current;
|
||||||
|
|
||||||
|
double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI;
|
||||||
|
Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
|
||||||
|
|
||||||
|
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
HitCircle,
|
HitCircle,
|
||||||
FollowPoint,
|
FollowPoint,
|
||||||
Cursor,
|
Cursor,
|
||||||
|
CursorTrail,
|
||||||
SliderScorePoint,
|
SliderScorePoint,
|
||||||
ApproachCircle,
|
ApproachCircle,
|
||||||
ReverseArrow,
|
ReverseArrow,
|
||||||
|
55
osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
Normal file
55
osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// 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;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning
|
||||||
|
{
|
||||||
|
public class LegacyCursorTrail : CursorTrail
|
||||||
|
{
|
||||||
|
private const double disjoint_trail_time_separation = 1000 / 60.0;
|
||||||
|
|
||||||
|
private bool disjointTrail;
|
||||||
|
private double lastTrailTime;
|
||||||
|
|
||||||
|
public LegacyCursorTrail()
|
||||||
|
{
|
||||||
|
Blending = BlendingParameters.Additive;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin)
|
||||||
|
{
|
||||||
|
Texture = skin.GetTexture("cursortrail");
|
||||||
|
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
||||||
|
|
||||||
|
if (Texture != null)
|
||||||
|
{
|
||||||
|
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||||
|
Texture.ScaleAdjust *= 1.6f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
||||||
|
|
||||||
|
protected override bool InterpolateMovements => !disjointTrail;
|
||||||
|
|
||||||
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
{
|
||||||
|
if (!disjointTrail)
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
|
||||||
|
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
|
||||||
|
{
|
||||||
|
lastTrailTime = Time.Current;
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,6 +78,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorTrail:
|
||||||
|
if (source.GetTexture("cursortrail") != null)
|
||||||
|
return new LegacyCursorTrail();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.HitCircleText:
|
case OsuSkinComponents.HitCircleText:
|
||||||
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
|
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
|
||||||
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0;
|
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0;
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Batches;
|
using osu.Framework.Graphics.Batches;
|
||||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||||
@ -20,14 +21,13 @@ using osuTK.Graphics.ES30;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||||
{
|
{
|
||||||
internal class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
|
public class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
|
||||||
{
|
{
|
||||||
private const int max_sprites = 2048;
|
private const int max_sprites = 2048;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
private int currentIndex;
|
private int currentIndex;
|
||||||
private IShader shader;
|
private IShader shader;
|
||||||
private Texture texture;
|
|
||||||
private double timeOffset;
|
private double timeOffset;
|
||||||
private float time;
|
private float time;
|
||||||
|
|
||||||
@ -47,11 +47,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ShaderManager shaders, TextureStore textures)
|
private void load(ShaderManager shaders)
|
||||||
{
|
{
|
||||||
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
|
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
|
||||||
texture = textures.Get(@"Cursor/cursortrail");
|
|
||||||
Scale = new Vector2(1 / texture.ScaleAdjust);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -60,6 +58,40 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
resetTime();
|
resetTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Texture texture = Texture.WhitePixel;
|
||||||
|
|
||||||
|
public Texture Texture
|
||||||
|
{
|
||||||
|
get => texture;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (texture == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
texture = value;
|
||||||
|
Invalidate(Invalidation.DrawNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Cached<Vector2> partSizeCache = new Cached<Vector2>();
|
||||||
|
|
||||||
|
private Vector2 partSize => partSizeCache.IsValid
|
||||||
|
? partSizeCache.Value
|
||||||
|
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
|
||||||
|
|
||||||
|
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
|
||||||
|
{
|
||||||
|
if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence)) > 0)
|
||||||
|
partSizeCache.Invalidate();
|
||||||
|
|
||||||
|
return base.Invalidate(invalidation, source, shallPropagate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of time to fade the cursor trail pieces.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double FadeDuration => 300;
|
||||||
|
|
||||||
public override bool IsPresent => true;
|
public override bool IsPresent => true;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -70,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
const int fade_clock_reset_threshold = 1000000;
|
const int fade_clock_reset_threshold = 1000000;
|
||||||
|
|
||||||
time = (float)(Time.Current - timeOffset) / 300f;
|
time = (float)((Time.Current - timeOffset) / FadeDuration);
|
||||||
if (time > fade_clock_reset_threshold)
|
if (time > fade_clock_reset_threshold)
|
||||||
resetTime();
|
resetTime();
|
||||||
}
|
}
|
||||||
@ -87,7 +119,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
timeOffset = Time.Current;
|
timeOffset = Time.Current;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 size => texture.Size * Scale;
|
/// <summary>
|
||||||
|
/// Whether to interpolate mouse movements and add trail pieces at intermediate points.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool InterpolateMovements => true;
|
||||||
|
|
||||||
private Vector2? lastPosition;
|
private Vector2? lastPosition;
|
||||||
private readonly InputResampler resampler = new InputResampler();
|
private readonly InputResampler resampler = new InputResampler();
|
||||||
@ -109,29 +144,41 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
Trace.Assert(lastPosition.HasValue);
|
Trace.Assert(lastPosition.HasValue);
|
||||||
|
|
||||||
// ReSharper disable once PossibleInvalidOperationException
|
if (InterpolateMovements)
|
||||||
Vector2 pos1 = lastPosition.Value;
|
|
||||||
Vector2 diff = pos2 - pos1;
|
|
||||||
float distance = diff.Length;
|
|
||||||
Vector2 direction = diff / distance;
|
|
||||||
|
|
||||||
float interval = size.X / 2 * 0.9f;
|
|
||||||
|
|
||||||
for (float d = interval; d < distance; d += interval)
|
|
||||||
{
|
{
|
||||||
lastPosition = pos1 + direction * d;
|
// ReSharper disable once PossibleInvalidOperationException
|
||||||
|
Vector2 pos1 = lastPosition.Value;
|
||||||
|
Vector2 diff = pos2 - pos1;
|
||||||
|
float distance = diff.Length;
|
||||||
|
Vector2 direction = diff / distance;
|
||||||
|
|
||||||
parts[currentIndex].Position = lastPosition.Value;
|
float interval = partSize.X / 2.5f;
|
||||||
parts[currentIndex].Time = time;
|
|
||||||
++parts[currentIndex].InvalidationID;
|
|
||||||
|
|
||||||
currentIndex = (currentIndex + 1) % max_sprites;
|
for (float d = interval; d < distance; d += interval)
|
||||||
|
{
|
||||||
|
lastPosition = pos1 + direction * d;
|
||||||
|
addPart(lastPosition.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastPosition = pos2;
|
||||||
|
addPart(lastPosition.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnMouseMove(e);
|
return base.OnMouseMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addPart(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
parts[currentIndex].Position = screenSpacePosition;
|
||||||
|
parts[currentIndex].Time = time;
|
||||||
|
++parts[currentIndex].InvalidationID;
|
||||||
|
|
||||||
|
currentIndex = (currentIndex + 1) % max_sprites;
|
||||||
|
}
|
||||||
|
|
||||||
protected override DrawNode CreateDrawNode() => new TrailDrawNode(this);
|
protected override DrawNode CreateDrawNode() => new TrailDrawNode(this);
|
||||||
|
|
||||||
private struct TrailPart
|
private struct TrailPart
|
||||||
@ -168,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
shader = Source.shader;
|
shader = Source.shader;
|
||||||
texture = Source.texture;
|
texture = Source.texture;
|
||||||
size = Source.size;
|
size = Source.partSize;
|
||||||
time = Source.time;
|
time = Source.time;
|
||||||
|
|
||||||
for (int i = 0; i < Source.parts.Length; ++i)
|
for (int i = 0; i < Source.parts.Length; ++i)
|
||||||
|
@ -6,9 +6,12 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Osu.Configuration;
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||||
{
|
{
|
||||||
@ -22,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
||||||
|
|
||||||
private readonly CursorTrail cursorTrail;
|
private readonly Drawable cursorTrail;
|
||||||
|
|
||||||
public OsuCursorContainer()
|
public OsuCursorContainer()
|
||||||
{
|
{
|
||||||
InternalChild = fadeContainer = new Container
|
InternalChild = fadeContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Child = cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling)
|
||||||
{
|
|
||||||
cursorTrail = new CursorTrail { Depth = 1 }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,5 +98,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint);
|
fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint);
|
||||||
ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint);
|
ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class DefaultCursorTrail : CursorTrail
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(TextureStore textures)
|
||||||
|
{
|
||||||
|
Texture = textures.Get(@"Cursor/cursortrail");
|
||||||
|
Scale = new Vector2(1 / Texture.ScaleAdjust);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user