1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 17:07:38 +08:00

Merge pull request #19911 from smoogipoo/multi-spectator-clock-cleanup

Invert creation of clocks in multi-spectator
This commit is contained in:
Dean Herbert 2022-08-23 14:26:06 +09:00 committed by GitHub
commit 51e607e834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 75 additions and 120 deletions

View File

@ -4,11 +4,13 @@
#nullable disable #nullable disable
using System; using System;
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.OnlinePlay namespace osu.Game.Tests.OnlinePlay
@ -16,20 +18,34 @@ namespace osu.Game.Tests.OnlinePlay
[HeadlessTest] [HeadlessTest]
public class TestSceneCatchUpSyncManager : OsuTestScene public class TestSceneCatchUpSyncManager : OsuTestScene
{ {
private TestManualClock master; private GameplayClockContainer master;
private CatchUpSyncManager syncManager; private CatchUpSyncManager syncManager;
private TestSpectatorPlayerClock player1; private Dictionary<ISpectatorPlayerClock, int> clocksById;
private TestSpectatorPlayerClock player2; private ISpectatorPlayerClock player1;
private ISpectatorPlayerClock player2;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
syncManager = new CatchUpSyncManager(master = new TestManualClock()); syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock()));
syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1)); player1 = syncManager.CreateManagedClock();
syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2)); player2 = syncManager.CreateManagedClock();
Schedule(() => Child = syncManager); clocksById = new Dictionary<ISpectatorPlayerClock, int>
{
{ player1, 1 },
{ player2, 2 }
};
Schedule(() =>
{
Children = new Drawable[]
{
syncManager,
master
};
});
} }
[Test] [Test]
@ -129,8 +145,8 @@ namespace osu.Game.Tests.OnlinePlay
assertPlayerClockState(() => player1, false); assertPlayerClockState(() => player1, false);
} }
private void setWaiting(Func<TestSpectatorPlayerClock> playerClock, bool waiting) private void setWaiting(Func<ISpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = 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}", () =>
{ {
@ -144,51 +160,14 @@ namespace osu.Game.Tests.OnlinePlay
/// <summary> /// <summary>
/// clock.Time = master.Time - offsetFromMaster /// clock.Time = master.Time - offsetFromMaster
/// </summary> /// </summary>
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster) private void setPlayerClockTime(Func<ISpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) => private void assertCatchingUp(Func<ISpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(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<TestSpectatorPlayerClock> playerClock, bool running) private void assertPlayerClockState(Func<ISpectatorPlayerClock> playerClock, bool running)
=> AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock
{
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public bool IsCatchingUp { get; set; }
public IFrameBasedClock Source
{
set => throw new NotImplementedException();
}
public readonly int Id;
public TestSpectatorPlayerClock(int id)
{
Id = id;
WaitingOnFrames.BindValueChanged(waiting =>
{
if (waiting.NewValue)
Stop();
else
Start();
});
}
public void ProcessFrame()
{
}
public double ElapsedFrameTime => 0;
public double FramesPerSecond => 0;
public FrameTimeInfo TimeInfo => default;
}
private class TestManualClock : ManualClock, IAdjustableClock private class TestManualClock : ManualClock, IAdjustableClock
{ {

View File

@ -17,15 +17,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary> /// </summary>
public const double CATCHUP_RATE = 2; public const double CATCHUP_RATE = 2;
/// <summary> public readonly IFrameBasedClock Source;
/// The source clock.
/// </summary>
public IFrameBasedClock? Source { get; set; }
public double CurrentTime { get; private set; } public double CurrentTime { get; private set; }
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public CatchUpSpectatorPlayerClock(IFrameBasedClock source)
{
Source = source;
}
public void Reset() => CurrentTime = 0; public void Reset() => CurrentTime = 0;
public void Start() => IsRunning = true; public void Start() => IsRunning = true;
@ -67,9 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
ElapsedFrameTime = 0; ElapsedFrameTime = 0;
FramesPerSecond = 0; FramesPerSecond = 0;
if (Source == null)
return;
Source.ProcessFrame(); Source.ProcessFrame();
if (IsRunning) if (IsRunning)

View File

@ -1,15 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
@ -33,12 +30,12 @@ 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; public event Action? ReadyToStart;
/// <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 IAdjustableClock MasterClock { get; } public GameplayClockContainer MasterClock { get; }
public IBindable<MasterClockState> MasterState => masterState; public IBindable<MasterClockState> MasterState => masterState;
@ -52,18 +49,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private bool hasStarted; private bool hasStarted;
private double? firstStartAttemptTime; private double? firstStartAttemptTime;
public CatchUpSyncManager(IAdjustableClock master) public CatchUpSyncManager(GameplayClockContainer master)
{ {
MasterClock = master; MasterClock = master;
} }
public void AddPlayerClock(ISpectatorPlayerClock clock) public ISpectatorPlayerClock CreateManagedClock()
{ {
Debug.Assert(!playerClocks.Contains(clock)); var clock = new CatchUpSpectatorPlayerClock(MasterClock);
playerClocks.Add(clock); playerClocks.Add(clock);
return clock;
} }
public void RemovePlayerClock(ISpectatorPlayerClock clock) public void RemoveManagedClock(ISpectatorPlayerClock clock)
{ {
playerClocks.Remove(clock); playerClocks.Remove(clock);
clock.Stop(); clock.Stop();

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -35,10 +33,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// Of note, this will be false if this clock is *ahead* of the master clock. /// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks> /// </remarks>
bool IsCatchingUp { get; set; } bool IsCatchingUp { get; set; }
/// <summary>
/// The source clock
/// </summary>
IFrameBasedClock Source { set; }
} }
} }

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
@ -17,12 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary> /// <summary>
/// An event which is invoked when gameplay is ready to start. /// An event which is invoked when gameplay is ready to start.
/// </summary> /// </summary>
event Action ReadyToStart; event Action? ReadyToStart;
/// <summary> /// <summary>
/// The master clock which player clocks should synchronise to. /// The master clock which player clocks should synchronise to.
/// </summary> /// </summary>
IAdjustableClock MasterClock { get; } GameplayClockContainer MasterClock { get; }
/// <summary> /// <summary>
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed. /// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
@ -30,15 +28,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
IBindable<MasterClockState> MasterState { get; } IBindable<MasterClockState> MasterState { get; }
/// <summary> /// <summary>
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage. /// Create a new managed <see cref="ISpectatorPlayerClock"/>.
/// </summary> /// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to add.</param> /// <returns>The newly created <see cref="ISpectatorPlayerClock"/>.</returns>
void AddPlayerClock(ISpectatorPlayerClock clock); ISpectatorPlayerClock CreateManagedClock();
/// <summary> /// <summary>
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>. /// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
/// </summary> /// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param> /// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
void RemovePlayerClock(ISpectatorPlayerClock clock); void RemoveManagedClock(ISpectatorPlayerClock clock);
} }
} }

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
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.Game.Beatmaps; using osu.Game.Beatmaps;
@ -42,17 +40,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; } = null!;
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } private MultiplayerClient multiplayerClient { get; set; } = null!;
private readonly PlayerArea[] instances; private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer; private MasterGameplayClockContainer masterClockContainer = null!;
private ISyncManager syncManager; private ISyncManager syncManager = null!;
private PlayerGrid grid; private PlayerGrid grid = null!;
private MultiSpectatorLeaderboard leaderboard; private MultiSpectatorLeaderboard leaderboard = null!;
private PlayerArea currentAudioSource; private PlayerArea? currentAudioSource;
private bool canStartMasterClock; private bool canStartMasterClock;
private readonly Room room; private readonly Room room;
@ -125,10 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}; };
for (int i = 0; i < Users.Count; i++) for (int i = 0; i < Users.Count; i++)
{ grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer));
syncManager.AddPlayerClock(instances[i].GameplayClock);
}
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
{ {
@ -181,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
} }
} }
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock) private bool isCandidateAudioSource(ISpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
private void onReadyToStart() private void onReadyToStart()
@ -189,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// 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.
double startTime = instances.Where(i => i.Score != null) double startTime = instances.Where(i => i.Score != null)
.SelectMany(i => i.Score.Replay.Frames) .SelectMany(i => i.Score.AsNonNull().Replay.Frames)
.Select(f => f.Time) .Select(f => f.Time)
.DefaultIfEmpty(0) .DefaultIfEmpty(0)
.Min(); .Min();
@ -242,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var instance = instances.Single(i => i.UserId == userId); var instance = instances.Single(i => i.UserId == userId);
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
syncManager.RemovePlayerClock(instance.GameplayClock); syncManager.RemoveManagedClock(instance.GameplayClock);
} }
public override bool OnBackButton() public override bool OnBackButton()

