1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Add support for pooling explosions in taiko

This commit is contained in:
Bartłomiej Dach 2021-03-14 15:51:38 +01:00
parent 716b9048c1
commit 8b74666cc3
7 changed files with 170 additions and 64 deletions

View File

@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
// the hit needs to be added to hierarchy in order for nested objects to be created correctly. // 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. // setting zero alpha is supposed to prevent the test from looking broken.
hit.With(h => h.Alpha = 0), hit.With(h => h.Alpha = 0),
new HitExplosion(hit, hit.Type) new HitExplosion(hit.Type)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
} }.With(explosion => explosion.Apply(hit))
} }
}; };
} }

View File

@ -1,22 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; 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 namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
public class LegacyHitExplosion : CompositeDrawable public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
{ {
private readonly Drawable sprite; public override bool RemoveWhenNotAlive => false;
private readonly Drawable strongSprite;
private DrawableStrongNestedHit nestedStrongHit; private readonly Drawable sprite;
private bool switchedToStrongSprite;
[CanBeNull]
private readonly Drawable strongSprite;
/// <summary> /// <summary>
/// Creates a new legacy hit explosion. /// Creates a new legacy hit explosion.
@ -27,14 +29,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
/// </remarks> /// </remarks>
/// <param name="sprite">The normal legacy explosion sprite.</param> /// <param name="sprite">The normal legacy explosion sprite.</param>
/// <param name="strongSprite">The strong 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.sprite = sprite;
this.strongSprite = strongSprite; this.strongSprite = strongSprite;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject judgedObject) private void load()
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -56,17 +58,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
s.Origin = Anchor.Centre; 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; 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.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
this.ScaleTo(0.6f) this.ScaleTo(0.6f)
@ -77,24 +77,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Expire(true); Expire(true);
} }
protected override void Update() public void AnimateSecondHit()
{ {
base.Update(); if (strongSprite == null)
return;
if (shouldSwitchToStrongSprite() && !switchedToStrongSprite)
{
sprite.FadeOut(50, Easing.OutQuint); sprite.FadeOut(50, Easing.OutQuint);
strongSprite.FadeIn(50, Easing.OutQuint); strongSprite.FadeIn(50, Easing.OutQuint);
switchedToStrongSprite = true;
}
}
private bool shouldSwitchToStrongSprite()
{
if (nestedStrongHit == null || strongSprite == null)
return false;
return nestedStrongHit.IsHit;
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -13,19 +14,25 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
internal class DefaultHitExplosion : CircularContainer internal class DefaultHitExplosion : CircularContainer, IHitExplosion
{ {
private readonly DrawableHitObject judgedObject; public override bool RemoveWhenNotAlive => false;
private readonly HitResult result; 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; this.result = result;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -40,26 +47,38 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.IsHit()) if (!result.IsHit())
return; return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
InternalChildren = new[] InternalChildren = new[]
{ {
new Box body = new Box
{ {
RelativeSizeAxes = Axes.Both, 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.ScaleTo(3f, 1000, Easing.OutQuint);
this.FadeOut(500); this.FadeOut(500);
Expire(true); Expire(true);
} }
public void AnimateSecondHit()
{
}
} }
} }

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osuTK; using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; 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.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -16,31 +18,35 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary> /// <summary>
/// A circle explodes from the hit target to indicate a hitobject has been hit. /// A circle explodes from the hit target to indicate a hitobject has been hit.
/// </summary> /// </summary>
internal class HitExplosion : CircularContainer internal class HitExplosion : PoolableDrawable
{ {
public override bool RemoveWhenNotAlive => true; public override bool RemoveWhenNotAlive => true;
public override bool RemoveCompletedTransforms => false;
[Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject;
private readonly HitResult result; private readonly HitResult result;
[CanBeNull]
public DrawableHitObject JudgedObject;
private SkinnableDrawable skinnable; private SkinnableDrawable skinnable;
public override double LifetimeStart => skinnable.Drawable.LifetimeStart; /// <summary>
/// This constructor only exists to meet the <c>new()</c> type constraint of <see cref="DrawablePool{T}"/>.
public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; /// </summary>
public HitExplosion()
public HitExplosion(DrawableHitObject judgedObject, HitResult result) : this(HitResult.Great)
{
}
public HitExplosion(HitResult result)
{ {
JudgedObject = judgedObject;
this.result = result; this.result = result;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE); Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
RelativeSizeAxes = Axes.Both;
RelativePositionAxes = Axes.Both; RelativePositionAxes = Axes.Both;
} }
@ -48,7 +54,44 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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;
}
protected override void PrepareForUse()
{
base.PrepareForUse();
runAnimation();
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
// clean up transforms on free instead of on prepare as is usually the case
// to avoid potentially overriding the effects of VisualiseSecondHit() in the case it is called before PrepareForUse().
ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true);
}
private void runAnimation()
{
if (JudgedObject?.Result == null)
return;
double resultTime = JudgedObject.Result.TimeAbsolute;
LifetimeStart = resultTime;
using (BeginAbsoluteSequence(resultTime))
(skinnable.Drawable as IHitExplosion)?.Animate(JudgedObject);
LifetimeEnd = skinnable.Drawable.LatestTransformEndTime;
} }
private static TaikoSkinComponents getComponentName(HitResult result) private static TaikoSkinComponents getComponentName(HitResult result)
@ -68,12 +111,13 @@ namespace osu.Game.Rulesets.Taiko.UI
throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}"); throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
} }
/// <summary> public void VisualiseSecondHit(JudgementResult judgementResult)
/// Transforms this hit explosion to visualise a secondary hit. {
/// </summary> using (BeginAbsoluteSequence(judgementResult.TimeAbsolute))
public void VisualiseSecondHit()
{ {
this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50); this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
(skinnable.Drawable as IHitExplosion)?.AnimateSecondHit();
}
} }
} }
} }

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>
/// Interface for hit explosions shown on the playfield's hit target in taiko.
/// </summary>
public interface IHitExplosion
{
/// <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 SkinnableDrawable mascot;
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>(); 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 ProxyContainer topLevelHitContainer;
private Container rightArea; private Container rightArea;
@ -166,10 +167,15 @@ namespace osu.Game.Rulesets.Taiko.UI
RegisterPool<SwellTick, DrawableSwellTick>(100); RegisterPool<SwellTick, DrawableSwellTick>(100);
var hitWindows = new TaikoHitWindows(); var hitWindows = new TaikoHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))) foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15)); judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
explosionPools.Add(result, new HitExplosionPool(result));
}
AddRangeInternal(judgementPools.Values); AddRangeInternal(judgementPools.Values);
AddRangeInternal(explosionPools.Values);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -281,7 +287,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
case TaikoStrongJudgement _: case TaikoStrongJudgement _:
if (result.IsHit) 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; break;
case TaikoDrumRollTickJudgement _: case TaikoDrumRollTickJudgement _:
@ -315,7 +321,8 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) 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) if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
} }