diff --git a/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs b/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs new file mode 100644 index 0000000000..c97fd369ea --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs @@ -0,0 +1,193 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK.Input; +using osu.Framework.GameModes.Testing; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Graphics.Transformations; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.MathUtils; + +namespace osu.Desktop.Tests +{ + class TestCaseScoreCounter : TestCase + { + public override string Name => @"ScoreCounter"; + + public override string Description => @"Tests multiple counters"; + + public override void Reset() + { + base.Reset(); + + Random rnd = new Random(); + + ScoreCounter uc = new ScoreCounter + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + TextSize = 40, + RollingDuration = 1000, + RollingEasing = EasingTypes.Out, + Count = 0, + Position = new Vector2(20, 20), + LeadingZeroes = 7, + }; + Add(uc); + + StandardComboCounter sc = new StandardComboCounter + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Position = new Vector2(20, 20), + IsRollingProportional = true, + RollingDuration = 20, + PopOutDuration = 250, + Count = 0, + TextSize = 40, + }; + Add(sc); + + CatchComboCounter cc = new CatchComboCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + IsRollingProportional = true, + RollingDuration = 20, + PopOutDuration = 250, + Count = 0, + TextSize = 40, + }; + Add(cc); + + AlternativeComboCounter ac = new AlternativeComboCounter + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Position = new Vector2(20, 80), + IsRollingProportional = true, + RollingDuration = 20, + ScaleFactor = 2, + Count = 0, + TextSize = 40, + }; + Add(ac); + + + AccuracyCounter pc = new AccuracyCounter + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + RollingDuration = 1000, + RollingEasing = EasingTypes.Out, + Count = 100.0f, + Position = new Vector2(20, 60), + }; + Add(pc); + + Button resetButton = new Button + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Text = @"Reset all", + Width = 100, + Height = 20, + Position = new Vector2(0, 0), + }; + resetButton.Action += delegate + { + uc.Count = 0; + sc.Count = 0; + ac.Count = 0; + cc.Count = 0; + pc.SetCount(0, 0); + }; + Add(resetButton); + + Button hitButton = new Button + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Text = @"Hit! :D", + Width = 100, + Height = 20, + Position = new Vector2(0, 20), + }; + hitButton.Action += delegate + { + uc.Count += 300 + (ulong)(300.0 * (sc.Count > 0 ? sc.Count - 1 : 0) / 25.0); + sc.Count++; + ac.Count++; + cc.CatchFruit(new Color4( + Math.Max(0.5f, RNG.NextSingle()), + Math.Max(0.5f, RNG.NextSingle()), + Math.Max(0.5f, RNG.NextSingle()), + 1) + ); + pc.Numerator++; + pc.Denominator++; + }; + Add(hitButton); + + Button missButton = new Button + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Text = @"miss...", + Width = 100, + Height = 20, + Position = new Vector2(0, 40), + }; + missButton.Action += delegate + { + sc.Count = 0; + ac.Count = 0; + cc.Count = 0; + pc.Denominator++; + }; + Add(missButton); + + Button forceResetButton = new Button + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Text = @"Force reset", + Width = 100, + Height = 20, + Position = new Vector2(0, 60), + }; + forceResetButton.Action += delegate + { + uc.ResetCount(); + sc.ResetCount(); + ac.ResetCount(); + pc.ResetCount(); + cc.ResetCount(); + }; + Add(forceResetButton); + + Button stopButton = new Button + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Text = @"STOP!", + Width = 100, + Height = 20, + Position = new Vector2(0, 80), + }; + stopButton.Action += delegate + { + uc.StopRolling(); + sc.StopRolling(); + cc.StopRolling(); + ac.StopRolling(); + pc.StopRolling(); + }; + Add(stopButton); + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index b731d53b4b..fdbd878a87 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -152,6 +152,7 @@ + diff --git a/osu.Game/Graphics/UserInterface/AccuracyCounter.cs b/osu.Game/Graphics/UserInterface/AccuracyCounter.cs new file mode 100644 index 0000000000..da04d9d7f7 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/AccuracyCounter.cs @@ -0,0 +1,102 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transformations; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Used as an accuracy counter. Represented visually as a percentage, internally as a fraction. + /// + public class AccuracyCounter : RollingCounter + { + private long numerator = 0; + public long Numerator + { + get + { + return numerator; + } + set + { + numerator = value; + updateCount(); + } + } + + private ulong denominator = 0; + public ulong Denominator + { + get + { + return denominator; + } + set + { + denominator = value; + updateCount(); + } + } + + public void SetCount(long num, ulong den) + { + numerator = num; + denominator = den; + updateCount(); + } + + private void updateCount() + { + Count = Denominator == 0 ? 100.0f : (Numerator * 100.0f) / Denominator; + } + + public override void ResetCount() + { + numerator = 0; + denominator = 0; + updateCount(); + StopRolling(); + } + + protected override string formatCount(float count) + { + return count.ToString("0.00") + "%"; + } + + protected override void transformCount(float currentValue, float newValue) + { + transformCount(new TransformAccuracy(Clock), currentValue, newValue); + } + + protected class TransformAccuracy : Transform + { + public override float CurrentValue + { + get + { + double time = Time; + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + } + + public override void Apply(Drawable d) + { + base.Apply(d); + (d as AccuracyCounter).VisibleCount = CurrentValue; + } + + public TransformAccuracy(IClock clock) + : base(clock) + { + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/AlternativeComboCounter.cs b/osu.Game/Graphics/UserInterface/AlternativeComboCounter.cs new file mode 100644 index 0000000000..edcad7f13c --- /dev/null +++ b/osu.Game/Graphics/UserInterface/AlternativeComboCounter.cs @@ -0,0 +1,86 @@ +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics.Transformations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Allows tint and vertical scaling animation. Used by osu!taiko and osu!mania. + /// + public class AlternativeComboCounter : ULongCounter // btw, I'm terribly bad with names... OUENDAN! + { + public Color4 OriginalColour; + public Color4 TintColour = Color4.OrangeRed; + public int TintDuration = 500; + public float ScaleFactor = 1; + public EasingTypes TintEasing = EasingTypes.None; + public bool CanAnimateWhenBackwards = false; + + public AlternativeComboCounter() + { + IsRollingContinuous = false; + } + + public override void Load() + { + base.Load(); + countSpriteText.Hide(); + OriginalColour = Colour; + } + + public override void ResetCount() + { + SetCountWithoutRolling(0); + } + + protected override void transformCount(ulong currentValue, ulong newValue) + { + // Animate rollover only when going backwards + if (newValue > currentValue) + { + updateTransforms(typeof(TranformULongCounter)); + removeTransforms(typeof(TranformULongCounter)); + VisibleCount = newValue; + } + else + transformCount(new TranformULongCounter(Clock), currentValue, newValue); + } + + protected override ulong GetProportionalDuration(ulong currentValue, ulong newValue) + { + ulong difference = currentValue > newValue ? currentValue - newValue : currentValue - newValue; + return difference * RollingDuration; + } + + protected virtual void transformAnimate() + { + countSpriteText.Colour = TintColour; + countSpriteText.ScaleTo(new Vector2(1, ScaleFactor)); + countSpriteText.FadeColour(OriginalColour, TintDuration, TintEasing); + countSpriteText.ScaleTo(new Vector2(1, 1), TintDuration, TintEasing); + } + + protected override void transformVisibleCount(ulong currentValue, ulong newValue) + { + if (countSpriteText != null) + { + countSpriteText.Text = newValue.ToString(@"#,0"); + if (newValue == 0) + { + countSpriteText.FadeOut(TintDuration); + return; + } + countSpriteText.Show(); + if (newValue > currentValue || CanAnimateWhenBackwards) + { + transformAnimate(); + } + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/CatchComboCounter.cs b/osu.Game/Graphics/UserInterface/CatchComboCounter.cs new file mode 100644 index 0000000000..07ac925bcd --- /dev/null +++ b/osu.Game/Graphics/UserInterface/CatchComboCounter.cs @@ -0,0 +1,56 @@ +using OpenTK.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Similar to Standard, but without the 'x' and has colour shadows. Used by osu!catch. + /// + public class CatchComboCounter : StandardComboCounter + { + public CatchComboCounter() + { + CanPopOutWhenBackwards = true; + } + + protected override string formatCount(ulong count) + { + return count.ToString("#,0"); + } + + protected override void transformCount(ulong currentValue, ulong newValue) + { + // Animate rollover only when going backwards + if (newValue > currentValue) + { + updateTransforms(typeof(TranformULongCounter)); + removeTransforms(typeof(TranformULongCounter)); + VisibleCount = newValue; + } + else + { + popOutSpriteText.Colour = countSpriteText.Colour; + transformCount(new TranformULongCounter(Clock), currentValue, newValue); + } + } + + /// + /// Tints pop-out before animation. Intended to use the last grabbed fruit colour. + /// + /// + public void CatchFruit(Color4 colour) + { + popOutSpriteText.Colour = colour; + Count++; + } + + public override void ResetCount() + { + base.ResetCount(); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs new file mode 100644 index 0000000000..0e714e07c6 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -0,0 +1,239 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transformations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Skeleton for a counter with a simple rollover animation. + /// + /// Type of the actual counter. + public abstract class RollingCounter : Container + { + protected SpriteText countSpriteText; + protected ulong RollingTotalDuration = 0; + + protected float textSize = 20.0f; + public float TextSize + { + get { return textSize; } + set + { + textSize = value; + updateTextSize(); + } + } + + /// + /// If true, each time the Count is updated, it will roll over from the current visible value. + /// Else, it will roll over from the current count value. + /// + public bool IsRollingContinuous = true; + + /// + /// If true, the rollover duration will be proportional to the counter. + /// + public bool IsRollingProportional = false; + + /// + /// If IsRollingProportional = false, duration in milliseconds for the counter rollover animation for each element. + /// If IsRollingProportional = true, duration in milliseconds for the counter rollover animation in total. + /// + public ulong RollingDuration = 0; + + /// + /// Easing for the counter rollover animation. + /// + public EasingTypes RollingEasing = EasingTypes.None; + + protected T visibleCount; + + /// + /// Value shown at the current moment. + /// + public virtual T VisibleCount + { + get + { + return visibleCount; + } + protected set + { + if (visibleCount.Equals(value)) + return; + transformVisibleCount(visibleCount, value); + visibleCount = value; + } + } + + protected T count; + + /// + /// Actual value of counter. + /// + public virtual T Count + { + get + { + return count; + } + set + { + if (Clock != null) + { + RollingTotalDuration = IsRollingProportional ? GetProportionalDuration(VisibleCount, value) : RollingDuration; + transformCount(IsRollingContinuous ? VisibleCount : count, value); + } + count = value; + } + } + + public override void Load() + { + base.Load(); + removeTransforms(typeof(Transform)); + if (Count == null) + ResetCount(); + VisibleCount = Count; + Children = new Drawable[] + { + countSpriteText = new SpriteText + { + Text = formatCount(Count), + TextSize = this.TextSize, + Anchor = this.Anchor, + Origin = this.Origin, + }, + }; + } + + /// + /// Calculates the duration of the rollover animation by using the difference between the current visible value and the new final value. + /// + /// + /// Intended to be used in conjunction with IsRolloverProportional = true. + /// If you're sure your superclass won't never need to be proportional, then it is not necessary to override this function. + /// + /// Current visible value. + /// New final value. + /// Calculated rollover duration in milliseconds. + protected virtual ulong GetProportionalDuration(T currentValue, T newValue) + { + return RollingDuration; + } + + /// + /// Used to format counts. + /// + /// Count to format. + /// Count formatted as a string. + protected virtual string formatCount(T count) + { + return count.ToString(); + } + + /// + /// Sets count value, bypassing rollover animation. + /// + /// New count value. + public virtual void SetCountWithoutRolling(T count) + { + Count = count; + StopRolling(); + } + + /// + /// Stops rollover animation, forcing the visible count to be the actual count. + /// + public virtual void StopRolling() + { + removeTransforms(typeof(Transform)); + VisibleCount = Count; + } + + /// + /// Resets count to default value. + /// + public abstract void ResetCount(); + + protected void updateTransforms(Type type) + { + foreach (ITransform t in Transforms.AliveItems) + if (t.GetType().IsAssignableFrom(type)) + t.Apply(this); + } + + protected void removeTransforms(Type type) + { + Transforms.RemoveAll(t => t.GetType().IsSubclassOf(type)); + } + + /// + /// Called when the count is updated to add a transformer that changes the value of the visible count (i.e. implement the rollover animation). + /// + /// Count value before modification. + /// Expected count value after modification- + /// + /// Unless you need to set a custom animation according to the current or new value of the count, the recommended approach is to call + /// transformCount(CustomTransformer(Clock), currentValue, newValue), where CustomTransformer is a custom Transformer related to the + /// type T of the RolloverCounter. + /// By using this approach, there is no need to check if the Clock is not null; this validation is done before adding the transformer. + /// + protected abstract void transformCount(T currentValue, T newValue); + + /// + /// Intended to be used by transformCount(). + /// + /// + protected void transformCount(Transform transform, T currentValue, T newValue) + { + Type type = transform.GetType(); + + updateTransforms(type); + removeTransforms(type); + + if (Clock == null) + return; + + if (RollingDuration == 0) + { + VisibleCount = Count; + return; + } + + transform.StartTime = Time; + transform.EndTime = Time + RollingTotalDuration; + transform.StartValue = currentValue; + transform.EndValue = newValue; + transform.Easing = RollingEasing; + + Transforms.Add(transform); + } + + /// + /// This procedure is called each time the visible count value is updated. + /// Override to create custom animations. + /// + /// Visible count value before modification. + /// Expected visible count value after modification- + protected virtual void transformVisibleCount(T currentValue, T newValue) + { + if (countSpriteText != null) + { + countSpriteText.Text = formatCount(newValue); + } + } + + protected virtual void updateTextSize() + { + if (countSpriteText != null) + countSpriteText.TextSize = TextSize; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs new file mode 100644 index 0000000000..08892f3d38 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + public class ScoreCounter : ULongCounter + { + /// + /// How many leading zeroes the counter will have. + /// + public uint LeadingZeroes = 0; + + protected override string formatCount(ulong count) + { + return count.ToString("D" + LeadingZeroes); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/StandardComboCounter.cs b/osu.Game/Graphics/UserInterface/StandardComboCounter.cs new file mode 100644 index 0000000000..15404fd2e5 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/StandardComboCounter.cs @@ -0,0 +1,110 @@ +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transformations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Uses the 'x' symbol and has a pop-out effect while rolling over. Used by osu! standard. + /// + public class StandardComboCounter : ULongCounter + { + public SpriteText popOutSpriteText; + + public ulong PopOutDuration = 0; + public float PopOutBigScale = 2.0f; + public float PopOutSmallScale = 1.2f; + public EasingTypes PopOutEasing = EasingTypes.None; + public bool CanPopOutWhenBackwards = false; + public float PopOutInitialAlpha = 1.0f; + + public StandardComboCounter() + { + IsRollingContinuous = false; + } + + public override void Load() + { + base.Load(); + countSpriteText.Alpha = 0; + Add(popOutSpriteText = new SpriteText + { + Text = formatCount(Count), + Origin = this.Origin, + Anchor = this.Anchor, + TextSize = this.TextSize, + Alpha = 0, + }); + } + + public override void ResetCount() + { + SetCountWithoutRolling(0); + } + + protected override void updateTextSize() + { + base.updateTextSize(); + if (popOutSpriteText != null) + popOutSpriteText.TextSize = this.TextSize; + } + + + protected override void transformCount(ulong currentValue, ulong newValue) + { + // Animate rollover only when going backwards + if (newValue > currentValue) + { + updateTransforms(typeof(TranformULongCounter)); + removeTransforms(typeof(TranformULongCounter)); + VisibleCount = newValue; + } + else + transformCount(new TranformULongCounter(Clock), currentValue, newValue); + } + + protected override ulong GetProportionalDuration(ulong currentValue, ulong newValue) + { + ulong difference = currentValue > newValue ? currentValue - newValue : currentValue - newValue; + return difference * RollingDuration; + } + + protected override string formatCount(ulong count) + { + return count.ToString("#,0") + "x"; + } + + protected virtual void transformPopOut() + { + countSpriteText.ScaleTo(PopOutSmallScale); + countSpriteText.ScaleTo(1, PopOutDuration, PopOutEasing); + + popOutSpriteText.ScaleTo(PopOutBigScale); + popOutSpriteText.FadeTo(PopOutInitialAlpha); + popOutSpriteText.ScaleTo(1, PopOutDuration, PopOutEasing); + popOutSpriteText.FadeOut(PopOutDuration, PopOutEasing); + } + + protected override void transformVisibleCount(ulong currentValue, ulong newValue) + { + if (countSpriteText != null && popOutSpriteText != null) + { + countSpriteText.Text = popOutSpriteText.Text = formatCount(newValue); + if (newValue == 0) + { + countSpriteText.FadeOut(PopOutDuration); + } + else + { + countSpriteText.Show(); + if (newValue > currentValue || CanPopOutWhenBackwards) + transformPopOut(); + } + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ULongCounter.cs b/osu.Game/Graphics/UserInterface/ULongCounter.cs new file mode 100644 index 0000000000..01ab357518 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ULongCounter.cs @@ -0,0 +1,59 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transformations; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// A simple rollover counter that accepts unsigned long values. + /// + public class ULongCounter : RollingCounter + { + protected override void transformCount(ulong currentValue, ulong newValue) + { + transformCount(new TranformULongCounter(Clock), currentValue, newValue); + } + + public override void ResetCount() + { + SetCountWithoutRolling(0); + } + + protected override string formatCount(ulong count) + { + return count.ToString("#,0"); + } + + protected class TranformULongCounter : Transform + { + public override ulong CurrentValue + { + get + { + double time = Time; + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return (ulong)Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + } + + public override void Apply(Drawable d) + { + base.Apply(d); + (d as ULongCounter).VisibleCount = CurrentValue; + } + + public TranformULongCounter(IClock clock) + : base(clock) + { + } + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8ef0f07fa7..ae45d04592 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -101,10 +101,17 @@ + + + + + + +