Merge pull request #22654 from ItsShamed/gameplay/key-counter-abstraction

Introduce common structure for key counters
This commit is contained in:
Bartłomiej Dach 2023-04-05 21:07:30 +02:00 committed by GitHub
commit a4e68e1845
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 420 additions and 310 deletions

View File

@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private TestActionKeyCounter leftKeyCounter = null!; private DefaultKeyCounter leftKeyCounter = null!;
private TestActionKeyCounter rightKeyCounter = null!; private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!; private OsuInputManager osuInputManager = null!;
@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Depth = float.MinValue, Depth = float.MinValue,
X = -100, X = -100,
}, },
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -598,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertKeyCounter(int left, int right) private void assertKeyCounter(int left, int right)
{ {
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
} }
private void releaseAllTouches() private void releaseAllTouches()
@ -615,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction> public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler<OsuAction>
{ {
public OsuAction Action { get; } public OsuAction Action { get; }
public TestActionKeyCounter(OsuAction action) public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString()) : base(action.ToString())
{ {
Action = action; Action = action;
@ -629,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (e.Action == Action) if (e.Action == Action)
{ {
IsLit = true; Activate();
Increment();
} }
return false; return false;
@ -638,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{ {
if (e.Action == Action) IsLit = false; if (e.Action == Action)
Deactivate();
} }
} }

View File

@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay
var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2));
seekTo(referenceBeatmap.Breaks[0].StartTime); seekTo(referenceBeatmap.Breaks[0].StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime()); seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);

View File

@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear()); AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0); addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
AddAssert("no results triggered", () => Player.Results.Count == 0); AddAssert("no results triggered", () => Player.Results.Count == 0);
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<DefaultKeyCounter>>().Single();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1; scoreProcessor.Combo.Value = 1;

View File

@ -7,7 +7,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,28 +17,29 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public TestSceneKeyCounter() public TestSceneKeyCounter()
{ {
KeyCounterKeyboard testCounter; KeyCounterDisplay kc = new DefaultKeyCounterDisplay
KeyCounterDisplay kc = new KeyCounterDisplay
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Children = new KeyCounter[]
{
testCounter = new KeyCounterKeyboard(Key.X),
new KeyCounterKeyboard(Key.X),
new KeyCounterMouse(MouseButton.Left),
new KeyCounterMouse(MouseButton.Right),
},
}; };
kc.AddRange(new InputTrigger[]
{
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterKeyboardTrigger(Key.X),
new KeyCounterMouseTrigger(MouseButton.Left),
new KeyCounterMouseTrigger(MouseButton.Right),
});
var testCounter = (DefaultKeyCounter)kc.Counters.First();
AddStep("Add random", () => AddStep("Add random", () =>
{ {
Key key = (Key)((int)Key.A + RNG.Next(26)); Key key = (Key)((int)Key.A + RNG.Next(26));
kc.Add(new KeyCounterKeyboard(key)); kc.Add(new KeyCounterKeyboardTrigger(key));
}); });
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key;
void addPressKeyStep() void addPressKeyStep()
{ {
@ -46,12 +47,12 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
addPressKeyStep(); addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
addPressKeyStep(); addPressKeyStep();
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
AddStep("Disable counting", () => testCounter.IsCounting = false); AddStep("Disable counting", () => testCounter.IsCounting.Value = false);
addPressKeyStep(); addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
Add(kc); Add(kc);
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0));
AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail);
} }

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1; scoreProcessor.Combo.Value = 1;
return new Container return new Container

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;
@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<DefaultKeyCounter>>().Single();
[Test] [Test]
public void TestComboCounterIncrementing() public void TestComboCounterIncrementing()
@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
action?.Invoke(hudOverlay); action?.Invoke(hudOverlay);

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input; using osuTK.Input;
@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false); AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
AddStep("press 'z'", () => InputManager.Key(Key.Z)); AddStep("press 'z'", () => InputManager.Key(Key.Z));
AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0); AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0);
AddStep("press 's'", () => InputManager.Key(Key.S)); AddStep("press 's'", () => InputManager.Key(Key.S));
AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1);
} }
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel

View File

@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK; using osuTK;

View File

