1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 20:32:55 +08:00

Merge pull request #672 from peppy/replay-playback-accuracy

Improve replay playback accuracy
This commit is contained in:
Dan Balasescu 2017-04-26 20:43:37 +09:00 committed by GitHub
commit ea226cff76
6 changed files with 110 additions and 53 deletions

@ -1 +1 @@
Subproject commit cc50d1251b00876331691ea2ce7ed18174e4eded Subproject commit dcbd7a0b6f536f6aadf13a720db40a1d76bf52e2

View File

@ -38,9 +38,9 @@ namespace osu.Desktop.VisualTests.Tests
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
}); });
AddStep("Toggle Bar", progress.ToggleBar); AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
AddWaitStep(5); AddWaitStep(5);
AddStep("Toggle Bar", progress.ToggleBar); AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
AddWaitStep(2); AddWaitStep(2);
AddRepeatStep("New Values", displayNewValues, 5); AddRepeatStep("New Values", displayNewValues, 5);

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.UI
where TObject : HitObject where TObject : HitObject
{ {
/// <summary> /// <summary>
/// The Beatmap /// The Beatmap
/// </summary> /// </summary>
public Beatmap<TObject> Beatmap; public Beatmap<TObject> Beatmap;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play
public Action RestartRequested; public Action RestartRequested;
public bool IsPaused => !interpolatedSourceClock.IsRunning; public bool IsPaused => !decoupledClock.IsRunning;
internal override bool AllowRulesetChange => false; internal override bool AllowRulesetChange => false;
@ -51,9 +51,9 @@ namespace osu.Game.Screens.Play
private bool canPause => ValidForResume && !HasFailed && Time.Current >= lastPauseActionTime + pause_cooldown; private bool canPause => ValidForResume && !HasFailed && Time.Current >= lastPauseActionTime + pause_cooldown;
private IAdjustableClock sourceClock; private IAdjustableClock adjustableSourceClock;
private OffsetClock offsetClock; private FramedOffsetClock offsetClock;
private IFrameBasedClock interpolatedSourceClock; private DecoupleableInterpolatingFramedClock decoupledClock;
private RulesetInfo ruleset; private RulesetInfo ruleset;
@ -70,6 +70,8 @@ namespace osu.Game.Screens.Play
private SkipButton skipButton; private SkipButton skipButton;
private Container hitRendererContainer;
private HudOverlay hudOverlay; private HudOverlay hudOverlay;
private PauseOverlay pauseOverlay; private PauseOverlay pauseOverlay;
private FailOverlay failOverlay; private FailOverlay failOverlay;
@ -87,10 +89,7 @@ namespace osu.Game.Screens.Play
if (Beatmap == null) if (Beatmap == null)
Beatmap = beatmaps.GetWorkingBeatmap(BeatmapInfo, withStoryboard: true); Beatmap = beatmaps.GetWorkingBeatmap(BeatmapInfo, withStoryboard: true);
if ((Beatmap?.Beatmap?.HitObjects.Count ?? 0) == 0) if (Beatmap?.Beatmap == null)
throw new Exception("No valid objects were found!");
if (Beatmap == null)
throw new Exception("Beatmap was not loaded"); throw new Exception("Beatmap was not loaded");
ruleset = osu?.Ruleset.Value ?? Beatmap.BeatmapInfo.Ruleset; ruleset = osu?.Ruleset.Value ?? Beatmap.BeatmapInfo.Ruleset;
@ -108,6 +107,9 @@ namespace osu.Game.Screens.Play
rulesetInstance = ruleset.CreateInstance(); rulesetInstance = ruleset.CreateInstance();
HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap); HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap);
} }
if (!HitRenderer.Objects.Any())
throw new Exception("Beatmap contains no hit objects!");
} }
catch (Exception e) catch (Exception e)
{ {
@ -123,23 +125,31 @@ namespace osu.Game.Screens.Play
if (track != null) if (track != null)
{ {
audio.Track.SetExclusive(track); audio.Track.SetExclusive(track);
sourceClock = track; adjustableSourceClock = track;
} }
sourceClock = (IAdjustableClock)track ?? new StopwatchClock(); adjustableSourceClock = (IAdjustableClock)track ?? new StopwatchClock();
offsetClock = new OffsetClock(sourceClock);
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
var firstObjectTime = HitRenderer.Objects.First().StartTime;
decoupledClock.Seek(Math.Min(0, firstObjectTime - Math.Max(Beatmap.Beatmap.TimingInfo.BeatLengthAt(firstObjectTime) * 4, Beatmap.BeatmapInfo.AudioLeadIn)));
decoupledClock.ProcessFrame();
offsetClock = new FramedOffsetClock(decoupledClock);
userAudioOffset = config.GetBindable<double>(OsuConfig.AudioOffset); userAudioOffset = config.GetBindable<double>(OsuConfig.AudioOffset);
userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.ValueChanged += v => offsetClock.Offset = v;
userAudioOffset.TriggerChange(); userAudioOffset.TriggerChange();
interpolatedSourceClock = new InterpolatingFramedClock(offsetClock);
Schedule(() => Schedule(() =>
{ {
sourceClock.Reset(); adjustableSourceClock.Reset();
foreach (var mod in Beatmap.Mods.Value.OfType<IApplicableToClock>()) foreach (var mod in Beatmap.Mods.Value.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock); mod.ApplyToClock(adjustableSourceClock);
decoupledClock.ChangeSource(adjustableSourceClock);
}); });
scoreProcessor = HitRenderer.CreateScoreProcessor(); scoreProcessor = HitRenderer.CreateScoreProcessor();
@ -155,7 +165,9 @@ namespace osu.Game.Screens.Play
hudOverlay.BindHitRenderer(HitRenderer); hudOverlay.BindHitRenderer(HitRenderer);
hudOverlay.Progress.Objects = HitRenderer.Objects; hudOverlay.Progress.Objects = HitRenderer.Objects;
hudOverlay.Progress.AudioClock = interpolatedSourceClock; hudOverlay.Progress.AudioClock = decoupledClock;
hudOverlay.Progress.AllowSeeking = HitRenderer.HasReplayLoaded;
hudOverlay.Progress.OnSeek = pos => decoupledClock.Seek(pos);
//bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation)
HitRenderer.OnAllJudged += onCompletion; HitRenderer.OnAllJudged += onCompletion;
@ -165,16 +177,20 @@ namespace osu.Game.Screens.Play
Children = new Drawable[] Children = new Drawable[]
{ {
new Container hitRendererContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Clock = interpolatedSourceClock,
Children = new Drawable[] Children = new Drawable[]
{ {
HitRenderer, new Container
skipButton = new SkipButton
{ {
Alpha = 0 RelativeSizeAxes = Axes.Both,
Clock = offsetClock,
Children = new Drawable[]
{
HitRenderer,
skipButton = new SkipButton { Alpha = 0 },
}
}, },
} }
}, },
@ -224,7 +240,7 @@ namespace osu.Game.Screens.Play
skipButton.Action = () => skipButton.Action = () =>
{ {
sourceClock.Seek(firstHitObject - skip_required_cutoff - fade_time); decoupledClock.Seek(firstHitObject - skip_required_cutoff - fade_time);
skipButton.Action = null; skipButton.Action = null;
}; };
@ -241,7 +257,7 @@ namespace osu.Game.Screens.Play
// we want to wait for the source clock to stop so we can be sure all components are in a stable state. // we want to wait for the source clock to stop so we can be sure all components are in a stable state.
if (!IsPaused) if (!IsPaused)
{ {
sourceClock.Stop(); decoupledClock.Stop();
Schedule(() => Pause(force)); Schedule(() => Pause(force));
return; return;
@ -268,7 +284,7 @@ namespace osu.Game.Screens.Play
hudOverlay.KeyCounter.IsCounting = true; hudOverlay.KeyCounter.IsCounting = true;
hudOverlay.Progress.Hide(); hudOverlay.Progress.Hide();
pauseOverlay.Hide(); pauseOverlay.Hide();
sourceClock.Start(); decoupledClock.Start();
} }
public void Restart() public void Restart()
@ -304,7 +320,7 @@ namespace osu.Game.Screens.Play
private void onFail() private void onFail()
{ {
sourceClock.Stop(); decoupledClock.Stop();
HasFailed = true; HasFailed = true;
failOverlay.Retries = RestartCount; failOverlay.Retries = RestartCount;
@ -332,13 +348,12 @@ namespace osu.Game.Screens.Play
Delay(750); Delay(750);
Schedule(() => Schedule(() =>
{ {
sourceClock.Start(); decoupledClock.Start();
initializeSkipButton(); initializeSkipButton();
}); });
//keep in mind this is using the interpolatedSourceClock so won't be run as early as we may expect. hitRendererContainer.Alpha = 0;
HitRenderer.Alpha = 0; hitRendererContainer.FadeIn(750, EasingTypes.OutQuint);
HitRenderer.FadeIn(750, EasingTypes.OutQuint);
} }
protected override void OnSuspending(Screen next) protected override void OnSuspending(Screen next)

View File

@ -36,6 +36,38 @@ namespace osu.Game.Screens.Play
Clock = new FramedClock(clock); Clock = new FramedClock(clock);
} }
/// <summary>
/// Whether we 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 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 && !UseParentState;
private const int max_catch_up_updates_per_frame = 50;
public override bool UpdateSubTree()
{
requireMoreUpdateLoops = true;
validState = true;
int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
if (!base.UpdateSubTree())
return false;
return true;
}
protected override void Update() protected override void Update()
{ {
if (parentClock == null) return; if (parentClock == null) return;
@ -43,28 +75,26 @@ namespace osu.Game.Screens.Play
clock.Rate = parentClock.Rate; clock.Rate = parentClock.Rate;
clock.IsRunning = parentClock.IsRunning; clock.IsRunning = parentClock.IsRunning;
//if a replayHandler is not attached, we should just pass-through. if (!isAttached)
if (UseParentState || replayInputHandler == null)
{ {
clock.CurrentTime = parentClock.CurrentTime; clock.CurrentTime = parentClock.CurrentTime;
base.Update();
return;
} }
else
while (true)
{ {
double? newTime = replayInputHandler.SetFrameFromTime(parentClock.CurrentTime); double? newTime = replayInputHandler.SetFrameFromTime(parentClock.CurrentTime);
if (newTime == null) if (newTime == null)
//we shouldn't execute for this time value {
break; // we shouldn't execute for this time value. probably waiting on more replay data.
validState = false;
if (clock.CurrentTime == parentClock.CurrentTime) return;
break; }
clock.CurrentTime = newTime.Value; clock.CurrentTime = newTime.Value;
base.Update();
} }
requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime;
base.Update();
} }
} }
} }

