mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into copyexternalurl-user
This commit is contained in:
@ -1,37 +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.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A clock which is used by <see cref="MultiSpectatorPlayer"/>s and managed by an <see cref="ISyncManager"/>.
|
||||
/// </summary>
|
||||
public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts this <see cref="ISpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
new void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops this <see cref="ISpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
new void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is waiting on frames to continue playback.
|
||||
/// </summary>
|
||||
Bindable<bool> WaitingOnFrames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||
/// </remarks>
|
||||
bool IsCatchingUp { get; set; }
|
||||
}
|
||||
}
|
@ -1,42 +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.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the synchronisation between one or more <see cref="ISpectatorPlayerClock"/>s in relation to a master clock.
|
||||
/// </summary>
|
||||
public interface ISyncManager
|
||||
{
|
||||
/// <summary>
|
||||
/// An event which is invoked when gameplay is ready to start.
|
||||
/// </summary>
|
||||
event Action? ReadyToStart;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which player clocks should synchronise to.
|
||||
/// </summary>
|
||||
GameplayClockContainer MasterClock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
|
||||
/// </summary>
|
||||
IBindable<MasterClockState> MasterState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new managed <see cref="ISpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
/// <returns>The newly created <see cref="ISpectatorPlayerClock"/>.</returns>
|
||||
ISpectatorPlayerClock CreateManagedClock();
|
||||
|
||||
/// <summary>
|
||||
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
|
||||
void RemoveManagedClock(ISpectatorPlayerClock clock);
|
||||
}
|
||||
}
|
@ -15,14 +15,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public class MultiSpectatorPlayer : SpectatorPlayer
|
||||
{
|
||||
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
|
||||
private readonly ISpectatorPlayerClock spectatorPlayerClock;
|
||||
private readonly SpectatorPlayerClock spectatorPlayerClock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The score containing the player's replay.</param>
|
||||
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
|
||||
public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock)
|
||||
public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock)
|
||||
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
|
||||
{
|
||||
this.spectatorPlayerClock = spectatorPlayerClock;
|
||||
@ -40,9 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
protected override void Update()
|
||||
{
|
||||
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
|
||||
CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock;
|
||||
|
||||
if (catchUpClock.IsRunning)
|
||||
if (GameplayClockContainer.SourceClock.IsRunning)
|
||||
GameplayClockContainer.Start();
|
||||
else
|
||||
GameplayClockContainer.Stop();
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@ -47,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
private readonly PlayerArea[] instances;
|
||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||
private ISyncManager syncManager = null!;
|
||||
private SpectatorSyncManager syncManager = null!;
|
||||
private PlayerGrid grid = null!;
|
||||
private MultiSpectatorLeaderboard leaderboard = null!;
|
||||
private PlayerArea? currentAudioSource;
|
||||
@ -80,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
|
||||
(Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer)),
|
||||
masterClockContainer.WithChild(new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
||||
{
|
||||
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
|
||||
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime))
|
||||
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime))
|
||||
.FirstOrDefault();
|
||||
|
||||
foreach (var instance in instances)
|
||||
@ -176,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
}
|
||||
|
||||
private bool isCandidateAudioSource(ISpectatorPlayerClock? clock)
|
||||
private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
|
||||
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
|
||||
|
||||
private void onReadyToStart()
|
||||
@ -198,6 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
|
||||
{
|
||||
Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}");
|
||||
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case MasterClockState.Synchronised:
|
||||
|
@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public readonly int UserId;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// The <see cref="SpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||
/// </summary>
|
||||
public readonly ISpectatorPlayerClock GameplayClock;
|
||||
public readonly SpectatorPlayerClock GameplayClock;
|
||||
|
||||
/// <summary>
|
||||
/// The currently-loaded score.
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private readonly LoadingLayer loadingLayer;
|
||||
private OsuScreenStack? stack;
|
||||
|
||||
public PlayerArea(int userId, ISpectatorPlayerClock clock)
|
||||
public PlayerArea(int userId, SpectatorPlayerClock clock)
|
||||
{
|
||||
UserId = userId;
|
||||
GameplayClock = clock;
|
||||
|
@ -4,44 +4,58 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment.
|
||||
/// A clock which catches up using rate adjustment.
|
||||
/// </summary>
|
||||
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
|
||||
public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||
{
|
||||
/// <summary>
|
||||
/// The catch up rate.
|
||||
/// </summary>
|
||||
public const double CATCHUP_RATE = 2;
|
||||
|
||||
public readonly IFrameBasedClock Source;
|
||||
private readonly GameplayClockContainer masterClock;
|
||||
|
||||
public double CurrentTime { get; private set; }
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
/// <summary>
|
||||
/// Whether this clock is waiting on frames to continue playback.
|
||||
/// </summary>
|
||||
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
|
||||
|
||||
public CatchUpSpectatorPlayerClock(IFrameBasedClock source)
|
||||
/// <summary>
|
||||
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||
/// </remarks>
|
||||
public bool IsCatchingUp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this spectator clock should be running.
|
||||
/// Use instead of <see cref="Start"/> / <see cref="Stop"/> to control time.
|
||||
/// </summary>
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
public SpectatorPlayerClock(GameplayClockContainer masterClock)
|
||||
{
|
||||
Source = source;
|
||||
this.masterClock = masterClock;
|
||||
}
|
||||
|
||||
public void Reset() => CurrentTime = 0;
|
||||
|
||||
public void Start() => IsRunning = true;
|
||||
|
||||
public void Stop() => IsRunning = false;
|
||||
|
||||
void IAdjustableClock.Start()
|
||||
public void Start()
|
||||
{
|
||||
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
|
||||
// Our running state should only be managed by SpectatorSyncManager via IsRunning.
|
||||
}
|
||||
|
||||
void IAdjustableClock.Stop()
|
||||
public void Stop()
|
||||
{
|
||||
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
|
||||
// Our running state should only be managed by an SpectatorSyncManager via IsRunning.
|
||||
}
|
||||
|
||||
public bool Seek(double position)
|
||||
@ -69,16 +83,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
ElapsedFrameTime = 0;
|
||||
FramesPerSecond = 0;
|
||||
|
||||
Source.ProcessFrame();
|
||||
masterClock.ProcessFrame();
|
||||
|
||||
if (IsRunning)
|
||||
{
|
||||
double elapsedSource = Source.ElapsedFrameTime;
|
||||
double elapsedSource = masterClock.ElapsedFrameTime;
|
||||
double elapsed = elapsedSource * Rate;
|
||||
|
||||
CurrentTime += elapsed;
|
||||
ElapsedFrameTime = elapsed;
|
||||
FramesPerSecond = Source.FramesPerSecond;
|
||||
FramesPerSecond = masterClock.FramesPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,9 +101,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public double FramesPerSecond { get; private set; }
|
||||
|
||||
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||
|
||||
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
|
||||
|
||||
public bool IsCatchingUp { get; set; }
|
||||
}
|
||||
}
|
@ -11,9 +11,9 @@ using osu.Game.Screens.Play;
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ISyncManager"/> which synchronises de-synced player clocks through catchup.
|
||||
/// Manages the synchronisation between one or more <see cref="SpectatorPlayerClock"/>s in relation to a master clock.
|
||||
/// </summary>
|
||||
public class CatchUpSyncManager : Component, ISyncManager
|
||||
public class SpectatorSyncManager : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
|
||||
@ -30,41 +30,57 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public const double MAXIMUM_START_DELAY = 15000;
|
||||
|
||||
/// <summary>
|
||||
/// An event which is invoked when gameplay is ready to start.
|
||||
/// </summary>
|
||||
public event Action? ReadyToStart;
|
||||
|
||||
/// <summary>
|
||||
/// The catch-up state of the master clock.
|
||||
/// </summary>
|
||||
public IBindable<MasterClockState> MasterState => masterState;
|
||||
|
||||
public double CurrentMasterTime => masterClock.CurrentTime;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which is used to control the timing of all player clocks clocks.
|
||||
/// </summary>
|
||||
public GameplayClockContainer MasterClock { get; }
|
||||
|
||||
public IBindable<MasterClockState> MasterState => masterState;
|
||||
private readonly GameplayClockContainer masterClock;
|
||||
|
||||
/// <summary>
|
||||
/// The player clocks.
|
||||
/// </summary>
|
||||
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>();
|
||||
private readonly List<SpectatorPlayerClock> playerClocks = new List<SpectatorPlayerClock>();
|
||||
|
||||
private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>();
|
||||
|
||||
private bool hasStarted;
|
||||
private double? firstStartAttemptTime;
|
||||
|
||||
public CatchUpSyncManager(GameplayClockContainer master)
|
||||
public SpectatorSyncManager(GameplayClockContainer master)
|
||||
{
|
||||
MasterClock = master;
|
||||
masterClock = master;
|
||||
}
|
||||
|
||||
public ISpectatorPlayerClock CreateManagedClock()
|
||||
/// <summary>
|
||||
/// Create a new managed <see cref="SpectatorPlayerClock"/>.
|
||||
/// </summary>
|
||||
/// <returns>The newly created <see cref="SpectatorPlayerClock"/>.</returns>
|
||||
public SpectatorPlayerClock CreateManagedClock()
|
||||
{
|
||||
var clock = new CatchUpSpectatorPlayerClock(MasterClock);
|
||||
var clock = new SpectatorPlayerClock(masterClock);
|
||||
playerClocks.Add(clock);
|
||||
return clock;
|
||||
}
|
||||
|
||||
public void RemoveManagedClock(ISpectatorPlayerClock clock)
|
||||
/// <summary>
|
||||
/// Removes an <see cref="SpectatorPlayerClock"/>, stopping it from being managed by this <see cref="SpectatorSyncManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="clock">The <see cref="SpectatorPlayerClock"/> to remove.</param>
|
||||
public void RemoveManagedClock(SpectatorPlayerClock clock)
|
||||
{
|
||||
playerClocks.Remove(clock);
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -75,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
// Ensure all player clocks are stopped until the start succeeds.
|
||||
foreach (var clock in playerClocks)
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -128,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
// How far this player's clock is out of sync, compared to the master clock.
|
||||
// A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up).
|
||||
double timeDelta = MasterClock.CurrentTime - clock.CurrentTime;
|
||||
double timeDelta = masterClock.CurrentTime - clock.CurrentTime;
|
||||
|
||||
// Check that the player clock isn't too far ahead.
|
||||
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
|
||||
@ -137,15 +153,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
// Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock
|
||||
// when it is required to be running (ie. if all players are ahead of the master).
|
||||
clock.IsCatchingUp = false;
|
||||
clock.Stop();
|
||||
clock.IsRunning = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the player clock is running if it can.
|
||||
if (!clock.WaitingOnFrames.Value)
|
||||
clock.Start();
|
||||
else
|
||||
clock.Stop();
|
||||
clock.IsRunning = !clock.WaitingOnFrames.Value;
|
||||
|
||||
if (clock.IsCatchingUp)
|
||||
{
|
Reference in New Issue
Block a user