@ -19,7 +19,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.UI
.Select(b => b.GetAction<T>()) .Select(b => b.GetAction<T>())
.Distinct() .Distinct()
.OrderBy(action => action) .OrderBy(action => action)
.Select(action => new KeyCounterAction<T>(action))); .Select(action => new KeyCounterActionTrigger<T>(action)));
} }
private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T> private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
@ -179,11 +179,14 @@ namespace osu.Game.Rulesets.UI
{ {
} }
public bool OnPressed(KeyBindingPressEvent<T> e) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnPressed(e.Action, Clock.Rate >= 0)); public bool OnPressed(KeyBindingPressEvent<T> e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger<T>)
.Select(c => (KeyCounterActionTrigger<T>)c.Trigger)
.Any(c => c.OnPressed(e.Action, Clock.Rate >= 0));
public void OnReleased(KeyBindingReleaseEvent<T> e) public void OnReleased(KeyBindingReleaseEvent<T> e)
{ {
foreach (var c in Target.Children.OfType<KeyCounterAction<T>>()) foreach (var c
in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger<T>).Select(c => (KeyCounterActionTrigger<T>)c.Trigger))
c.OnReleased(e.Action, Clock.Rate >= 0); c.OnReleased(e.Action, Clock.Rate >= 0);
} }
} }

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
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,70 +11,23 @@ using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play.HUD
{ {
public abstract partial class KeyCounter : Container public partial class DefaultKeyCounter : KeyCounter
{ {
private Sprite buttonSprite; private Sprite buttonSprite = null!;
private Sprite glowSprite; private Sprite glowSprite = null!;
private Container textLayer; private Container textLayer = null!;
private SpriteText countSpriteText; private SpriteText countSpriteText = null!;
public bool IsCounting { get; set; } = true;
private int countPresses;
public int CountPresses
{
get => countPresses;
private set
{
if (countPresses != value)
{
countPresses = value;
countSpriteText.Text = value.ToString(@"#,0");
}
}
}
private bool isLit;
public bool IsLit
{
get => isLit;
protected set
{
if (isLit != value)
{
isLit = value;
updateGlowSprite(value);
}
}
}
public void Increment()
{
if (!IsCounting)
return;
CountPresses++;
}
public void Decrement()
{
if (!IsCounting)
return;
CountPresses--;
}
//further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor //further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor
public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray; public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray;
public Color4 KeyUpTextColor { get; set; } = Color4.White; public Color4 KeyUpTextColor { get; set; } = Color4.White;
public double FadeTime { get; set; } public double FadeTime { get; set; }
protected KeyCounter(string name) public DefaultKeyCounter(InputTrigger trigger)
: base(trigger)
{ {
Name = name;
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -116,7 +67,7 @@ namespace osu.Game.Screens.Play
}, },
countSpriteText = new OsuSpriteText countSpriteText = new OsuSpriteText
{ {
Text = CountPresses.ToString(@"#,0"), Text = CountPresses.Value.ToString(@"#,0"),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
@ -130,6 +81,9 @@ namespace osu.Game.Screens.Play
// so the size can be changing between buttonSprite and glowSprite. // so the size can be changing between buttonSprite and glowSprite.
Height = buttonSprite.DrawHeight; Height = buttonSprite.DrawHeight;
Width = buttonSprite.DrawWidth; Width = buttonSprite.DrawWidth;
IsActive.BindValueChanged(e => updateGlowSprite(e.NewValue), true);
CountPresses.BindValueChanged(e => countSpriteText.Text = e.NewValue.ToString(@"#,0"), true);
} }
private void updateGlowSprite(bool show) private void updateGlowSprite(bool show)

View File

@ -0,0 +1,83 @@
// 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.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class DefaultKeyCounterDisplay : KeyCounterDisplay
{
private const int duration = 100;
private const double key_fade_time = 80;
private readonly FillFlowContainer<DefaultKeyCounter> keyFlow;
public override IEnumerable<KeyCounter> Counters => keyFlow;
public DefaultKeyCounterDisplay()
{
InternalChild = keyFlow = new FillFlowContainer<DefaultKeyCounter>
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Alpha = 0,
};
}
protected override void Update()
{
base.Update();
// Don't use autosize as it will shrink to zero when KeyFlow is hidden.
// In turn this can cause the display to be masked off screen and never become visible again.
Size = keyFlow.Size;
}
public override void Add(InputTrigger trigger) =>
keyFlow.Add(new DefaultKeyCounter(trigger)
{
FadeTime = key_fade_time,
KeyDownTextColor = KeyDownTextColor,
KeyUpTextColor = KeyUpTextColor,
});
protected override void UpdateVisibility() =>
// Isolate changing visibility of the key counters from fading this component.
keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
private Color4 keyDownTextColor = Color4.DarkGray;
public Color4 KeyDownTextColor
{
get => keyDownTextColor;
set
{
if (value != keyDownTextColor)
{
keyDownTextColor = value;
foreach (var child in keyFlow)
child.KeyDownTextColor = value;
}
}
}
private Color4 keyUpTextColor = Color4.White;
public Color4 KeyUpTextColor
{
get => keyUpTextColor;
set
{
if (value != keyUpTextColor)
{
keyUpTextColor = value;
foreach (var child in keyFlow)
child.KeyUpTextColor = value;
}
}
}
}
}

View File

@ -0,0 +1,37 @@
// 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;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An event trigger which can be used with <see cref="KeyCounter"/> to create visual tracking of button/key presses.
/// </summary>
public abstract partial class InputTrigger : Component
{
/// <summary>
/// Callback to invoke when the associated input has been activated.
/// </summary>
/// <param name="forwardPlayback">Whether gameplay is progressing in the forward direction time-wise.</param>
public delegate void OnActivateCallback(bool forwardPlayback);
/// <summary>
/// Callback to invoke when the associated input has been deactivated.
/// </summary>
/// <param name="forwardPlayback">Whether gameplay is progressing in the forward direction time-wise.</param>
public delegate void OnDeactivateCallback(bool forwardPlayback);
public event OnActivateCallback? OnActivate;
public event OnDeactivateCallback? OnDeactivate;
protected InputTrigger(string name)
{
Name = name;
}
protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback);
protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback);
}
}

