1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-16 13:02:55 +08:00

Merge pull request #12012 from bdach/taiko-explosion-pooling

This commit is contained in:
Dean Herbert 2021-03-18 21:09:09 +09:00 committed by GitHub
commit e94644d7ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 185 additions and 76 deletions

View File

@ -2,8 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@ -13,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
protected override double TimePerAction => 100;
[Test]
public void TestNormalHit()
{
@ -21,11 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
}
[Test]
public void TestStrongHit([Values(false, true)] bool hitBoth)
[TestCase(HitResult.Great)]
[TestCase(HitResult.Ok)]
public void TestStrongHit(HitResult type)
{
AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth))));
AddStep("create hit", () => SetContents(() => getContentFor(createStrongHit(type))));
AddStep("visualise second hit",
() => this.ChildrenOfType<HitExplosion>()
.ForEach(e => e.VisualiseSecondHit(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()))));
}
private Drawable getContentFor(DrawableTestHit hit)
@ -38,17 +47,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
// the hit needs to be added to hierarchy in order for nested objects to be created correctly.
// setting zero alpha is supposed to prevent the test from looking broken.
hit.With(h => h.Alpha = 0),
new HitExplosion(hit, hit.Type)
new HitExplosion(hit.Type)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}.With(explosion => explosion.Apply(hit))
}
};
}
private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth);
private DrawableTestHit createStrongHit(HitResult type) => new DrawableTestStrongHit(Time.Current, type);
}
}

View File

@ -11,7 +11,6 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
@ -108,12 +107,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
Hit hit = new Hit();
Hit hit = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current };
var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
}
private void addStrongHitJudgement(bool kiai)
@ -122,6 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit hit = new Hit
{
StartTime = DrawableRuleset.Playfield.Time.Current,
IsStrong = true,
Samples = createSamples(strong: true)
};
@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(hit.NestedHitObjects.Single(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
private void addMissJudgement()

View File

@ -1,22 +1,22 @@
// 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.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class LegacyHitExplosion : CompositeDrawable
public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion
{
private readonly Drawable sprite;
private readonly Drawable strongSprite;
private DrawableStrongNestedHit nestedStrongHit;
private bool switchedToStrongSprite;
[CanBeNull]
private readonly Drawable strongSprite;
/// <summary>
/// Creates a new legacy hit explosion.
@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
/// </remarks>
/// <param name="sprite">The normal legacy explosion sprite.</param>
/// <param name="strongSprite">The strong legacy explosion sprite.</param>
public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null)
public LegacyHitExplosion(Drawable sprite, [CanBeNull] Drawable strongSprite = null)
{
this.sprite = sprite;
this.strongSprite = strongSprite;
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject judgedObject)
private void load()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -56,45 +56,30 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
s.Origin = Anchor.Centre;
}));
}
if (judgedObject is DrawableHit hit)
nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit;
}
protected override void LoadComplete()
public void Animate(DrawableHitObject drawableHitObject)
{
base.LoadComplete();
const double animation_time = 120;
(sprite as IFramedAnimation)?.GotoFrame(0);
(strongSprite as IFramedAnimation)?.GotoFrame(0);
this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
this.ScaleTo(0.6f)
.Then().ScaleTo(1.1f, animation_time * 0.8)
.Then().ScaleTo(0.9f, animation_time * 0.4)
.Then().ScaleTo(1f, animation_time * 0.2);
Expire(true);
}
protected override void Update()
public void AnimateSecondHit()
{
base.Update();
if (strongSprite == null)
return;
if (shouldSwitchToStrongSprite() && !switchedToStrongSprite)
{
sprite.FadeOut(50, Easing.OutQuint);
strongSprite.FadeIn(50, Easing.OutQuint);
switchedToStrongSprite = true;
}
}
private bool shouldSwitchToStrongSprite()
{
if (nestedStrongHit == null || strongSprite == null)
return false;
return nestedStrongHit.IsHit;
sprite.FadeOut(50, Easing.OutQuint);
strongSprite.FadeIn(50, Easing.OutQuint);
}
}
}

View File

