mirror of
https://github.com/ppy/osu.git
synced 2025-02-01 02:43:09 +08:00
Merge branch 'master' into no-gameplay-clock-gameplay-offset
This commit is contained in:
commit
5079e0d83d
@ -19,20 +19,20 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
public class TestSceneCatchUpSyncManager : OsuTestScene
|
public class TestSceneCatchUpSyncManager : OsuTestScene
|
||||||
{
|
{
|
||||||
private GameplayClockContainer master;
|
private GameplayClockContainer master;
|
||||||
private CatchUpSyncManager syncManager;
|
private SpectatorSyncManager syncManager;
|
||||||
|
|
||||||
private Dictionary<ISpectatorPlayerClock, int> clocksById;
|
private Dictionary<SpectatorPlayerClock, int> clocksById;
|
||||||
private ISpectatorPlayerClock player1;
|
private SpectatorPlayerClock player1;
|
||||||
private ISpectatorPlayerClock player2;
|
private SpectatorPlayerClock player2;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock()));
|
syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock()));
|
||||||
player1 = syncManager.CreateManagedClock();
|
player1 = syncManager.CreateManagedClock();
|
||||||
player2 = syncManager.CreateManagedClock();
|
player2 = syncManager.CreateManagedClock();
|
||||||
|
|
||||||
clocksById = new Dictionary<ISpectatorPlayerClock, int>
|
clocksById = new Dictionary<SpectatorPlayerClock, int>
|
||||||
{
|
{
|
||||||
{ player1, 1 },
|
{ player1, 1 },
|
||||||
{ player2, 2 }
|
{ player2, 2 }
|
||||||
@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
|
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
|
||||||
{
|
{
|
||||||
setWaiting(() => player1, false);
|
setWaiting(() => player1, false);
|
||||||
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
AddWaitStep($"wait {SpectatorSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
assertPlayerClockState(() => player1, true);
|
assertPlayerClockState(() => player1, true);
|
||||||
assertPlayerClockState(() => player2, false);
|
assertPlayerClockState(() => player2, false);
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1);
|
setMasterTime(SpectatorSyncManager.SYNC_TARGET + 1);
|
||||||
assertCatchingUp(() => player1, false);
|
assertCatchingUp(() => player1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
|
setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
|
||||||
assertCatchingUp(() => player1, true);
|
assertCatchingUp(() => player1, true);
|
||||||
assertCatchingUp(() => player2, true);
|
assertCatchingUp(() => player2, true);
|
||||||
}
|
}
|
||||||
@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
|
setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
|
||||||
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1);
|
setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET + 1);
|
||||||
assertCatchingUp(() => player1, true);
|
assertCatchingUp(() => player1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,8 +103,8 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2);
|
setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 2);
|
||||||
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET);
|
setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET);
|
||||||
assertCatchingUp(() => player1, false);
|
assertCatchingUp(() => player1, false);
|
||||||
assertCatchingUp(() => player2, true);
|
assertCatchingUp(() => player2, true);
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET);
|
setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET);
|
||||||
assertCatchingUp(() => player1, false);
|
assertCatchingUp(() => player1, false);
|
||||||
assertPlayerClockState(() => player1, true);
|
assertPlayerClockState(() => player1, true);
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
{
|
{
|
||||||
setAllWaiting(false);
|
setAllWaiting(false);
|
||||||
|
|
||||||
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1);
|
setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET - 1);
|
||||||
|
|
||||||
// This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
|
// This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
|
||||||
assertCatchingUp(() => player1, false);
|
assertCatchingUp(() => player1, false);
|
||||||
@ -145,13 +145,13 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
assertPlayerClockState(() => player1, false);
|
assertPlayerClockState(() => player1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setWaiting(Func<ISpectatorPlayerClock> playerClock, bool waiting)
|
private void setWaiting(Func<SpectatorPlayerClock> playerClock, bool waiting)
|
||||||
=> AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
|
=> AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames = waiting);
|
||||||
|
|
||||||
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
|
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
|
||||||
{
|
{
|
||||||
player1.WaitingOnFrames.Value = waiting;
|
player1.WaitingOnFrames = waiting;
|
||||||
player2.WaitingOnFrames.Value = waiting;
|
player2.WaitingOnFrames = waiting;
|
||||||
});
|
});
|
||||||
|
|
||||||
private void setMasterTime(double time)
|
private void setMasterTime(double time)
|
||||||
@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// clock.Time = master.Time - offsetFromMaster
|
/// clock.Time = master.Time - offsetFromMaster
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void setPlayerClockTime(Func<ISpectatorPlayerClock> playerClock, double offsetFromMaster)
|
private void setPlayerClockTime(Func<SpectatorPlayerClock> playerClock, double offsetFromMaster)
|
||||||
=> AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
|
=> AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
|
||||||
|
|
||||||
private void assertCatchingUp(Func<ISpectatorPlayerClock> playerClock, bool catchingUp) =>
|
private void assertCatchingUp(Func<SpectatorPlayerClock> playerClock, bool catchingUp) =>
|
||||||
AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
|
AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
|
||||||
|
|
||||||
private void assertPlayerClockState(Func<ISpectatorPlayerClock> playerClock, bool running)
|
private void assertPlayerClockState(Func<SpectatorPlayerClock> playerClock, bool running)
|
||||||
=> AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
|
=> AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
|
||||||
|
|
||||||
private class TestManualClock : ManualClock, IAdjustableClock
|
private class TestManualClock : ManualClock, IAdjustableClock
|
||||||
|
@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
checkPausedInstant(PLAYER_2_ID, true);
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
|
||||||
// Wait for the start delay seconds...
|
// Wait for the start delay seconds...
|
||||||
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
|
|
||||||
// Player 1 should start playing by itself, player 2 should remain paused.
|
// Player 1 should start playing by itself, player 2 should remain paused.
|
||||||
checkPausedInstant(PLAYER_1_ID, false);
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
loadSpectateScreen();
|
loadSpectateScreen();
|
||||||
sendFrames(PLAYER_1_ID, 300);
|
sendFrames(PLAYER_1_ID, 300);
|
||||||
|
|
||||||
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
checkPaused(PLAYER_1_ID, false);
|
checkPaused(PLAYER_1_ID, false);
|
||||||
|
|
||||||
sendFrames(PLAYER_2_ID, 300);
|
sendFrames(PLAYER_2_ID, 300);
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -14,15 +13,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class MultiSpectatorPlayer : SpectatorPlayer
|
public class MultiSpectatorPlayer : SpectatorPlayer
|
||||||
{
|
{
|
||||||
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
|
private readonly SpectatorPlayerClock spectatorPlayerClock;
|
||||||
private readonly ISpectatorPlayerClock spectatorPlayerClock;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
|
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score containing the player's replay.</param>
|
/// <param name="score">The score containing the player's replay.</param>
|
||||||
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</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 })
|
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
|
||||||
{
|
{
|
||||||
this.spectatorPlayerClock = spectatorPlayerClock;
|
this.spectatorPlayerClock = spectatorPlayerClock;
|
||||||
@ -31,8 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
|
|
||||||
|
|
||||||
HUDOverlay.PlayerSettingsOverlay.Expire();
|
HUDOverlay.PlayerSettingsOverlay.Expire();
|
||||||
HUDOverlay.HoldToQuit.Expire();
|
HUDOverlay.HoldToQuit.Expire();
|
||||||
}
|
}
|
||||||
@ -40,9 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
protected override void Update()
|
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.
|
// 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 (GameplayClockContainer.SourceClock.IsRunning)
|
||||||
|
|
||||||
if (catchUpClock.IsRunning)
|
|
||||||
GameplayClockContainer.Start();
|
GameplayClockContainer.Start();
|
||||||
else
|
else
|
||||||
GameplayClockContainer.Stop();
|
GameplayClockContainer.Stop();
|
||||||
@ -55,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
|
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
|
||||||
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
|
spectatorPlayerClock.WaitingOnFrames = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||||
|
@ -4,12 +4,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
@ -48,11 +45,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
|
|
||||||
private readonly PlayerArea[] instances;
|
private readonly PlayerArea[] instances;
|
||||||
private MasterGameplayClockContainer masterClockContainer = null!;
|
private MasterGameplayClockContainer masterClockContainer = null!;
|
||||||
private ISyncManager syncManager = null!;
|
private SpectatorSyncManager syncManager = null!;
|
||||||
private PlayerGrid grid = null!;
|
private PlayerGrid grid = null!;
|
||||||
private MultiSpectatorLeaderboard leaderboard = null!;
|
private MultiSpectatorLeaderboard leaderboard = null!;
|
||||||
private PlayerArea? currentAudioSource;
|
private PlayerArea? currentAudioSource;
|
||||||
private bool canStartMasterClock;
|
|
||||||
|
|
||||||
private readonly Room room;
|
private readonly Room room;
|
||||||
private readonly MultiplayerRoomUser[] users;
|
private readonly MultiplayerRoomUser[] users;
|
||||||
@ -77,12 +73,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
FillFlowContainer leaderboardFlow;
|
FillFlowContainer leaderboardFlow;
|
||||||
Container scoreDisplayContainer;
|
Container scoreDisplayContainer;
|
||||||
|
|
||||||
masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value);
|
InternalChildren = new Drawable[]
|
||||||
|
|
||||||
InternalChildren = new[]
|
|
||||||
{
|
{
|
||||||
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
|
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
|
||||||
masterClockContainer.WithChild(new GridContainer
|
{
|
||||||
|
Child = new GridContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||||
@ -120,7 +115,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
syncManager = new SpectatorSyncManager(masterClockContainer)
|
||||||
|
{
|
||||||
|
ReadyToStart = performInitialSeek,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < Users.Count; i++)
|
for (int i = 0; i < Users.Count; i++)
|
||||||
@ -156,8 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
syncManager.ReadyToStart += onReadyToStart;
|
masterClockContainer.Reset();
|
||||||
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -167,7 +166,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
||||||
{
|
{
|
||||||
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.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();
|
.FirstOrDefault();
|
||||||
|
|
||||||
foreach (var instance in instances)
|
foreach (var instance in instances)
|
||||||
@ -175,10 +174,10 @@ 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;
|
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;
|
||||||
|
|
||||||
private void onReadyToStart()
|
private void performInitialSeek()
|
||||||
{
|
{
|
||||||
// Seek the master clock to the gameplay time.
|
// Seek the master clock to the gameplay time.
|
||||||
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
|
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
|
||||||
@ -188,28 +187,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
.DefaultIfEmpty(0)
|
.DefaultIfEmpty(0)
|
||||||
.Min();
|
.Min();
|
||||||
|
|
||||||
masterClockContainer.Reset(startTime, true);
|
masterClockContainer.StartTime = startTime;
|
||||||
|
masterClockContainer.Reset(true);
|
||||||
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
|
|
||||||
canStartMasterClock = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
|
|
||||||
{
|
|
||||||
Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}");
|
|
||||||
|
|
||||||
switch (state.NewValue)
|
|
||||||
{
|
|
||||||
case MasterClockState.Synchronised:
|
|
||||||
if (canStartMasterClock)
|
|
||||||
masterClockContainer.Start();
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MasterClockState.TooFarAhead:
|
|
||||||
masterClockContainer.Stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||||
@ -251,7 +230,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
|
|
||||||
return base.OnBackButton();
|
return base.OnBackButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
public readonly int UserId;
|
public readonly int UserId;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public readonly ISpectatorPlayerClock GameplayClock;
|
public readonly SpectatorPlayerClock GameplayClock;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently-loaded score.
|
/// The currently-loaded score.
|
||||||
@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
private readonly LoadingLayer loadingLayer;
|
private readonly LoadingLayer loadingLayer;
|
||||||
private OsuScreenStack? stack;
|
private OsuScreenStack? stack;
|
||||||
|
|
||||||
public PlayerArea(int userId, ISpectatorPlayerClock clock)
|
public PlayerArea(int userId, SpectatorPlayerClock clock)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
GameplayClock = clock;
|
GameplayClock = clock;
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
// 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.Timing;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A clock which catches up using rate adjustment.
|
||||||
|
/// </summary>
|
||||||
|
public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The catch up rate.
|
||||||
|
/// </summary>
|
||||||
|
private const double catchup_rate = 2;
|
||||||
|
|
||||||
|
private readonly GameplayClockContainer masterClock;
|
||||||
|
|
||||||
|
public double CurrentTime { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this clock is waiting on frames to continue playback.
|
||||||
|
/// </summary>
|
||||||
|
public bool WaitingOnFrames { get; set; } = true;
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
this.masterClock = masterClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset() => CurrentTime = 0;
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
// Our running state should only be managed by SpectatorSyncManager via IsRunning.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
// Our running state should only be managed by an SpectatorSyncManager via IsRunning.
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Seek(double position)
|
||||||
|
{
|
||||||
|
CurrentTime = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSpeedAdjustments()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Rate
|
||||||
|
{
|
||||||
|
get => IsCatchingUp ? catchup_rate : 1;
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessFrame()
|
||||||
|
{
|
||||||
|
if (IsRunning)
|
||||||
|
{
|
||||||
|
double elapsedSource = masterClock.ElapsedFrameTime;
|
||||||
|
double elapsed = elapsedSource * Rate;
|
||||||
|
|
||||||
|
CurrentTime += elapsed;
|
||||||
|
ElapsedFrameTime = elapsed;
|
||||||
|
FramesPerSecond = masterClock.FramesPerSecond;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ElapsedFrameTime = 0;
|
||||||
|
FramesPerSecond = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ElapsedFrameTime { get; private set; }
|
||||||
|
|
||||||
|
public double FramesPerSecond { get; private set; }
|
||||||
|
|
||||||
|
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||||
|
}
|
||||||
|
}
|
@ -4,16 +4,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public class CatchUpSyncManager : Component, ISyncManager
|
public class SpectatorSyncManager : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
|
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
|
||||||
@ -30,41 +30,53 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MAXIMUM_START_DELAY = 15000;
|
public const double MAXIMUM_START_DELAY = 15000;
|
||||||
|
|
||||||
public event Action? ReadyToStart;
|
/// <summary>
|
||||||
|
/// An event which is invoked when gameplay is ready to start.
|
||||||
|
/// </summary>
|
||||||
|
public Action? ReadyToStart;
|
||||||
|
|
||||||
|
public double CurrentMasterTime => masterClock.CurrentTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The master clock which is used to control the timing of all player clocks clocks.
|
/// The master clock which is used to control the timing of all player clocks clocks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GameplayClockContainer MasterClock { get; }
|
private readonly GameplayClockContainer masterClock;
|
||||||
|
|
||||||
public IBindable<MasterClockState> MasterState => masterState;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The player clocks.
|
/// The player clocks.
|
||||||
/// </summary>
|
/// </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 MasterClockState masterState = MasterClockState.Synchronised;
|
||||||
|
|
||||||
private bool hasStarted;
|
private bool hasStarted;
|
||||||
|
|
||||||
private double? firstStartAttemptTime;
|
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);
|
playerClocks.Add(clock);
|
||||||
return 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);
|
playerClocks.Remove(clock);
|
||||||
clock.Stop();
|
clock.IsRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -75,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
{
|
{
|
||||||
// Ensure all player clocks are stopped until the start succeeds.
|
// Ensure all player clocks are stopped until the start succeeds.
|
||||||
foreach (var clock in playerClocks)
|
foreach (var clock in playerClocks)
|
||||||
clock.Stop();
|
clock.IsRunning = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +107,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
if (playerClocks.Count == 0)
|
if (playerClocks.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
|
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames);
|
||||||
|
|
||||||
if (readyCount == playerClocks.Count)
|
if (readyCount == playerClocks.Count)
|
||||||
return performStart();
|
return performStart();
|
||||||
@ -128,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
|
|
||||||
// How far this player's clock is out of sync, compared to the master clock.
|
// 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).
|
// 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.
|
// 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.
|
// 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 +149,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
|
// 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).
|
// when it is required to be running (ie. if all players are ahead of the master).
|
||||||
clock.IsCatchingUp = false;
|
clock.IsCatchingUp = false;
|
||||||
clock.Stop();
|
clock.IsRunning = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the player clock is running if it can.
|
// Make sure the player clock is running if it can.
|
||||||
if (!clock.WaitingOnFrames.Value)
|
clock.IsRunning = !clock.WaitingOnFrames;
|
||||||
clock.Start();
|
|
||||||
else
|
|
||||||
clock.Stop();
|
|
||||||
|
|
||||||
if (clock.IsCatchingUp)
|
if (clock.IsCatchingUp)
|
||||||
{
|
{
|
||||||
@ -167,8 +176,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void updateMasterState()
|
private void updateMasterState()
|
||||||
{
|
{
|
||||||
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
|
MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
|
||||||
masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
|
|
||||||
|
if (masterState == newState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
masterState = newState;
|
||||||
|
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
|
||||||
|
|
||||||
|
switch (masterState)
|
||||||
|
{
|
||||||
|
case MasterClockState.Synchronised:
|
||||||
|
if (hasStarted)
|
||||||
|
masterClock.Start();
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MasterClockState.TooFarAhead:
|
||||||
|
masterClock.Stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user