View File

@ -0,0 +1,98 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An individual key display which is intended to be displayed within a <see cref="KeyCounterDisplay"/>.
/// </summary>
public abstract partial class KeyCounter : Container
{
/// <summary>
/// The <see cref="InputTrigger"/> which activates and deactivates this <see cref="KeyCounter"/>.
/// </summary>
public readonly InputTrigger Trigger;
/// <summary>
/// Whether the actions reported by <see cref="Trigger"/> should be counted.
/// </summary>
public Bindable<bool> IsCounting { get; } = new BindableBool(true);
private readonly Bindable<int> countPresses = new BindableInt
{
MinValue = 0
};
/// <summary>
/// The current count of registered key presses.
/// </summary>
public IBindable<int> CountPresses => countPresses;
private readonly Container content;
protected override Container<Drawable> Content => content;
/// <summary>
/// Whether this <see cref="KeyCounter"/> is currently in the "activated" state because the associated key is currently pressed.
/// </summary>
protected readonly Bindable<bool> IsActive = new BindableBool();
protected KeyCounter(InputTrigger trigger)
{
InternalChildren = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both
},
Trigger = trigger,
};
Trigger.OnActivate += Activate;
Trigger.OnDeactivate += Deactivate;
Name = trigger.Name;
}
private void increment()
{
if (!IsCounting.Value)
return;
countPresses.Value++;
}
private void decrement()
{
if (!IsCounting.Value)
return;
countPresses.Value--;
}
protected virtual void Activate(bool forwardPlayback = true)
{
IsActive.Value = true;
if (forwardPlayback)
increment();
}
protected virtual void Deactivate(bool forwardPlayback = true)
{
IsActive.Value = false;
if (!forwardPlayback)
decrement();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Trigger.OnActivate -= Activate;
Trigger.OnDeactivate -= Deactivate;
}
}
}

View File

@ -1,18 +1,16 @@
// 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.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play.HUD
{ {
public partial class KeyCounterAction<T> : KeyCounter public partial class KeyCounterActionTrigger<T> : InputTrigger
where T : struct where T : struct
{ {
public T Action { get; } public T Action { get; }
public KeyCounterAction(T action) public KeyCounterActionTrigger(T action)
: base($"B{(int)(object)action + 1}") : base($"B{(int)(object)action + 1}")
{ {
Action = action; Action = action;
@ -23,9 +21,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer<T>.Default.Equals(action, Action)) if (!EqualityComparer<T>.Default.Equals(action, Action))
return false; return false;
IsLit = true; Activate(forwards);
if (forwards)
Increment();
return false; return false;
} }
@ -34,9 +30,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer<T>.Default.Equals(action, Action)) if (!EqualityComparer<T>.Default.Equals(action, Action))
return; return;
IsLit = false; Deactivate(forwards);
if (!forwards)
Decrement();
} }
} }
} }

