1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 05:57:29 +08:00
osu-lazer/osu.Game/Screens/Play/GameplayClockContainer.cs

305 lines
11 KiB
C#
Raw Normal View History

// 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;
using osu.Framework.Allocation;
using osu.Framework.Audio;
2020-08-05 20:10:38 +08:00
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
namespace osu.Game.Screens.Play
{
2021-04-14 16:47:11 +08:00
public abstract class GameplayClockContainer : Container
{
2021-04-14 16:47:11 +08:00
/// <summary>
/// The final clock which is exposed to underlying components.
/// </summary>
public GameplayClock GameplayClock { get; private set; }
public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
/// </summary>
2021-04-14 16:47:11 +08:00
protected readonly DecoupleableInterpolatingFramedClock AdjustableClock;
2021-04-14 16:47:11 +08:00
protected readonly IClock SourceClock;
2019-11-21 15:55:18 +08:00
2021-04-14 16:47:11 +08:00
protected GameplayClockContainer(IClock sourceClock)
{
2021-04-14 16:47:11 +08:00
SourceClock = sourceClock;
2021-04-14 16:47:11 +08:00
RelativeSizeAxes = Axes.Both;
2021-04-14 16:47:11 +08:00
AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
AdjustableClock.ChangeSource(SourceClock);
2019-04-03 15:58:20 +08:00
2021-04-14 16:47:11 +08:00
IsPaused.BindValueChanged(OnPauseChanged);
}
2021-04-14 16:47:11 +08:00
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
2021-04-14 16:47:11 +08:00
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
2020-08-06 17:31:08 +08:00
2021-04-14 16:47:11 +08:00
dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock));
2021-04-14 17:29:34 +08:00
GameplayClock.IsPaused.BindTo(IsPaused);
2021-04-14 16:47:11 +08:00
return dependencies;
}
2021-04-14 16:47:11 +08:00
public virtual void Start()
{
if (!AdjustableClock.IsRunning)
{
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the clock source potentially taking time to enter a completely stopped state
Seek(GameplayClock.CurrentTime);
2021-04-14 16:47:11 +08:00
AdjustableClock.Start();
}
2021-04-14 16:47:11 +08:00
IsPaused.Value = false;
}
2021-04-14 16:47:11 +08:00
/// <summary>
/// Seek to a specific time in gameplay.
/// <remarks>
/// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track).
/// </remarks>
/// </summary>
/// <param name="time">The destination time to seek to.</param>
public virtual void Seek(double time) => AdjustableClock.Seek(time);
2021-04-14 16:47:11 +08:00
public virtual void Stop() => IsPaused.Value = true;
2021-04-14 16:47:11 +08:00
public virtual void Restart()
{
2021-04-14 16:47:11 +08:00
AdjustableClock.Seek(0);
AdjustableClock.Stop();
if (!IsPaused.Value)
Start();
}
2021-04-14 16:47:11 +08:00
protected abstract void OnPauseChanged(ValueChangedEvent<bool> isPaused);
protected abstract GameplayClock CreateGameplayClock(IClock source);
}
2019-04-03 15:58:20 +08:00
2021-04-14 16:47:11 +08:00
public class MasterGameplayClockContainer : GameplayClockContainer
{
/// <summary>
/// Duration before gameplay start time required before skip button displays.
/// </summary>
public const double MINIMUM_SKIP_TIME = 1000;
2021-04-14 16:47:11 +08:00
protected new DecoupleableInterpolatingFramedClock SourceClock => (DecoupleableInterpolatingFramedClock)base.SourceClock;
public readonly BindableNumber<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
};
private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset;
2019-06-09 23:56:35 +08:00
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1);
2021-04-14 16:47:11 +08:00
private readonly WorkingBeatmap beatmap;
private readonly double gameplayStartTime;
private readonly bool startAtGameplayStart;
private readonly double firstHitObjectTime;
private FramedOffsetClock userOffsetClock;
private FramedOffsetClock platformOffsetClock;
private LocalGameplayClock localGameplayClock;
private Bindable<double> userAudioOffset;
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false)
: base(new DecoupleableInterpolatingFramedClock())
{
this.beatmap = beatmap;
this.gameplayStartTime = gameplayStartTime;
this.startAtGameplayStart = startAtGameplayStart;
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
SourceClock.ChangeSource(beatmap.Track);
}
[BackgroundDependencyLoader]
2020-08-06 17:31:08 +08:00
private void load(OsuConfigManager config)
{
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
2019-04-03 15:58:20 +08:00
userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true);
2019-11-21 15:55:18 +08:00
// sane default provided by ruleset.
double startTime = gameplayStartTime;
if (!startAtGameplayStart)
{
startTime = Math.Min(0, startTime);
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is commonly used to display an intro before the audio track start.
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
if (firstStoryboardEvent != null)
startTime = Math.Min(startTime, firstStoryboardEvent.Value);
2019-11-21 15:55:18 +08:00
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
}
Seek(startTime);
2021-04-14 16:47:11 +08:00
AdjustableClock.ProcessFrame();
}
2021-04-14 16:47:11 +08:00
protected override void OnPauseChanged(ValueChangedEvent<bool> isPaused)
{
2021-04-14 16:47:11 +08:00
if (isPaused.NewValue)
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop());
else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
}
2021-04-14 16:47:11 +08:00
public override void Seek(double time)
{
2021-04-14 16:47:11 +08:00
// remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track.
// we may want to consider reversing the application of offsets in the future as it may feel more correct.
base.Seek(time - totalOffset);
2019-06-09 23:56:35 +08:00
2021-04-14 16:47:11 +08:00
// manually process frame to ensure GameplayClock is correctly updated after a seek.
userOffsetClock.ProcessFrame();
}
2021-04-14 16:47:11 +08:00
public override void Restart()
{
updateRate();
base.Restart();
}
/// <summary>
2019-11-21 17:57:19 +08:00
/// Skip forward to the next valid skip point.
/// </summary>
public void Skip()
{
if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME)
return;
double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME;
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
// double skip exception for storyboards with very long intros
skipTarget = 0;
Seek(skipTarget);
}
2021-04-14 16:47:11 +08:00
protected override GameplayClock CreateGameplayClock(IClock source)
2019-04-03 15:58:20 +08:00
{
2021-04-14 16:47:11 +08:00
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
2021-04-14 16:47:11 +08:00
// the final usable gameplay clock with user-set offsets applied.
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
return localGameplayClock = new LocalGameplayClock(userOffsetClock);
2019-04-03 15:58:20 +08:00
}
2021-04-14 16:47:11 +08:00
protected override void Update()
{
2021-04-14 16:47:11 +08:00
if (!IsPaused.Value)
userOffsetClock.ProcessFrame();
base.Update();
}
2020-08-05 20:10:38 +08:00
/// <summary>
/// Changes the backing clock to avoid using the originally provided track.
/// </summary>
public void StopUsingBeatmapClock()
{
removeSourceClockAdjustments();
2021-04-14 16:47:11 +08:00
SourceClock.ChangeSource(new TrackVirtual(beatmap.Track.Length));
}
private bool speedAdjustmentsApplied;
private void updateRate()
{
if (speedAdjustmentsApplied)
return;
2020-08-05 20:10:38 +08:00
2021-04-14 16:47:11 +08:00
var track = (Track)SourceClock.Source;
2020-08-12 00:41:21 +08:00
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
2020-08-05 20:10:38 +08:00
localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true;
}
2019-06-10 00:14:46 +08:00
private void removeSourceClockAdjustments()
{
2021-04-14 16:47:11 +08:00
if (!speedAdjustmentsApplied)
return;
var track = (Track)SourceClock.Source;
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false;
2019-06-10 00:14:46 +08:00
}
2021-04-14 16:47:11 +08:00
protected override void Dispose(bool isDisposing)
{
2021-04-14 16:47:11 +08:00
base.Dispose(isDisposing);
removeSourceClockAdjustments();
}
private class HardwareCorrectionOffsetClock : FramedOffsetClock
{
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
// base implementation already adds offset at 1.0 rate, so we only add the difference from that here.
2020-06-14 23:46:20 +08:00
public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1);
public HardwareCorrectionOffsetClock(IClock source, bool processSource = true)
: base(source, processSource)
{
}
}
2021-04-14 16:47:11 +08:00
private class LocalGameplayClock : GameplayClock
{
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
public LocalGameplayClock(FramedOffsetClock underlyingClock)
: base(underlyingClock)
{
}
}
}
}