Merge pull request #24415 from peppy/multi-spectator-improvements

General visual improvements to mutliplayer spectatator screen
This commit is contained in:
Bartłomiej Dach 2023-07-30 12:43:26 +02:00 committed by GitHub
commit 898913f32a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 85 deletions

View File

@ -65,6 +65,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("clear playing users", () => playingUsers.Clear());
}
[TestCase(1)]
[TestCase(4)]
public void TestGeneral(int count)
{
int[] userIds = getPlayerIds(count);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestDelayedStart()
{
@ -88,18 +101,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
[Test]
public void TestGeneral()
{
int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestSpectatorPlayerInteractiveElementsHidden()
{

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
public override bool? ApplyModTrackAdjustments => false;
public override bool HideOverlaysOnEnter => true;
/// <summary>
/// Whether all spectating players have finished loading.
/// </summary>

View File

@ -67,7 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
SpectatorPlayerClock = clock;
RelativeSizeAxes = Axes.Both;
Masking = true;
AudioContainer audioContainer;
InternalChildren = new Drawable[]

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@ -15,20 +16,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public partial class PlayerGrid : CompositeDrawable
{
public const float ANIMATION_DELAY = 400;
/// <summary>
/// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen.
/// Todo: Can be removed in the future with scrolling support + performance improvements.
/// </summary>
public const int MAX_PLAYERS = 16;
private const float player_spacing = 5;
private const float player_spacing = 6;
/// <summary>
/// The currently-maximised facade.
/// </summary>
public Drawable MaximisedFacade => maximisedFacade;
public Facade MaximisedFacade { get; }
private readonly Facade maximisedFacade;
private readonly Container paddingContainer;
private readonly FillFlowContainer<Facade> facadeContainer;
private readonly Container<Cell> cellContainer;
@ -48,12 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
RelativeSizeAxes = Axes.Both,
Child = facadeContainer = new FillFlowContainer<Facade>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(player_spacing),
}
},
maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both }
MaximisedFacade = new Facade
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
}
}
},
cellContainer = new Container<Cell> { RelativeSizeAxes = Axes.Both }
@ -75,8 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var facade = new Facade();
facadeContainer.Add(facade);
var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState };
cell.SetFacade(facade);
var cell = new Cell(index, content, facade) { ToggleMaximisationState = toggleMaximisationState };
cellContainer.Add(cell);
}
@ -91,26 +98,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void toggleMaximisationState(Cell target)
{
// Iterate through all cells to ensure only one is maximised at any time.
foreach (var i in cellContainer.ToList())
{
if (i == target)
i.IsMaximised = !i.IsMaximised;
else
i.IsMaximised = false;
// in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised.
bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1;
if (i.IsMaximised)
// Iterate through all cells to ensure only one is maximised at any time.
foreach (var cell in cellContainer.ToList())
{
if (hasMaximised && cell == target)
{
// Transfer cell to the maximised facade.
i.SetFacade(maximisedFacade);
cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f);
cell.SetFacade(MaximisedFacade, true);
cellContainer.ChangeChildDepth(cell, maximisedInstanceDepth -= 0.001f);
}
else
{
// Transfer cell back to its original facade.
i.SetFacade(facadeContainer[i.FacadeIndex]);
cell.SetFacade(facadeContainer[cell.FacadeIndex], false);
}
cell.FadeColour(hasMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.OutQuint);
}
facadeContainer.ScaleTo(hasMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint);
}
protected override void Update()
@ -169,5 +178,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
foreach (var cell in facadeContainer)
cell.Size = cellSize;
}
/// <summary>
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
/// </summary>
public partial class Facade : Drawable
{
public Facade()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
}
}
}

View File

@ -1,13 +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.
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
@ -32,68 +31,79 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary>
/// An action that toggles the maximisation state of this cell.
/// </summary>
public Action<Cell> ToggleMaximisationState;
public Action<Cell>? ToggleMaximisationState;
/// <summary>
/// Whether this cell is currently maximised.
/// </summary>
public bool IsMaximised;
public bool IsMaximised { get; private set; }
private Facade facade;
private bool isTracking = true;
public Cell(int facadeIndex, Drawable content)
private bool isAnimating;
public Cell(int facadeIndex, Drawable content, Facade facade)
{
FacadeIndex = facadeIndex;
this.facade = facade;
Origin = Anchor.Centre;
InternalChild = Content = content;
Masking = true;
CornerRadius = 5;
}
protected override void Update()
{
base.Update();
if (isTracking)
{
Position = getFinalPosition();
Size = getFinalSize();
}
var targetPos = getFinalPosition();
var targetSize = getFinalSize();
double duration = isAnimating ? 60 : 0;
Position = new Vector2(
(float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed),
(float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed)
);
Size = new Vector2(
(float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed),
(float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed)
);
// If we don't track the animating state, the animation will also occur when resizing the window.
isAnimating &= !Precision.AlmostEquals(Position, targetPos, 0.01f);
}
/// <summary>
/// Makes this cell track a new facade.
/// </summary>
public void SetFacade([NotNull] Facade newFacade)
public void SetFacade(Facade newFacade, bool isMaximised)
{
Facade lastFacade = facade;
facade = newFacade;
IsMaximised = isMaximised;
isAnimating = true;
if (lastFacade == null || lastFacade == newFacade)
return;
isTracking = false;
this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
.Then()
.OnComplete(_ =>
{
if (facade == newFacade)
isTracking = true;
});
TweenEdgeEffectTo(new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = isMaximised ? 30 : 10,
Colour = Colour4.Black.Opacity(isMaximised ? 0.5f : 0.2f),
}, ANIMATION_DELAY, Easing.OutQuint);
}
private Vector2 getFinalPosition()
{
var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
return topLeft + facade.DrawSize / 2;
}
private Vector2 getFinalPosition() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre);
private Vector2 getFinalSize() => facade.DrawSize;
private Vector2 getFinalSize() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight)
- Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft);
protected override bool OnClick(ClickEvent e)
{
ToggleMaximisationState(this);
ToggleMaximisationState?.Invoke(this);
return true;
}
}

View File

@ -1,22 +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.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public partial class PlayerGrid
{
/// <summary>
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
/// </summary>
private partial class Facade : Drawable
{
public Facade()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
}
}
}