View File

@ -0,0 +1,109 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// A flowing display of all gameplay keys. Individual keys can be added using <see cref="InputTrigger"/> implementations.
/// </summary>
public abstract partial class KeyCounterDisplay : CompositeDrawable
{
/// <summary>
/// Whether the key counter should be visible regardless of the configuration value.
/// This is true by default, but can be changed.
/// </summary>
public Bindable<bool> AlwaysVisible { get; } = new Bindable<bool>(true);
/// <summary>
/// The <see cref="KeyCounter"/>s contained in this <see cref="KeyCounterDisplay"/>.
/// </summary>
public abstract IEnumerable<KeyCounter> Counters { get; }
/// <summary>
/// Whether the actions reported by all <see cref="InputTrigger"/>s within this <see cref="KeyCounterDisplay"/> should be counted.
/// </summary>
public Bindable<bool> IsCounting { get; } = new BindableBool(true);
protected readonly Bindable<bool> ConfigVisibility = new Bindable<bool>();
protected abstract void UpdateVisibility();
private Receptor? receptor;
public void SetReceptor(Receptor receptor)
{
if (this.receptor != null)
throw new InvalidOperationException("Cannot set a new receptor when one is already active");
this.receptor = receptor;
}
/// <summary>
/// Add a <see cref="InputTrigger"/> to this display.
/// </summary>
public abstract void Add(InputTrigger trigger);
/// <summary>
/// Add a range of <see cref="InputTrigger"/> to this display.
/// </summary>
public void AddRange(IEnumerable<InputTrigger> triggers) => triggers.ForEach(Add);
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility);
}
protected override void LoadComplete()
{
base.LoadComplete();
AlwaysVisible.BindValueChanged(_ => UpdateVisibility());
ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true);
}
public override bool HandleNonPositionalInput => receptor == null;
public override bool HandlePositionalInput => receptor == null;
public partial class Receptor : Drawable
{
protected readonly KeyCounterDisplay Target;
public Receptor(KeyCounterDisplay target)
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
Target = target;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e)
{
switch (e)
{
case KeyDownEvent:
case KeyUpEvent:
case MouseDownEvent:
case MouseUpEvent:
return Target.InternalChildren.Any(c => c.TriggerEvent(e));
}
return base.Handle(e);
}
}
}
}

View File

