Merge pull request #12321 from smoogipoo/add-spectate-button-and-state

Add multiplayer spectating user state and button
This commit is contained in:
Dean Herbert
2021-04-08 19:48:58 +09:00
committed by GitHub
14 changed files with 500 additions and 10 deletions

View File

@ -96,6 +96,9 @@ namespace osu.Game.Online.Multiplayer
if (!IsConnected.Value)
return Task.CompletedTask;
if (newState == MultiplayerUserState.Spectating)
return Task.CompletedTask; // Not supported yet.
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
}

View File

@ -55,5 +55,10 @@ namespace osu.Game.Online.Multiplayer
/// The user is currently viewing results. This is a reserved state, and is set by the server.
/// </summary>
Results,
/// <summary>
/// The user is currently spectating this room.
/// </summary>
Spectating
}
}

View File

@ -249,6 +249,33 @@ namespace osu.Game.Online.Multiplayer
}
}
/// <summary>
/// Toggles the <see cref="LocalUser"/>'s spectating state.
/// </summary>
/// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
public async Task ToggleSpectate()
{
var localUser = LocalUser;
if (localUser == null)
return;
switch (localUser.State)
{
case MultiplayerUserState.Idle:
case MultiplayerUserState.Ready:
await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);
return;
case MultiplayerUserState.Spectating:
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
return;
default:
throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}");
}
}
public abstract Task TransferHost(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);

View File

@ -8,21 +8,28 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerMatchFooter : CompositeDrawable
{
public const float HEIGHT = 50;
private const float ready_button_width = 600;
private const float spectate_button_width = 200;
public Action OnReadyClick
{
set => readyButton.OnReadyClick = value;
}
public Action OnSpectateClick
{
set => spectateButton.OnSpectateClick = value;
}
private readonly Drawable background;
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerSpectateButton spectateButton;
public MultiplayerMatchFooter()
{
@ -32,11 +39,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
readyButton = new MultiplayerReadyButton
new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(600, 50),
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
null,
spectateButton = new MultiplayerSpectateButton
{
RelativeSizeAxes = Axes.Both,
},
null,
readyButton = new MultiplayerReadyButton
{
RelativeSizeAxes = Axes.Both,
},
null
}
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(maxSize: spectate_button_width),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(maxSize: ready_button_width),
new Dimension()
}
}
};
}

View File

@ -78,8 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(Room != null);
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countText = $"({newCountReady} / {Room.Users.Count} ready)";
string countText = $"({newCountReady} / {newCountTotal} ready)";
switch (localUser.State)
{
@ -88,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
updateButtonColour(true);
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (Room?.Host?.Equals(localUser) == true)
{
@ -103,7 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser.State == MultiplayerUserState.Spectating)
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
button.Enabled.Value = enableButton;
if (newCountReady != countReady)
{

View File

@ -0,0 +1,92 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerSpectateButton : MultiplayerRoomComposite
{
public Action OnSpectateClick
{
set => button.Action = value;
}
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private IBindable<bool> operationInProgress;
private readonly ButtonWithTrianglesExposed button;
public MultiplayerSpectateButton()
{
InternalChild = button = new ButtonWithTrianglesExposed
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Enabled = { Value = true },
};
}
[BackgroundDependencyLoader]
private void load()
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
updateState();
}
private void updateState()
{
var localUser = Client.LocalUser;
if (localUser == null)
return;
Debug.Assert(Room != null);
switch (localUser.State)
{
default:
button.Text = "Spectate";
button.BackgroundColour = colours.BlueDark;
button.Triangles.ColourDark = colours.BlueDarker;
button.Triangles.ColourLight = colours.Blue;
break;
case MultiplayerUserState.Spectating:
button.Text = "Stop spectating";
button.BackgroundColour = colours.Gray4;
button.Triangles.ColourDark = colours.Gray5;
button.Triangles.ColourLight = colours.Gray6;
break;
}
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
}
private class ButtonWithTrianglesExposed : TriangleButton
{
public new Triangles Triangles => base.Triangles;
}
}
}

View File

@ -221,7 +221,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
new MultiplayerMatchFooter
{
OnReadyClick = onReadyClick
OnReadyClick = onReadyClick,
OnSpectateClick = onSpectateClick
}
}
},
@ -363,7 +364,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready)
if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating))
{
client.StartMatch()
.ContinueWith(t =>
@ -390,6 +391,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
private void onSpectateClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
client.ToggleSpectate().ContinueWith(t => endOperation());
void endOperation()
{
readyClickOperation?.Dispose();
readyClickOperation = null;
}
}
private void onRoomUpdated()
{
// user mods may have changed.

View File

@ -135,6 +135,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
icon.Colour = colours.BlueLighter;
break;
case MultiplayerUserState.Spectating:
text.Text = "spectating";
icon.Icon = FontAwesome.Solid.Binoculars;
icon.Colour = colours.BlueLight;
break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}

View File

@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
public void ChangeRoomState(MultiplayerRoomState newState)
{
Debug.Assert(Room != null);
((IMultiplayerClient)this).RoomStateChanged(newState);
}
public void ChangeUserState(int userId, MultiplayerUserState newState)
{
Debug.Assert(Room != null);
@ -71,6 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
case MultiplayerUserState.Loaded:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
ChangeRoomState(MultiplayerRoomState.Playing);
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
@ -82,6 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
case MultiplayerUserState.FinishedPlay:
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
{
ChangeRoomState(MultiplayerRoomState.Open);
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
@ -173,6 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Debug.Assert(Room != null);
ChangeRoomState(MultiplayerRoomState.WaitingForLoad);
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);