View File

@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1; private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1;
private double firstHitTime => objects.First().StartTime;
private IEnumerable<HitObject> objects; private IEnumerable<HitObject> objects;
public IEnumerable<HitObject> Objects public IEnumerable<HitObject> Objects
@ -75,7 +77,7 @@ namespace osu.Game.Screens.Play
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
SeekRequested = delegate (float position) SeekRequested = delegate (float position)
{ {
OnSeek?.Invoke(position); OnSeek?.Invoke(firstHitTime + position * (lastHitTime - firstHitTime));
}, },
}, },
}; };
@ -86,18 +88,28 @@ namespace osu.Game.Screens.Play
State = Visibility.Visible; State = Visibility.Visible;
} }
private bool barVisible; private bool allowSeeking;
public void ToggleBar() public bool AllowSeeking
{ {
barVisible = !barVisible; get
updateBarVisibility(); {
return allowSeeking;
}
set
{
if (allowSeeking == value) return;
allowSeeking = value;
updateBarVisibility();
}
} }
private void updateBarVisibility() private void updateBarVisibility()
{ {
bar.FadeTo(barVisible ? 1 : 0, transition_duration, EasingTypes.In); bar.FadeTo(allowSeeking ? 1 : 0, transition_duration, EasingTypes.In);
MoveTo(new Vector2(0, barVisible ? 0 : bottom_bar_height), transition_duration, EasingTypes.In); MoveTo(new Vector2(0, allowSeeking ? 0 : bottom_bar_height), transition_duration, EasingTypes.In);
} }
protected override void PopIn() protected override void PopIn()
@ -118,7 +130,7 @@ namespace osu.Game.Screens.Play
if (objects == null) if (objects == null)
return; return;
double progress = (AudioClock?.CurrentTime ?? Time.Current) / lastHitTime; double progress = ((AudioClock?.CurrentTime ?? Time.Current) - firstHitTime) / lastHitTime;
bar.UpdatePosition((float)progress); bar.UpdatePosition((float)progress);
graph.Progress = (int)(graph.ColumnCount * progress); graph.Progress = (int)(graph.ColumnCount * progress);