@ -1,6 +1,7 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -13,19 +14,23 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DefaultHitExplosion : CircularContainer
internal class DefaultHitExplosion : CircularContainer, IAnimatableHitExplosion
{
private readonly DrawableHitObject judgedObject;
private readonly HitResult result;
public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result)
[CanBeNull]
private Box body;
[Resolved]
private OsuColour colours { get; set; }
public DefaultHitExplosion(HitResult result)
{
this.judgedObject = judgedObject;
this.result = result;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
RelativeSizeAxes = Axes.Both;
@ -40,26 +45,36 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.IsHit())
return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
InternalChildren = new[]
{
new Box
body = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
}
};
updateColour();
}
protected override void LoadComplete()
private void updateColour([CanBeNull] DrawableHitObject judgedObject = null)
{
base.LoadComplete();
if (body == null)
return;
bool isRim = (judgedObject?.HitObject as Hit)?.Type == HitType.Rim;
body.Colour = isRim ? colours.BlueDarker : colours.PinkDarker;
}
public void Animate(DrawableHitObject drawableHitObject)
{
updateColour(drawableHitObject);
this.ScaleTo(3f, 1000, Easing.OutQuint);
this.FadeOut(500);
}
Expire(true);
public void AnimateSecondHit()
{
}
}
}

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
@ -16,31 +18,37 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary>
/// A circle explodes from the hit target to indicate a hitobject has been hit.
/// </summary>
internal class HitExplosion : CircularContainer
internal class HitExplosion : PoolableDrawable
{
public override bool RemoveWhenNotAlive => true;
[Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject;
public override bool RemoveCompletedTransforms => false;
private readonly HitResult result;
private double? secondHitTime;
[CanBeNull]
public DrawableHitObject JudgedObject;
private SkinnableDrawable skinnable;
public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
public HitExplosion(DrawableHitObject judgedObject, HitResult result)
/// <summary>
/// This constructor only exists to meet the <c>new()</c> type constraint of <see cref="DrawablePool{T}"/>.
/// </summary>
public HitExplosion()
: this(HitResult.Great)
{
}
public HitExplosion(HitResult result)
{
JudgedObject = judgedObject;
this.result = result;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
RelativeSizeAxes = Axes.Both;
RelativePositionAxes = Axes.Both;
}
@ -48,7 +56,47 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result));
InternalChild = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(result));
skinnable.OnSkinChanged += runAnimation;
}
public void Apply([CanBeNull] DrawableHitObject drawableHitObject)
{
JudgedObject = drawableHitObject;
secondHitTime = null;
}
protected override void PrepareForUse()
{
base.PrepareForUse();
runAnimation();
}
private void runAnimation()
{
if (JudgedObject?.Result == null)
return;
double resultTime = JudgedObject.Result.TimeAbsolute;
LifetimeStart = resultTime;
ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true);
using (BeginAbsoluteSequence(resultTime))
(skinnable.Drawable as IAnimatableHitExplosion)?.Animate(JudgedObject);
if (secondHitTime != null)
{
using (BeginAbsoluteSequence(secondHitTime.Value))
{
this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
(skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit();
}
}
LifetimeEnd = skinnable.Drawable.LatestTransformEndTime;
}
private static TaikoSkinComponents getComponentName(HitResult result)
@ -68,12 +116,10 @@ namespace osu.Game.Rulesets.Taiko.UI
throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
}
/// <summary>
/// Transforms this hit explosion to visualise a secondary hit.
/// </summary>
public void VisualiseSecondHit()
public void VisualiseSecondHit(JudgementResult judgementResult)
{
this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
secondHitTime = judgementResult.TimeAbsolute;
runAnimation();
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Graphics.Pooling;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.UI
{
/// <summary>
/// Pool for hit explosions of a specific type.
/// </summary>
internal class HitExplosionPool : DrawablePool<HitExplosion>
{
private readonly HitResult hitResult;
public HitExplosionPool(HitResult hitResult)
: base(15)
{
this.hitResult = hitResult;
}
protected override HitExplosion CreateNewDrawable() => new HitExplosion(hitResult);
}
}

View File

@ -0,0 +1,23 @@
// 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.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
/// <summary>
/// A skinnable element of a hit explosion that supports playing an animation from the current point in time.
/// </summary>
public interface IAnimatableHitExplosion
{
/// <summary>
/// Shows the hit explosion for the supplied <paramref name="drawableHitObject"/>.
/// </summary>
void Animate(DrawableHitObject drawableHitObject);
/// <summary>
/// Transforms the hit explosion to visualise a secondary hit.
/// </summary>
void AnimateSecondHit();
}
}

View File

@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private SkinnableDrawable mascot;
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
private ProxyContainer topLevelHitContainer;
private Container rightArea;
@ -166,10 +167,15 @@ namespace osu.Game.Rulesets.Taiko.UI
RegisterPool<SwellTick, DrawableSwellTick>(100);
var hitWindows = new TaikoHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
explosionPools.Add(result, new HitExplosionPool(result));
}
AddRangeInternal(judgementPools.Values);
AddRangeInternal(explosionPools.Values);
}
protected override void LoadComplete()
@ -281,7 +287,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
case TaikoStrongJudgement _:
if (result.IsHit)
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit();
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result);
break;
case TaikoDrumRollTickJudgement _:
@ -315,7 +321,8 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type)
{
hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
hitExplosionContainer.Add(explosionPools[result]
.Get(explosion => explosion.Apply(drawableObject)));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
}