mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 17:52:56 +08:00
Reimplement multiplayer syncing using new master/slave clocks
This commit is contained in:
parent
fe3ba2b80e
commit
1705d472b5
@ -1,15 +1,12 @@
|
||||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@ -156,27 +153,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
checkPausedInstant(55, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerStartsCatchingUpOnlyAfterExceedingMaxOffset()
|
||||
{
|
||||
start(new[] { 55, 56 });
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(55, 1000);
|
||||
sendFrames(56, 1000);
|
||||
|
||||
Bindable<double> slowDownAdjustment;
|
||||
|
||||
AddStep("slow down player 2", () =>
|
||||
{
|
||||
slowDownAdjustment = new Bindable<double>(0.99);
|
||||
getInstance(56).Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, slowDownAdjustment);
|
||||
});
|
||||
|
||||
AddUntilStep("exceeded min offset but not catching up", () => getGameplayOffset(55, 56) > PlayerInstance.MAX_OFFSET && !getInstance(56).IsCatchingUp);
|
||||
AddUntilStep("catching up or caught up", () => getInstance(56).IsCatchingUp || Math.Abs(getGameplayOffset(55, 56)) < PlayerInstance.SYNC_TARGET * 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayersCatchUpAfterFallingBehind()
|
||||
{
|
||||
@ -184,7 +160,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
loadSpectateScreen();
|
||||
|
||||
// Send initial frames for both players. A few more for player 1.
|
||||
sendFrames(55, 100);
|
||||
sendFrames(55, 1000);
|
||||
sendFrames(56, 10);
|
||||
checkPausedInstant(55, false);
|
||||
checkPausedInstant(56, false);
|
||||
@ -194,11 +170,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddWaitStep("wait a few more frames", 10);
|
||||
|
||||
// Send more frames for player 2. It should unpause.
|
||||
sendFrames(56, 100);
|
||||
sendFrames(56, 1000);
|
||||
checkPausedInstant(56, false);
|
||||
|
||||
// Player 2 should catch up to player 1 after unpausing.
|
||||
AddUntilStep("player 2 not catching up", () => !getInstance(56).IsCatchingUp);
|
||||
AddUntilStep("player 2 not catching up", () => !getInstance(56).GameplayClock.IsCatchingUp);
|
||||
AddWaitStep("wait a bit", 10);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,78 @@
|
||||
// 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.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSlaveClock : IFrameBasedClock, IMultiplayerSlaveClock
|
||||
{
|
||||
/// <summary>
|
||||
/// The catchup rate.
|
||||
/// </summary>
|
||||
public const double CATCHUP_RATE = 2;
|
||||
|
||||
private readonly IFrameBasedClock masterClock;
|
||||
|
||||
public MultiplayerSlaveClock(IFrameBasedClock masterClock)
|
||||
{
|
||||
this.masterClock = masterClock;
|
||||
}
|
||||
|
||||
public double CurrentTime { get; private set; }
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public void Reset() => CurrentTime = 0;
|
||||
|
||||
public void Start() => IsRunning = true;
|
||||
|
||||
public void Stop() => IsRunning = false;
|
||||
|
||||
public bool Seek(double position) => true;
|
||||
|
||||
public void ResetSpeedAdjustments()
|
||||
{
|
||||
}
|
||||
|
||||
public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
|
||||
|
||||
double IAdjustableClock.Rate
|
||||
{
|
||||
get => Rate;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
double IClock.Rate => Rate;
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
masterClock.ProcessFrame();
|
||||
|
||||
ElapsedFrameTime = 0;
|
||||
FramesPerSecond = 0;
|
||||
|
||||
if (IsRunning)
|
||||
{
|
||||
double elapsedSource = masterClock.ElapsedFrameTime;
|
||||
double elapsed = elapsedSource * Rate;
|
||||
|
||||
CurrentTime += elapsed;
|
||||
ElapsedFrameTime = elapsed;
|
||||
FramesPerSecond = masterClock.FramesPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime { get; private set; }
|
||||
|
||||
public double FramesPerSecond { get; private set; }
|
||||
|
||||
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||
|
||||
public IBindable<bool> WaitingOnFrames { get; } = new Bindable<bool>();
|
||||
|
||||
public bool IsCatchingUp { get; set; }
|
||||
}
|
||||
}
|
@ -2,14 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Spectate;
|
||||
@ -18,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSpectator : SpectatorScreen
|
||||
{
|
||||
private const double min_duration_to_allow_playback = 50;
|
||||
private const double maximum_start_delay = 15000;
|
||||
|
||||
// Isolates beatmap/ruleset to this screen.
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
@ -30,10 +23,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private SpectatorStreamingClient spectatorClient { get; set; }
|
||||
|
||||
private readonly PlayerInstance[] instances;
|
||||
private GameplayClockContainer gameplayClockContainer;
|
||||
private MasterGameplayClockContainer masterClockContainer;
|
||||
private IMultiplayerSyncManager syncManager;
|
||||
private PlayerGrid grid;
|
||||
private MultiplayerSpectatorLeaderboard leaderboard;
|
||||
private double? loadFinishTime;
|
||||
|
||||
public MultiplayerSpectator(int[] userIds)
|
||||
: base(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray())
|
||||
@ -46,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
Container leaderboardContainer;
|
||||
|
||||
InternalChild = gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
|
||||
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
|
||||
{
|
||||
Child = new GridContainer
|
||||
{
|
||||
@ -70,6 +63,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
};
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
(Drawable)(syncManager = new MultiplayerSyncManager(masterClockContainer)),
|
||||
masterClockContainer
|
||||
};
|
||||
|
||||
// Todo: This is not quite correct - it should be per-user to adjust for other mod combinations.
|
||||
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
|
||||
@ -78,65 +77,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, UserIds) { Expanded = { Value = true } }, leaderboardContainer.Add);
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
base.LoadComplete();
|
||||
|
||||
if (AllPlayersLoaded)
|
||||
loadFinishTime ??= Time.Current;
|
||||
|
||||
updateGameplayPlayingState();
|
||||
}
|
||||
|
||||
private bool canStartGameplay =>
|
||||
// All players must be loaded, and...
|
||||
AllPlayersLoaded
|
||||
&& (
|
||||
// All players have frames...
|
||||
instances.All(i => i.Score.Replay.Frames.Count > 0)
|
||||
// Or any player has frames and the maximum start delay has been exceeded.
|
||||
|| (Time.Current - loadFinishTime > maximum_start_delay
|
||||
&& instances.Any(i => i.Score.Replay.Frames.Count > 0))
|
||||
);
|
||||
|
||||
private bool firstStartFrame = true;
|
||||
|
||||
private void updateGameplayPlayingState()
|
||||
{
|
||||
// Make sure all players are loaded and have frames before starting any.
|
||||
if (!canStartGameplay)
|
||||
{
|
||||
foreach (var inst in instances)
|
||||
inst?.PauseGameplay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstStartFrame)
|
||||
gameplayClockContainer.Restart();
|
||||
|
||||
// Not all instances may be in a valid gameplay state (see canStartGameplay). Only control the ones that are.
|
||||
IEnumerable<PlayerInstance> validInstances = instances.Where(i => i.Score.Replay.Frames.Count > 0);
|
||||
|
||||
double targetGameplayTime = gameplayClockContainer.GameplayClock.CurrentTime;
|
||||
|
||||
var instanceTimes = string.Join(',', validInstances.Select(i => $" {i.User.Id}: {(int)i.GetCurrentGameplayTime()}"));
|
||||
Logger.Log($"target: {(int)targetGameplayTime},{instanceTimes}");
|
||||
|
||||
foreach (var inst in validInstances)
|
||||
{
|
||||
Debug.Assert(inst != null);
|
||||
|
||||
double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last();
|
||||
double currentTime = inst.GetCurrentGameplayTime();
|
||||
|
||||
bool canContinuePlayback = Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback);
|
||||
if (!canContinuePlayback)
|
||||
continue;
|
||||
|
||||
inst.ContinueGameplay(targetGameplayTime);
|
||||
}
|
||||
|
||||
firstStartFrame = false;
|
||||
masterClockContainer.Stop();
|
||||
masterClockContainer.Restart();
|
||||
}
|
||||
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
@ -151,15 +97,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
if (existingInstance != null)
|
||||
{
|
||||
grid.Remove(existingInstance);
|
||||
syncManager.RemoveSlave(existingInstance.GameplayClock);
|
||||
leaderboard.RemoveClock(existingInstance.User.Id);
|
||||
}
|
||||
|
||||
LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, gameplayClockContainer.GameplayClock), d =>
|
||||
LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score, new MultiplayerSlaveClock(masterClockContainer.GameplayClock)), d =>
|
||||
{
|
||||
if (instances[userIndex] == d)
|
||||
{
|
||||
grid.Add(d);
|
||||
leaderboard.AddClock(d.User.Id, d.Beatmap.Track);
|
||||
syncManager.AddSlave(d.GameplayClock);
|
||||
leaderboard.AddClock(d.User.Id, d.GameplayClock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
@ -12,13 +11,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSpectatorPlayer : SpectatorPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
private readonly MultiplayerSlaveClock gameplayClock;
|
||||
|
||||
public new SubGameplayClockContainer GameplayClockContainer => (SubGameplayClockContainer)base.GameplayClockContainer;
|
||||
|
||||
private readonly GameplayClock gameplayClock;
|
||||
|
||||
public MultiplayerSpectatorPlayer(Score score, GameplayClock gameplayClock)
|
||||
public MultiplayerSpectatorPlayer(Score score, MultiplayerSlaveClock gameplayClock)
|
||||
: base(score)
|
||||
{
|
||||
this.gameplayClock = gameplayClock;
|
||||
|
@ -26,11 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public const double MAXIMUM_START_DELAY = 15000;
|
||||
|
||||
/// <summary>
|
||||
/// The catchup rate.
|
||||
/// </summary>
|
||||
public const double CATCHUP_RATE = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which is used to control the timing of all slave clocks.
|
||||
/// </summary>
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -14,21 +13,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class PlayerInstance : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The rate at which a user catches up after becoming desynchronised.
|
||||
/// </summary>
|
||||
private const double catchup_rate = 2;
|
||||
|
||||
/// <summary>
|
||||
/// The offset from the expected time at which to START synchronisation.
|
||||
/// </summary>
|
||||
public const double MAX_OFFSET = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum offset from the expected time at which to STOP synchronisation.
|
||||
/// </summary>
|
||||
public const double SYNC_TARGET = 16;
|
||||
|
||||
public bool PlayerLoaded => stack?.CurrentScreen is Player;
|
||||
|
||||
public User User => Score.ScoreInfo.User;
|
||||
@ -36,17 +20,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public WorkingBeatmap Beatmap { get; private set; }
|
||||
|
||||
public readonly Score Score;
|
||||
private readonly GameplayClock gameplayClock;
|
||||
|
||||
public bool IsCatchingUp { get; private set; }
|
||||
public readonly MultiplayerSlaveClock GameplayClock;
|
||||
|
||||
private OsuScreenStack stack;
|
||||
private MultiplayerSpectatorPlayer player;
|
||||
|
||||
public PlayerInstance(Score score, GameplayClock gameplayClock)
|
||||
public PlayerInstance(Score score, MultiplayerSlaveClock gameplayClock)
|
||||
{
|
||||
Score = score;
|
||||
this.gameplayClock = gameplayClock;
|
||||
GameplayClock = gameplayClock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -67,83 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
};
|
||||
|
||||
stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score, gameplayClock)));
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
updateCatchup();
|
||||
}
|
||||
|
||||
private double targetGameplayTime;
|
||||
|
||||
private void updateCatchup()
|
||||
{
|
||||
if (player?.IsLoaded != true)
|
||||
return;
|
||||
|
||||
if (Score.Replay.Frames.Count == 0)
|
||||
return;
|
||||
|
||||
if (player.GameplayClockContainer.IsPaused.Value)
|
||||
return;
|
||||
|
||||
double currentTime = Beatmap.Track.CurrentTime;
|
||||
double timeBehind = targetGameplayTime - currentTime;
|
||||
|
||||
double offsetForCatchup = IsCatchingUp ? SYNC_TARGET : MAX_OFFSET;
|
||||
bool catchupRequired = timeBehind > offsetForCatchup;
|
||||
|
||||
// Skip catchup if no work needs to be done.
|
||||
if (catchupRequired == IsCatchingUp)
|
||||
return;
|
||||
|
||||
if (catchupRequired)
|
||||
{
|
||||
// player.GameplayClockContainer.AdjustableClock.Rate = catchup_rate;
|
||||
Logger.Log($"{User.Id} catchup started (behind: {(int)timeBehind})");
|
||||
}
|
||||
else
|
||||
{
|
||||
// player.GameplayClockContainer.AdjustableClock.Rate = 1;
|
||||
Logger.Log($"{User.Id} catchup finished (behind: {(int)timeBehind})");
|
||||
}
|
||||
|
||||
IsCatchingUp = catchupRequired;
|
||||
}
|
||||
|
||||
public double GetCurrentGameplayTime()
|
||||
{
|
||||
if (player?.IsLoaded != true)
|
||||
return 0;
|
||||
|
||||
return player.GameplayClockContainer.GameplayClock.CurrentTime;
|
||||
}
|
||||
|
||||
public bool IsPlaying()
|
||||
{
|
||||
if (player.IsLoaded != true)
|
||||
return false;
|
||||
|
||||
return player.GameplayClockContainer.GameplayClock.IsRunning;
|
||||
}
|
||||
|
||||
public void ContinueGameplay(double targetGameplayTime)
|
||||
{
|
||||
if (player?.IsLoaded != true)
|
||||
return;
|
||||
|
||||
player.GameplayClockContainer.Start();
|
||||
this.targetGameplayTime = targetGameplayTime;
|
||||
}
|
||||
|
||||
public void PauseGameplay()
|
||||
{
|
||||
if (player?.IsLoaded != true)
|
||||
return;
|
||||
|
||||
player.GameplayClockContainer.Stop();
|
||||
stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => new MultiplayerSpectatorPlayer(Score, GameplayClock)));
|
||||
}
|
||||
|
||||
// Player interferes with global input, so disable input for now.
|
||||
|
Loading…
Reference in New Issue
Block a user