View File

@ -1,17 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -29,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary> /// <summary>
/// Raised after <see cref="Player.StartGameplay"/> is called on <see cref="Player"/>. /// Raised after <see cref="Player.StartGameplay"/> is called on <see cref="Player"/>.
/// </summary> /// </summary>
public event Action OnGameplayStarted; public event Action? OnGameplayStarted;
/// <summary> /// <summary>
/// Whether a <see cref="Player"/> is loaded in the area. /// Whether a <see cref="Player"/> is loaded in the area.
@ -44,23 +40,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary> /// <summary>
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>. /// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
/// </summary> /// </summary>
[NotNull] public readonly ISpectatorPlayerClock GameplayClock;
public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
/// <summary> /// <summary>
/// The currently-loaded score. /// The currently-loaded score.
/// </summary> /// </summary>
[CanBeNull] public Score? Score { get; private set; }
public Score Score { get; private set; }
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly BindableDouble volumeAdjustment = new BindableDouble(); private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent; private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer; private readonly LoadingLayer loadingLayer;
private OsuScreenStack stack; private OsuScreenStack? stack;
public PlayerArea(int userId, IFrameBasedClock masterClock) public PlayerArea(int userId, ISpectatorPlayerClock clock)
{ {
UserId = userId; UserId = userId;
GameplayClock = clock;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Masking = true; Masking = true;
@ -77,14 +75,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}; };
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
GameplayClock.Source = masterClock;
} }
[Resolved] public void LoadScore(Score score)
private IBindable<WorkingBeatmap> beatmap { get; set; }
public void LoadScore([NotNull] Score score)
{ {
if (Score != null) if (Score != null)
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score."); throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");