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:
commit
ea226cff76
@ -1 +1 @@
|
|||||||
Subproject commit cc50d1251b00876331691ea2ce7ed18174e4eded
|
Subproject commit dcbd7a0b6f536f6aadf13a720db40a1d76bf52e2
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user