mirror of
https://github.com/osukey/osukey.git
synced 2025-07-02 00:40:09 +09:00
Split RulesetInputManager out to FrameStabilityContainer
This commit is contained in:
161
osu.Game/Rulesets/UI/FrameStabilityContainer.cs
Normal file
161
osu.Game/Rulesets/UI/FrameStabilityContainer.cs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Input.Handlers;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A container which consumes a parent gameplay clock and standardises frame counts for children.
|
||||||
|
/// Will ensure a minimum of 40 frames per clock second is maintained, regardless of any system lag or seeks.
|
||||||
|
/// </summary>
|
||||||
|
public class FrameStabilityContainer : Container, IHasReplayHandler
|
||||||
|
{
|
||||||
|
public FrameStabilityContainer()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Clock control
|
||||||
|
|
||||||
|
private readonly ManualClock manualClock;
|
||||||
|
|
||||||
|
private readonly FramedClock framedClock;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private GameplayClock gameplayClock;
|
||||||
|
|
||||||
|
private IFrameBasedClock parentGameplayClock;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(GameplayClock clock)
|
||||||
|
{
|
||||||
|
if (clock != null)
|
||||||
|
parentGameplayClock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
setClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we are running up-to-date with our parent clock.
|
||||||
|
/// If not, we will need to keep processing children until we catch up.
|
||||||
|
/// </summary>
|
||||||
|
private bool requireMoreUpdateLoops;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we are in a valid state (ie. should we keep processing children frames).
|
||||||
|
/// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
|
||||||
|
/// </summary>
|
||||||
|
private bool validState;
|
||||||
|
|
||||||
|
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
|
||||||
|
|
||||||
|
private bool isAttached => ReplayInputHandler != null;
|
||||||
|
|
||||||
|
private const int max_catch_up_updates_per_frame = 50;
|
||||||
|
|
||||||
|
private const double sixty_frame_time = 1000.0 / 60;
|
||||||
|
|
||||||
|
public override bool UpdateSubTree()
|
||||||
|
{
|
||||||
|
requireMoreUpdateLoops = true;
|
||||||
|
validState = true;
|
||||||
|
|
||||||
|
int loops = 0;
|
||||||
|
|
||||||
|
while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
|
||||||
|
{
|
||||||
|
updateClock();
|
||||||
|
|
||||||
|
if (validState)
|
||||||
|
{
|
||||||
|
base.UpdateSubTree();
|
||||||
|
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClock()
|
||||||
|
{
|
||||||
|
if (parentGameplayClock == null)
|
||||||
|
setClock(); // LoadComplete may not be run yet, but we still want the clock.
|
||||||
|
|
||||||
|
validState = true;
|
||||||
|
|
||||||
|
manualClock.Rate = parentGameplayClock.Rate;
|
||||||
|
manualClock.IsRunning = parentGameplayClock.IsRunning;
|
||||||
|
|
||||||
|
var newProposedTime = parentGameplayClock.CurrentTime;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
|
||||||
|
{
|
||||||
|
newProposedTime = manualClock.Rate > 0
|
||||||
|
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
|
||||||
|
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAttached)
|
||||||
|
{
|
||||||
|
manualClock.CurrentTime = newProposedTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime);
|
||||||
|
|
||||||
|
if (newTime == null)
|
||||||
|
{
|
||||||
|
// we shouldn't execute for this time value. probably waiting on more replay data.
|
||||||
|
validState = false;
|
||||||
|
|
||||||
|
requireMoreUpdateLoops = true;
|
||||||
|
manualClock.CurrentTime = newProposedTime;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualClock.CurrentTime = newTime.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// The manual clock time has changed in the above code. The framed clock now needs to be updated
|
||||||
|
// to ensure that the its time is valid for our children before input is processed
|
||||||
|
framedClock.ProcessFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setClock()
|
||||||
|
{
|
||||||
|
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||||
|
if (parentGameplayClock == null)
|
||||||
|
parentGameplayClock = Clock;
|
||||||
|
|
||||||
|
Clock = gameplayClock;
|
||||||
|
ProcessCustomClock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IHasReplayHandler
|
||||||
|
|
||||||
|
public ReplayInputHandler ReplayInputHandler { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -132,6 +132,8 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
|
protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
|
||||||
|
|
||||||
|
protected FrameStabilityContainer FrameStabilityContainer;
|
||||||
|
|
||||||
public Score ReplayScore { get; private set; }
|
public Score ReplayScore { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -149,7 +151,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available");
|
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available");
|
||||||
|
|
||||||
ReplayScore = replayScore;
|
ReplayScore = replayScore;
|
||||||
ReplayInputManager.ReplayInputHandler = replayScore != null ? CreateReplayInputHandler(replayScore.Replay) : null;
|
|
||||||
|
var handler = replayScore != null ? CreateReplayInputHandler(replayScore.Replay) : null;
|
||||||
|
|
||||||
|
ReplayInputManager.ReplayInputHandler = handler;
|
||||||
|
FrameStabilityContainer.ReplayInputHandler = handler;
|
||||||
|
|
||||||
HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null;
|
HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null;
|
||||||
}
|
}
|
||||||
@ -243,7 +249,6 @@ namespace osu.Game.Rulesets.UI
|
|||||||
Beatmap = (Beatmap<TObject>)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
|
Beatmap = (Beatmap<TObject>)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
|
||||||
|
|
||||||
KeyBindingInputManager = CreateInputManager();
|
KeyBindingInputManager = CreateInputManager();
|
||||||
KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
applyBeatmapMods(Mods);
|
applyBeatmapMods(Mods);
|
||||||
}
|
}
|
||||||
@ -262,7 +267,10 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
KeyBindingInputManager,
|
FrameStabilityContainer = new FrameStabilityContainer
|
||||||
|
{
|
||||||
|
Child = KeyBindingInputManager,
|
||||||
|
},
|
||||||
Overlays = new Container { RelativeSizeAxes = Axes.Both }
|
Overlays = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +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.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -12,7 +11,6 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Input.StateChanges.Events;
|
using osu.Framework.Input.StateChanges.Events;
|
||||||
using osu.Framework.Input.States;
|
using osu.Framework.Input.States;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Input.Handlers;
|
using osu.Game.Input.Handlers;
|
||||||
@ -41,7 +39,12 @@ namespace osu.Game.Rulesets.UI
|
|||||||
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||||
{
|
{
|
||||||
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
|
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
|
||||||
gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Action mapping (for replays)
|
#region Action mapping (for replays)
|
||||||
@ -85,137 +88,6 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Clock control
|
|
||||||
|
|
||||||
private readonly ManualClock manualClock;
|
|
||||||
|
|
||||||
private readonly FramedClock framedClock;
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private GameplayClock gameplayClock;
|
|
||||||
|
|
||||||
private IFrameBasedClock parentGameplayClock;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
|
||||||
private void load(OsuConfigManager config, GameplayClock clock)
|
|
||||||
{
|
|
||||||
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
|
|
||||||
|
|
||||||
if (clock != null)
|
|
||||||
parentGameplayClock = clock;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
setClock();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether we are running up-to-date with our parent clock.
|
|
||||||
/// If not, we will need to keep processing children until we catch up.
|
|
||||||
/// </summary>
|
|
||||||
private bool requireMoreUpdateLoops;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether we are in a valid state (ie. should we keep processing children frames).
|
|
||||||
/// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
|
|
||||||
/// </summary>
|
|
||||||
private bool validState;
|
|
||||||
|
|
||||||
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
|
|
||||||
|
|
||||||
private bool isAttached => replayInputHandler != null && !UseParentInput;
|
|
||||||
|
|
||||||
private const int max_catch_up_updates_per_frame = 50;
|
|
||||||
|
|
||||||
private const double sixty_frame_time = 1000.0 / 60;
|
|
||||||
|
|
||||||
public override bool UpdateSubTree()
|
|
||||||
{
|
|
||||||
requireMoreUpdateLoops = true;
|
|
||||||
validState = true;
|
|
||||||
|
|
||||||
int loops = 0;
|
|
||||||
|
|
||||||
while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
|
|
||||||
{
|
|
||||||
updateClock();
|
|
||||||
|
|
||||||
if (validState)
|
|
||||||
{
|
|
||||||
base.UpdateSubTree();
|
|
||||||
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateClock()
|
|
||||||
{
|
|
||||||
if (parentGameplayClock == null)
|
|
||||||
setClock(); // LoadComplete may not be run yet, but we still want the clock.
|
|
||||||
|
|
||||||
validState = true;
|
|
||||||
|
|
||||||
manualClock.Rate = parentGameplayClock.Rate;
|
|
||||||
manualClock.IsRunning = parentGameplayClock.IsRunning;
|
|
||||||
|
|
||||||
var newProposedTime = parentGameplayClock.CurrentTime;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
|
|
||||||
{
|
|
||||||
newProposedTime = manualClock.Rate > 0
|
|
||||||
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
|
|
||||||
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAttached)
|
|
||||||
{
|
|
||||||
manualClock.CurrentTime = newProposedTime;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
double? newTime = replayInputHandler.SetFrameFromTime(newProposedTime);
|
|
||||||
|
|
||||||
if (newTime == null)
|
|
||||||
{
|
|
||||||
// we shouldn't execute for this time value. probably waiting on more replay data.
|
|
||||||
validState = false;
|
|
||||||
|
|
||||||
requireMoreUpdateLoops = true;
|
|
||||||
manualClock.CurrentTime = newProposedTime;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
manualClock.CurrentTime = newTime.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// The manual clock time has changed in the above code. The framed clock now needs to be updated
|
|
||||||
// to ensure that the its time is valid for our children before input is processed
|
|
||||||
framedClock.ProcessFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setClock()
|
|
||||||
{
|
|
||||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
|
||||||
if (parentGameplayClock == null)
|
|
||||||
parentGameplayClock = Clock;
|
|
||||||
|
|
||||||
Clock = gameplayClock;
|
|
||||||
ProcessCustomClock = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Setting application (disables etc.)
|
#region Setting application (disables etc.)
|
||||||
|
|
||||||
private Bindable<bool> mouseDisabled;
|
private Bindable<bool> mouseDisabled;
|
||||||
|
Reference in New Issue
Block a user