From e81d33dcec78ce816fc823214ea99a0e524c69da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 21:27:02 +0200 Subject: [PATCH] Refactor mascot animations to split logic paths --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 35 ++---- .../UI/DrawableTaikoMascot.cs | 19 +-- .../UI/TaikoMascotAnimation.cs | 116 ++++++++++++++++++ .../UI/TaikoMascotTextureAnimation.cs | 109 ---------------- 4 files changed, 133 insertions(+), 146 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs delete mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index f37c723a36..28065c401c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -27,6 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(DrawableTaikoMascot), + typeof(TaikoMascotAnimation) }).ToList(); [Cached(typeof(IScrollingInfo))] @@ -36,23 +36,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - private IEnumerable mascots => this.ChildrenOfType(); + private IEnumerable mascots => this.ChildrenOfType(); private IEnumerable playfields => this.ChildrenOfType(); [Test] - public void TestStateTextures() + public void TestStateAnimations() { AddStep("set beatmap", () => setBeatmap()); - AddStep("create mascot", () => - { - SetContents(() => new TestDrawableTaikoMascot()); - }); - - AddStep("clear state", () => setState(TaikoMascotAnimationState.Clear)); - AddStep("kiai state", () => setState(TaikoMascotAnimationState.Kiai)); - AddStep("fail state", () => setState(TaikoMascotAnimationState.Fail)); - AddStep("idle state", () => setState(TaikoMascotAnimationState.Idle)); + AddStep("clear state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear))); + AddStep("idle state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle))); + AddStep("kiai state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai))); + AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail))); } [Test] @@ -126,12 +121,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); } - private void setState(TaikoMascotAnimationState state) - { - foreach (var mascot in mascots) - mascot.State.Value = state; - } - private void addJudgement(HitResult result) { foreach (var playfield in playfields) @@ -144,15 +133,5 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } private bool assertState(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); - - private class TestDrawableTaikoMascot : DrawableTaikoMascot - { - public TestDrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) - : base(startingState) - { - } - - public new Bindable State => base.State; - } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index be744de5f4..bfc1d958c2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -17,9 +17,10 @@ namespace osu.Game.Rulesets.Taiko.UI { public class DrawableTaikoMascot : BeatSyncedContainer { - protected Bindable State { get; } + public IBindable State => state; - private readonly Dictionary animations; + private readonly Bindable state; + private readonly Dictionary animations; private Drawable currentAnimation; private bool lastHitMissed; @@ -29,8 +30,8 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both; - State = new Bindable(startingState); - animations = new Dictionary(); + state = new Bindable(startingState); + animations = new Dictionary(); } [BackgroundDependencyLoader] @@ -38,10 +39,10 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { - animations[TaikoMascotAnimationState.Idle] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), - animations[TaikoMascotAnimationState.Clear] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), - animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), - animations[TaikoMascotAnimationState.Fail] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), + animations[TaikoMascotAnimationState.Idle] = new TaikoMascotAnimation(TaikoMascotAnimationState.Idle), + animations[TaikoMascotAnimationState.Clear] = new TaikoMascotAnimation(TaikoMascotAnimationState.Clear), + animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai), + animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; updateState(); @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI private void updateState() { - State.Value = getNextState(); + state.Value = getNextState(); } private TaikoMascotAnimationState getNextState() diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs new file mode 100644 index 0000000000..1e289c1a74 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class TaikoMascotAnimation : BeatSyncedContainer + { + private readonly TextureAnimation textureAnimation; + + private int currentFrame; + + public TaikoMascotAnimation(TaikoMascotAnimationState state) + { + InternalChild = textureAnimation = createTextureAnimation(state).With(animation => + { + animation.Origin = animation.Anchor = Anchor.BottomLeft; + RelativeSizeAxes = Axes.Both; + }); + + RelativeSizeAxes = Axes.Both; + Origin = Anchor = Anchor.BottomLeft; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + // assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched. + if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying) + return; + + textureAnimation.GotoFrame(currentFrame); + currentFrame = (currentFrame + 1) % textureAnimation.FrameCount; + } + + private static TextureAnimation createTextureAnimation(TaikoMascotAnimationState state) + { + switch (state) + { + case TaikoMascotAnimationState.Clear: + return new ClearMascotTextureAnimation(); + + case TaikoMascotAnimationState.Idle: + case TaikoMascotAnimationState.Kiai: + case TaikoMascotAnimationState.Fail: + return new ManualMascotTextureAnimation(state); + + default: + throw new ArgumentOutOfRangeException(nameof(state), $"Mascot animations for state {state} are not supported"); + } + } + + private class ManualMascotTextureAnimation : TextureAnimation + { + private readonly TaikoMascotAnimationState state; + + public ManualMascotTextureAnimation(TaikoMascotAnimationState state) + { + this.state = state; + + IsPlaying = false; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + for (int frameIndex = 0; true; frameIndex++) + { + var texture = getAnimationFrame(skin, state, frameIndex); + + if (texture == null) + break; + + AddFrame(texture); + } + } + } + + private class ClearMascotTextureAnimation : TextureAnimation + { + private const float clear_animation_speed = 1000 / 10f; + + private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; + + public ClearMascotTextureAnimation() + { + DefaultFrameLength = clear_animation_speed; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + foreach (var frameIndex in clear_animation_sequence) + { + var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex); + + if (texture == null) + continue; + + AddFrame(texture); + } + } + } + + private static Texture getAnimationFrame(ISkinSource skin, TaikoMascotAnimationState state, int frameIndex) + => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs deleted file mode 100644 index 080d30c3f2..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class TaikoMascotTextureAnimation : TextureAnimation - { - private const float clear_animation_speed = 1000 / 10f; - private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; - private int currentFrame; - - public TaikoMascotAnimationState State { get; } - - public TaikoMascotTextureAnimation(TaikoMascotAnimationState state) - : base(true) - { - State = state; - - // We're animating on beat if it's not the clear animation - if (state == TaikoMascotAnimationState.Clear) - DefaultFrameLength = clear_animation_speed; - else - this.Stop(); - - Origin = Anchor.BottomLeft; - Anchor = Anchor.BottomLeft; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - if (State == TaikoMascotAnimationState.Clear) - { - foreach (var textureIndex in clear_animation_sequence) - { - if (!addFrame(skin, textureIndex)) - break; - } - } - else - { - for (int i = 0; true; i++) - { - if (!addFrame(skin, i)) - break; - } - } - } - - private bool addFrame(ISkinSource skin, int textureIndex) - { - var textureName = getStateTextureName(textureIndex); - var texture = skin.GetTexture(textureName); - - if (texture == null) - return false; - - AddFrame(texture); - - return true; - } - - /// - /// Advances the current frame by one. - /// - public void Move() - { - // Check whether there are frames before causing a crash. - if (FrameCount == 0) - return; - - if (currentFrame >= FrameCount) - currentFrame = 0; - - GotoFrame(currentFrame); - - currentFrame += 1; - } - - private string getStateTextureName(int i) => $"pippidon{getStateString(State)}{i}"; - - private string getStateString(TaikoMascotAnimationState state) - { - switch (state) - { - case TaikoMascotAnimationState.Clear: - return "clear"; - - case TaikoMascotAnimationState.Fail: - return "fail"; - - case TaikoMascotAnimationState.Idle: - return "idle"; - - case TaikoMascotAnimationState.Kiai: - return "kiai"; - - default: - throw new ArgumentOutOfRangeException(nameof(state), $"There's no case for animation state {state} available"); - } - } - } -}