@ -1,18 +1,16 @@
// 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.
#nullable disable
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play.HUD
{ {
public partial class KeyCounterKeyboard : KeyCounter public partial class KeyCounterKeyboardTrigger : InputTrigger
{ {
public Key Key { get; } public Key Key { get; }
public KeyCounterKeyboard(Key key) public KeyCounterKeyboardTrigger(Key key)
: base(key.ToString()) : base(key.ToString())
{ {
Key = key; Key = key;
@ -22,8 +20,7 @@ namespace osu.Game.Screens.Play
{ {
if (e.Key == Key) if (e.Key == Key)
{ {
IsLit = true; Activate();
Increment();
} }
return base.OnKeyDown(e); return base.OnKeyDown(e);
@ -31,7 +28,9 @@ namespace osu.Game.Screens.Play
protected override void OnKeyUp(KeyUpEvent e) protected override void OnKeyUp(KeyUpEvent e)
{ {
if (e.Key == Key) IsLit = false; if (e.Key == Key)
Deactivate();
base.OnKeyUp(e); base.OnKeyUp(e);
} }
} }

View File

@ -1,19 +1,17 @@
// 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.
#nullable disable
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osuTK.Input;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play.HUD
{ {
public partial class KeyCounterMouse : KeyCounter public partial class KeyCounterMouseTrigger : InputTrigger
{ {
public MouseButton Button { get; } public MouseButton Button { get; }
public KeyCounterMouse(MouseButton button) public KeyCounterMouseTrigger(MouseButton button)
: base(getStringRepresentation(button)) : base(getStringRepresentation(button))
{ {
Button = button; Button = button;
@ -39,17 +37,16 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button == Button) if (e.Button == Button)
{ Activate();
IsLit = true;
Increment();
}
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
protected override void OnMouseUp(MouseUpEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
if (e.Button == Button) IsLit = false; if (e.Button == Button)
Deactivate();
base.OnMouseUp(e); base.OnMouseUp(e);
} }
} }

View File

@ -331,7 +331,7 @@ namespace osu.Game.Screens.Play
ShowHealth = { BindTarget = ShowHealthBar } ShowHealth = { BindTarget = ShowHealthBar }
}; };
protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,

View File

@ -1,172 +0,0 @@
// 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.
#nullable disable
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.Input.Events;
using osu.Game.Configuration;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play
{
public partial class KeyCounterDisplay : Container<KeyCounter>
{
private const int duration = 100;
private const double key_fade_time = 80;
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
protected readonly FillFlowContainer<KeyCounter> KeyFlow;
protected override Container<KeyCounter> Content => KeyFlow;
/// <summary>
/// Whether the key counter should be visible regardless of the configuration value.
/// This is true by default, but can be changed.
/// </summary>
public readonly Bindable<bool> AlwaysVisible = new Bindable<bool>(true);
public KeyCounterDisplay()
{
InternalChild = KeyFlow = new FillFlowContainer<KeyCounter>
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Alpha = 0,
};
}
protected override void Update()
{
base.Update();
// Don't use autosize as it will shrink to zero when KeyFlow is hidden.
// In turn this can cause the display to be masked off screen and never become visible again.
Size = KeyFlow.Size;
}
public override void Add(KeyCounter key)
{
ArgumentNullException.ThrowIfNull(key);
base.Add(key);
key.IsCounting = IsCounting;
key.FadeTime = key_fade_time;
key.KeyDownTextColor = KeyDownTextColor;
key.KeyUpTextColor = KeyUpTextColor;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.KeyOverlay, configVisibility);
}
protected override void LoadComplete()
{
base.LoadComplete();
AlwaysVisible.BindValueChanged(_ => updateVisibility());
configVisibility.BindValueChanged(_ => updateVisibility(), true);
}
private bool isCounting = true;
public bool IsCounting
{
get => isCounting;
set
{
if (value == isCounting) return;
isCounting = value;
foreach (var child in Children)
child.IsCounting = value;
}
}
private Color4 keyDownTextColor = Color4.DarkGray;
public Color4 KeyDownTextColor
{
get => keyDownTextColor;
set
{
if (value != keyDownTextColor)
{
keyDownTextColor = value;
foreach (var child in Children)
child.KeyDownTextColor = value;
}
}
}
private Color4 keyUpTextColor = Color4.White;
public Color4 KeyUpTextColor
{
get => keyUpTextColor;
set
{
if (value != keyUpTextColor)
{
keyUpTextColor = value;
foreach (var child in Children)
child.KeyUpTextColor = value;
}
}
}
private void updateVisibility() =>
// Isolate changing visibility of the key counters from fading this component.
KeyFlow.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
public override bool HandleNonPositionalInput => receptor == null;
public override bool HandlePositionalInput => receptor == null;
private Receptor receptor;
public void SetReceptor(Receptor receptor)
{
if (this.receptor != null)
throw new InvalidOperationException("Cannot set a new receptor when one is already active");
this.receptor = receptor;
}
public partial class Receptor : Drawable
{
protected readonly KeyCounterDisplay Target;
public Receptor(KeyCounterDisplay target)
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
Target = target;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e)
{
switch (e)
{
case KeyDownEvent:
case KeyUpEvent:
case MouseDownEvent:
case MouseUpEvent:
return Target.Children.Any(c => c.TriggerEvent(e));
}
return base.Handle(e);
}
}
}
}

View File

@ -437,8 +437,11 @@ namespace osu.Game.Screens.Play
}, },
KeyCounter = KeyCounter =
{ {
IsCounting =
{
Value = false
},
AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded },
IsCounting = false
}, },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
@ -478,7 +481,7 @@ namespace osu.Game.Screens.Play
{ {
updateGameplayState(); updateGameplayState();
updatePauseOnFocusLostState(); updatePauseOnFocusLostState();
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue;
} }
private void updateGameplayState() private void updateGameplayState()