mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 11:42:56 +08:00
Merge pull request #14759 from Opelkuh/add-legacy-star-particles
Implement legacy osu! cursor particles
This commit is contained in:
commit
9ec927a688
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ContentModelUserStore">
|
<component name="UserContentModel">
|
||||||
<attachedFolders />
|
<attachedFolders />
|
||||||
<explicitIncludes />
|
<explicitIncludes />
|
||||||
<explicitExcludes />
|
<explicitExcludes />
|
||||||
|
174
osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
Normal file
174
osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class TestSceneCursorParticles : TestSceneOsuPlayer
|
||||||
|
{
|
||||||
|
protected override bool Autoplay => autoplay;
|
||||||
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
|
private bool autoplay;
|
||||||
|
private IBeatmap currentBeatmap;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentBeatmap ?? base.CreateBeatmap(ruleset);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyBreakParticles()
|
||||||
|
{
|
||||||
|
LegacyCursorParticles cursorParticles = null;
|
||||||
|
|
||||||
|
createLegacyTest(false, () => new Beatmap
|
||||||
|
{
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(8500, 10000),
|
||||||
|
},
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 8000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 11000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("fetch cursor particles", () =>
|
||||||
|
{
|
||||||
|
cursorParticles = this.ChildrenOfType<LegacyCursorParticles>().SingleOrDefault();
|
||||||
|
return cursorParticles != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("press right mouse button", () => InputManager.PressButton(MouseButton.Right));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("release right mouse button", () => InputManager.ReleaseButton(MouseButton.Right));
|
||||||
|
|
||||||
|
AddUntilStep("wait for beatmap start", () => !Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for break", () => Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for break end", () => !Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyKiaiParticles()
|
||||||
|
{
|
||||||
|
LegacyCursorParticles cursorParticles = null;
|
||||||
|
DrawableSpinner spinner = null;
|
||||||
|
DrawableSlider slider = null;
|
||||||
|
|
||||||
|
createLegacyTest(true, () =>
|
||||||
|
{
|
||||||
|
var controlPointInfo = new ControlPointInfo();
|
||||||
|
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||||
|
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPointInfo,
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Spinner
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Duration = 1000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = 2500,
|
||||||
|
RepeatCount = 0,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(Vector2.Zero),
|
||||||
|
new PathControlPoint(new Vector2(100, 0)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 4500,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AddUntilStep("fetch cursor particles", () =>
|
||||||
|
{
|
||||||
|
cursorParticles = this.ChildrenOfType<LegacyCursorParticles>().SingleOrDefault();
|
||||||
|
return cursorParticles != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for spinner tracking", () =>
|
||||||
|
{
|
||||||
|
spinner = this.ChildrenOfType<DrawableSpinner>().SingleOrDefault();
|
||||||
|
return spinner?.RotationTracker.Tracking == true;
|
||||||
|
});
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("spinner tracking stopped", () => !spinner.RotationTracker.Tracking);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for slider tracking", () =>
|
||||||
|
{
|
||||||
|
slider = this.ChildrenOfType<DrawableSlider>().SingleOrDefault();
|
||||||
|
return slider?.Tracking.Value == true;
|
||||||
|
});
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("slider tracking stopped", () => !slider.Tracking.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createLegacyTest(bool autoplay, Func<IBeatmap> beatmap) => CreateTest(() =>
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () =>
|
||||||
|
{
|
||||||
|
this.autoplay = autoplay;
|
||||||
|
currentBeatmap = beatmap();
|
||||||
|
});
|
||||||
|
AddStep("setup default legacy skin", () =>
|
||||||
|
{
|
||||||
|
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
FollowPoint,
|
FollowPoint,
|
||||||
Cursor,
|
Cursor,
|
||||||
CursorTrail,
|
CursorTrail,
|
||||||
|
CursorParticles,
|
||||||
SliderScorePoint,
|
SliderScorePoint,
|
||||||
ReverseArrow,
|
ReverseArrow,
|
||||||
HitCircleText,
|
HitCircleText,
|
||||||
|
256
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
Normal file
256
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
|
{
|
||||||
|
public class LegacyCursorParticles : CompositeDrawable, IKeyBindingHandler<OsuAction>
|
||||||
|
{
|
||||||
|
public bool Active => breakSpewer?.Active.Value == true || kiaiSpewer?.Active.Value == true;
|
||||||
|
|
||||||
|
private LegacyCursorParticleSpewer breakSpewer;
|
||||||
|
private LegacyCursorParticleSpewer kiaiSpewer;
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private Player player { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private OsuPlayfield playfield { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private GameplayBeatmap gameplayBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private GameplayClock gameplayClock { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin, OsuColour colours)
|
||||||
|
{
|
||||||
|
var texture = skin.GetTexture("star2");
|
||||||
|
var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);
|
||||||
|
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||||
|
texture.ScaleAdjust *= 1.6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
breakSpewer = new LegacyCursorParticleSpewer(texture, 20)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = starBreakAdditive,
|
||||||
|
Direction = SpewDirection.None,
|
||||||
|
},
|
||||||
|
kiaiSpewer = new LegacyCursorParticleSpewer(texture, 60)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = starBreakAdditive,
|
||||||
|
Direction = SpewDirection.None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
((IBindable<bool>)breakSpewer.Active).BindTo(player.IsBreakTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
if (playfield == null || gameplayBeatmap == null) return;
|
||||||
|
|
||||||
|
DrawableHitObject kiaiHitObject = null;
|
||||||
|
|
||||||
|
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
|
||||||
|
if (gameplayBeatmap.ControlPointInfo.EffectPointAt(gameplayBeatmap.Time.Current).KiaiMode)
|
||||||
|
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
|
||||||
|
|
||||||
|
kiaiSpewer.Active.Value = kiaiHitObject != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isTracking(DrawableHitObject h)
|
||||||
|
{
|
||||||
|
if (!h.HitObject.Kiai)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (h)
|
||||||
|
{
|
||||||
|
case DrawableSlider slider:
|
||||||
|
return slider.Tracking.Value;
|
||||||
|
|
||||||
|
case DrawableSpinner spinner:
|
||||||
|
return spinner.RotationTracker.Tracking;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||||
|
{
|
||||||
|
handleInput(e.Action, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
||||||
|
{
|
||||||
|
handleInput(e.Action, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool leftPressed;
|
||||||
|
private bool rightPressed;
|
||||||
|
|
||||||
|
private void handleInput(OsuAction action, bool pressed)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case OsuAction.LeftButton:
|
||||||
|
leftPressed = pressed;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuAction.RightButton:
|
||||||
|
rightPressed = pressed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftPressed && rightPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Omni;
|
||||||
|
else if (leftPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Left;
|
||||||
|
else if (rightPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Right;
|
||||||
|
else
|
||||||
|
breakSpewer.Direction = SpewDirection.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LegacyCursorParticleSpewer : ParticleSpewer, IRequireHighFrequencyMousePosition
|
||||||
|
{
|
||||||
|
private const int particle_duration_min = 300;
|
||||||
|
private const int particle_duration_max = 1000;
|
||||||
|
|
||||||
|
public SpewDirection Direction { get; set; }
|
||||||
|
|
||||||
|
protected override bool CanSpawnParticles => base.CanSpawnParticles && cursorScreenPosition.HasValue;
|
||||||
|
protected override float ParticleGravity => 240;
|
||||||
|
|
||||||
|
public LegacyCursorParticleSpewer(Texture texture, int perSecond)
|
||||||
|
: base(texture, perSecond, particle_duration_max)
|
||||||
|
{
|
||||||
|
Active.BindValueChanged(_ => resetVelocityCalculation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2? cursorScreenPosition;
|
||||||
|
private Vector2 cursorVelocity;
|
||||||
|
|
||||||
|
private const double max_velocity_frame_length = 15;
|
||||||
|
private double velocityFrameLength;
|
||||||
|
private Vector2 totalPosDifference;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
{
|
||||||
|
if (cursorScreenPosition == null)
|
||||||
|
{
|
||||||
|
cursorScreenPosition = e.ScreenSpaceMousePosition;
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate cursor velocity.
|
||||||
|
totalPosDifference += e.ScreenSpaceMousePosition - cursorScreenPosition.Value;
|
||||||
|
cursorScreenPosition = e.ScreenSpaceMousePosition;
|
||||||
|
|
||||||
|
velocityFrameLength += Math.Abs(Clock.ElapsedFrameTime);
|
||||||
|
|
||||||
|
if (velocityFrameLength > max_velocity_frame_length)
|
||||||
|
{
|
||||||
|
cursorVelocity = totalPosDifference / (float)velocityFrameLength;
|
||||||
|
|
||||||
|
totalPosDifference = Vector2.Zero;
|
||||||
|
velocityFrameLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetVelocityCalculation()
|
||||||
|
{
|
||||||
|
cursorScreenPosition = null;
|
||||||
|
totalPosDifference = Vector2.Zero;
|
||||||
|
velocityFrameLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override FallingParticle CreateParticle() =>
|
||||||
|
new FallingParticle
|
||||||
|
{
|
||||||
|
StartPosition = ToLocalSpace(cursorScreenPosition ?? Vector2.Zero),
|
||||||
|
Duration = RNG.NextSingle(particle_duration_min, particle_duration_max),
|
||||||
|
StartAngle = (float)(RNG.NextDouble() * 4 - 2),
|
||||||
|
EndAngle = RNG.NextSingle(-2f, 2f),
|
||||||
|
EndScale = RNG.NextSingle(2f),
|
||||||
|
Velocity = getVelocity(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private Vector2 getVelocity()
|
||||||
|
{
|
||||||
|
Vector2 velocity = Vector2.Zero;
|
||||||
|
|
||||||
|
switch (Direction)
|
||||||
|
{
|
||||||
|
case SpewDirection.Left:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-460f, 0),
|
||||||
|
RNG.NextSingle(-40f, 40f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpewDirection.Right:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(0, 460f),
|
||||||
|
RNG.NextSingle(-40f, 40f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpewDirection.Omni:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-460f, 460f),
|
||||||
|
RNG.NextSingle(-160f, 160f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
velocity += cursorVelocity * 40;
|
||||||
|
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SpewDirection
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Omni,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -89,6 +89,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorParticles:
|
||||||
|
if (GetTexture("star2") != null)
|
||||||
|
return new LegacyCursorParticles();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.HitCircleText:
|
case OsuSkinComponents.HitCircleText:
|
||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
return null;
|
return null;
|
||||||
|
@ -9,5 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
SliderBorder,
|
SliderBorder,
|
||||||
SliderBall,
|
SliderBall,
|
||||||
SpinnerBackground,
|
SpinnerBackground,
|
||||||
|
StarBreakAdditive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
InternalChild = fadeContainer = new Container
|
InternalChild = fadeContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling)
|
Children = new[]
|
||||||
|
{
|
||||||
|
cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||||
|
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public class OsuPlayfield : Playfield
|
public class OsuPlayfield : Playfield
|
||||||
{
|
{
|
||||||
private readonly PlayfieldBorder playfieldBorder;
|
private readonly PlayfieldBorder playfieldBorder;
|
||||||
|
128
osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
Normal file
128
osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.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.Graphics;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneParticleSpewer : OsuTestScene
|
||||||
|
{
|
||||||
|
private TestParticleSpewer spewer;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = spewer = createSpewer();
|
||||||
|
|
||||||
|
AddToggleStep("toggle spawning", value => spewer.Active.Value = value);
|
||||||
|
AddSliderStep("particle gravity", 0f, 1f, 0f, value => spewer.Gravity = value);
|
||||||
|
AddSliderStep("particle velocity", 0f, 1f, 0.5f, value => spewer.MaxVelocity = value);
|
||||||
|
AddStep("move to new location", () =>
|
||||||
|
{
|
||||||
|
spewer.TransformTo(nameof(spewer.SpawnPosition), new Vector2(RNG.NextSingle(), RNG.NextSingle()), 1000, Easing.Out);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("create spewer", () => Child = spewer = createSpewer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPresence()
|
||||||
|
{
|
||||||
|
AddStep("start spewer", () => spewer.Active.Value = true);
|
||||||
|
AddAssert("is present", () => spewer.IsPresent);
|
||||||
|
|
||||||
|
AddWaitStep("wait for some particles", 3);
|
||||||
|
AddStep("stop spewer", () => spewer.Active.Value = false);
|
||||||
|
|
||||||
|
AddWaitStep("wait for clean screen", 8);
|
||||||
|
AddAssert("is not present", () => !spewer.IsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTimeJumps()
|
||||||
|
{
|
||||||
|
ManualClock testClock = new ManualClock();
|
||||||
|
|
||||||
|
AddStep("prepare clock", () =>
|
||||||
|
{
|
||||||
|
testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -3;
|
||||||
|
spewer.Clock = new FramedClock(testClock);
|
||||||
|
});
|
||||||
|
AddStep("start spewer", () => spewer.Active.Value = true);
|
||||||
|
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1);
|
||||||
|
|
||||||
|
AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
|
||||||
|
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2);
|
||||||
|
|
||||||
|
AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
|
||||||
|
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestParticleSpewer createSpewer() =>
|
||||||
|
new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativePositionAxes = Axes.Both,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Position = new Vector2(0.5f),
|
||||||
|
Size = new Vector2(0.5f),
|
||||||
|
};
|
||||||
|
|
||||||
|
private class TestParticleSpewer : ParticleSpewer
|
||||||
|
{
|
||||||
|
public const int MAX_DURATION = 1500;
|
||||||
|
private const int rate = 250;
|
||||||
|
|
||||||
|
public int TotalCreatedParticles { get; private set; }
|
||||||
|
|
||||||
|
public float Gravity;
|
||||||
|
|
||||||
|
public float MaxVelocity = 0.25f;
|
||||||
|
|
||||||
|
public Vector2 SpawnPosition { get; set; } = new Vector2(0.5f);
|
||||||
|
|
||||||
|
protected override float ParticleGravity => Gravity;
|
||||||
|
|
||||||
|
public TestParticleSpewer(Texture texture)
|
||||||
|
: base(texture, rate, MAX_DURATION)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override FallingParticle CreateParticle()
|
||||||
|
{
|
||||||
|
TotalCreatedParticles++;
|
||||||
|
|
||||||
|
return new FallingParticle
|
||||||
|
{
|
||||||
|
Velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-MaxVelocity, MaxVelocity),
|
||||||
|
RNG.NextSingle(-MaxVelocity, MaxVelocity)
|
||||||
|
),
|
||||||
|
StartPosition = SpawnPosition,
|
||||||
|
Duration = RNG.NextSingle(MAX_DURATION),
|
||||||
|
StartAngle = RNG.NextSingle(MathF.PI * 2),
|
||||||
|
EndAngle = RNG.NextSingle(MathF.PI * 2),
|
||||||
|
EndScale = RNG.NextSingle(0.5f, 1.5f)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
198
osu.Game/Graphics/ParticleSpewer.cs
Normal file
198
osu.Game/Graphics/ParticleSpewer.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics
|
||||||
|
{
|
||||||
|
public abstract class ParticleSpewer : Sprite
|
||||||
|
{
|
||||||
|
private readonly FallingParticle[] particles;
|
||||||
|
private int currentIndex;
|
||||||
|
private double lastParticleAdded;
|
||||||
|
|
||||||
|
private readonly double cooldown;
|
||||||
|
private readonly double maxDuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether particles are being spawned.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableBool Active = new BindableBool();
|
||||||
|
|
||||||
|
public override bool IsPresent => base.IsPresent && hasActiveParticles;
|
||||||
|
|
||||||
|
protected virtual bool CanSpawnParticles => true;
|
||||||
|
protected virtual float ParticleGravity => 0;
|
||||||
|
|
||||||
|
private bool hasActiveParticles => Active.Value || (lastParticleAdded + maxDuration) > Time.Current;
|
||||||
|
|
||||||
|
protected ParticleSpewer(Texture texture, int perSecond, double maxDuration)
|
||||||
|
{
|
||||||
|
Texture = texture;
|
||||||
|
Blending = BlendingParameters.Additive;
|
||||||
|
|
||||||
|
particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)];
|
||||||
|
|
||||||
|
cooldown = 1000f / perSecond;
|
||||||
|
this.maxDuration = maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown)
|
||||||
|
{
|
||||||
|
var newParticle = CreateParticle();
|
||||||
|
newParticle.StartTime = (float)Time.Current;
|
||||||
|
|
||||||
|
particles[currentIndex] = newParticle;
|
||||||
|
|
||||||
|
currentIndex = (currentIndex + 1) % particles.Length;
|
||||||
|
lastParticleAdded = Time.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invalidate(Invalidation.DrawNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called each time a new particle should be spawned.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual FallingParticle CreateParticle() => new FallingParticle();
|
||||||
|
|
||||||
|
protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this);
|
||||||
|
|
||||||
|
# region DrawNode
|
||||||
|
|
||||||
|
private class ParticleSpewerDrawNode : SpriteDrawNode
|
||||||
|
{
|
||||||
|
private readonly FallingParticle[] particles;
|
||||||
|
|
||||||
|
protected new ParticleSpewer Source => (ParticleSpewer)base.Source;
|
||||||
|
|
||||||
|
private readonly float maxDuration;
|
||||||
|
|
||||||
|
private float currentTime;
|
||||||
|
private float gravity;
|
||||||
|
private Axes relativePositionAxes;
|
||||||
|
private Vector2 sourceSize;
|
||||||
|
|
||||||
|
public ParticleSpewerDrawNode(ParticleSpewer source)
|
||||||
|
: base(source)
|
||||||
|
{
|
||||||
|
particles = new FallingParticle[Source.particles.Length];
|
||||||
|
maxDuration = (float)Source.maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyState()
|
||||||
|
{
|
||||||
|
base.ApplyState();
|
||||||
|
|
||||||
|
Source.particles.CopyTo(particles, 0);
|
||||||
|
|
||||||
|
currentTime = (float)Source.Time.Current;
|
||||||
|
gravity = Source.ParticleGravity;
|
||||||
|
relativePositionAxes = Source.RelativePositionAxes;
|
||||||
|
sourceSize = Source.DrawSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||||
|
{
|
||||||
|
foreach (var p in particles)
|
||||||
|
{
|
||||||
|
var timeSinceStart = currentTime - p.StartTime;
|
||||||
|
|
||||||
|
// ignore particles from the future.
|
||||||
|
// these can appear when seeking in replays.
|
||||||
|
if (timeSinceStart < 0) continue;
|
||||||
|
|
||||||
|
var alpha = p.AlphaAtTime(timeSinceStart);
|
||||||
|
if (alpha <= 0) continue;
|
||||||
|
|
||||||
|
var pos = p.PositionAtTime(timeSinceStart, gravity, maxDuration);
|
||||||
|
var scale = p.ScaleAtTime(timeSinceStart);
|
||||||
|
var angle = p.AngleAtTime(timeSinceStart);
|
||||||
|
|
||||||
|
var rect = createDrawRect(pos, scale);
|
||||||
|
|
||||||
|
var quad = new Quad(
|
||||||
|
transformPosition(rect.TopLeft, rect.Centre, angle),
|
||||||
|
transformPosition(rect.TopRight, rect.Centre, angle),
|
||||||
|
transformPosition(rect.BottomLeft, rect.Centre, angle),
|
||||||
|
transformPosition(rect.BottomRight, rect.Centre, angle)
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
|
||||||
|
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||||
|
null, TextureCoords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RectangleF createDrawRect(Vector2 position, float scale)
|
||||||
|
{
|
||||||
|
var width = Texture.DisplayWidth * scale;
|
||||||
|
var height = Texture.DisplayHeight * scale;
|
||||||
|
|
||||||
|
if (relativePositionAxes.HasFlagFast(Axes.X))
|
||||||
|
position.X *= sourceSize.X;
|
||||||
|
if (relativePositionAxes.HasFlagFast(Axes.Y))
|
||||||
|
position.Y *= sourceSize.Y;
|
||||||
|
|
||||||
|
return new RectangleF(
|
||||||
|
position.X - width / 2,
|
||||||
|
position.Y - height / 2,
|
||||||
|
width,
|
||||||
|
height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 transformPosition(Vector2 pos, Vector2 centre, float angle)
|
||||||
|
{
|
||||||
|
float cos = MathF.Cos(angle);
|
||||||
|
float sin = MathF.Sin(angle);
|
||||||
|
|
||||||
|
float x = centre.X + (pos.X - centre.X) * cos + (pos.Y - centre.Y) * sin;
|
||||||
|
float y = centre.Y + (pos.Y - centre.Y) * cos - (pos.X - centre.X) * sin;
|
||||||
|
|
||||||
|
return Vector2Extensions.Transform(new Vector2(x, y), DrawInfo.Matrix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected struct FallingParticle
|
||||||
|
{
|
||||||
|
public float StartTime;
|
||||||
|
public Vector2 StartPosition;
|
||||||
|
public Vector2 Velocity;
|
||||||
|
public float Duration;
|
||||||
|
public float StartAngle;
|
||||||
|
public float EndAngle;
|
||||||
|
public float EndScale;
|
||||||
|
|
||||||
|
public float AlphaAtTime(float timeSinceStart) => 1 - progressAtTime(timeSinceStart);
|
||||||
|
|
||||||
|
public float ScaleAtTime(float timeSinceStart) => (float)Interpolation.Lerp(1, EndScale, progressAtTime(timeSinceStart));
|
||||||
|
|
||||||
|
public float AngleAtTime(float timeSinceStart) => (float)Interpolation.Lerp(StartAngle, EndAngle, progressAtTime(timeSinceStart));
|
||||||
|
|
||||||
|
public Vector2 PositionAtTime(float timeSinceStart, float gravity, float maxDuration)
|
||||||
|
{
|
||||||
|
var progress = progressAtTime(timeSinceStart);
|
||||||
|
var currentGravity = new Vector2(0, gravity * Duration / maxDuration * progress);
|
||||||
|
|
||||||
|
return StartPosition + (Velocity + currentGravity) * timeSinceStart / maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float progressAtTime(float timeSinceStart) => Math.Clamp(timeSinceStart / Duration, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user