mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 08:02:55 +08:00
Add initial multiplayer screen implementation
This commit is contained in:
parent
d64b236f86
commit
709016f0d6
@ -0,0 +1,146 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Spectate;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSpectator : SpectatorScreen
|
||||
{
|
||||
private const double min_duration_to_allow_playback = 50;
|
||||
private const double max_sync_offset = 2;
|
||||
|
||||
// Isolates beatmap/ruleset to this screen.
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
|
||||
|
||||
[Resolved]
|
||||
private SpectatorStreamingClient spectatorClient { get; set; }
|
||||
|
||||
private readonly int[] userIds;
|
||||
private readonly PlayerInstance[] instances;
|
||||
|
||||
private PlayerGrid grid;
|
||||
|
||||
public MultiplayerSpectator(PlaylistItem playlistItem, int[] userIds)
|
||||
: this(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
private MultiplayerSpectator(int[] userIds)
|
||||
: base(userIds)
|
||||
{
|
||||
this.userIds = userIds;
|
||||
instances = new PlayerInstance[userIds.Length];
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = grid = new PlayerGrid
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updatePlayTime();
|
||||
}
|
||||
|
||||
private bool gameplayStarted;
|
||||
|
||||
private void updatePlayTime()
|
||||
{
|
||||
if (gameplayStarted)
|
||||
{
|
||||
ensurePlaying(instances.Select(i => i.Beatmap.Track.CurrentTime).Max());
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure all players are loaded.
|
||||
if (!AllPlayersLoaded)
|
||||
{
|
||||
ensureAllStopped();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!instances.All(i => i.Score.Replay.Frames.Count > 0))
|
||||
{
|
||||
ensureAllStopped();
|
||||
return;
|
||||
}
|
||||
|
||||
gameplayStarted = true;
|
||||
}
|
||||
|
||||
private void ensureAllStopped()
|
||||
{
|
||||
foreach (var inst in instances)
|
||||
inst.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.Stop();
|
||||
}
|
||||
|
||||
private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(2.0);
|
||||
|
||||
private void ensurePlaying(double targetTime)
|
||||
{
|
||||
foreach (var inst in instances)
|
||||
{
|
||||
double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last();
|
||||
double currentTime = inst.Beatmap.Track.CurrentTime;
|
||||
|
||||
// If we have enough frames to play back, start playback.
|
||||
if (Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback))
|
||||
{
|
||||
inst.ChildrenOfType<GameplayClockContainer>().Single().Start();
|
||||
|
||||
if (targetTime < lastFrameTime && targetTime > currentTime + 16)
|
||||
inst.Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
|
||||
else
|
||||
inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
|
||||
}
|
||||
else
|
||||
inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
||||
{
|
||||
int userIndex = getIndexForUser(userId);
|
||||
var existingInstance = instances[userIndex];
|
||||
|
||||
if (existingInstance != null)
|
||||
grid.Remove(existingInstance);
|
||||
|
||||
LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score), d =>
|
||||
{
|
||||
if (instances[userIndex] == d)
|
||||
grid.Add(d);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void EndGameplay(int userId)
|
||||
{
|
||||
spectatorClient.StopWatchingUser(userId);
|
||||
}
|
||||
|
||||
private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// 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.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSpectatorPlayer : SpectatorPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
public MultiplayerSpectatorPlayer(Score score)
|
||||
: base(score)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// 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.Game.Scoring;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiplayerSpectatorPlayerLoader : SpectatorPlayerLoader
|
||||
{
|
||||
public MultiplayerSpectatorPlayerLoader(Score score, Func<MultiplayerSpectatorPlayer> createPlayer)
|
||||
: base(score, createPlayer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LogoExiting(OsuLogo logo)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -75,6 +75,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
cellContainer.Add(cell);
|
||||
}
|
||||
|
||||
public void Remove(Drawable content)
|
||||
{
|
||||
var cell = cellContainer.FirstOrDefault(c => c.Content == content);
|
||||
if (cell == null)
|
||||
return;
|
||||
|
||||
if (cell.IsMaximised)
|
||||
toggleMaximisationState(cell);
|
||||
|
||||
cellContainer.Remove(cell);
|
||||
facadeContainer.Remove(facadeContainer[cell.FacadeIndex]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The content added to this grid.
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,55 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class PlayerInstance : CompositeDrawable
|
||||
{
|
||||
public bool PlayerLoaded => stack?.CurrentScreen is Player;
|
||||
|
||||
public User User => Score.ScoreInfo.User;
|
||||
public ScoreProcessor ScoreProcessor => player?.ScoreProcessor;
|
||||
|
||||
public WorkingBeatmap Beatmap { get; private set; }
|
||||
public Ruleset Ruleset { get; private set; }
|
||||
|
||||
public readonly Score Score;
|
||||
|
||||
private OsuScreenStack stack;
|
||||
private MultiplayerSpectatorPlayer player;
|
||||
|
||||
public PlayerInstance(Score score)
|
||||
{
|
||||
Score = score;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapManager beatmapManager)
|
||||
{
|
||||
Beatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap, bypassCache: true);
|
||||
Ruleset = Score.ScoreInfo.Ruleset.CreateInstance();
|
||||
|
||||
InternalChild = new GameplayIsolationContainer(Beatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new DrawSizePreservingFillContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = stack = new OsuScreenStack()
|
||||
}
|
||||
};
|
||||
|
||||
stack.Push(new MultiplayerSpectatorPlayerLoader(Score, () => player = new MultiplayerSpectatorPlayer(Score)));
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,12 @@ namespace osu.Game.Screens.Play
|
||||
public readonly ScoreInfo Score;
|
||||
|
||||
public SpectatorPlayerLoader(Score score)
|
||||
: base(() => new SpectatorPlayer(score))
|
||||
: this(score, () => new SpectatorPlayer(score))
|
||||
{
|
||||
}
|
||||
|
||||
public SpectatorPlayerLoader(Score score, Func<Player> createPlayer)
|
||||
: base(createPlayer)
|
||||
{
|
||||
if (score.Replay == null)
|
||||
throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score));
|
||||
|
Loading…
Reference in New Issue
Block a user