From 648a9d52584896db0cd3ce3cc7b5bb350bb46d32 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 23:15:09 +0900 Subject: [PATCH 1/3] Add multiplayer spectator player grid --- .../Multiplayer/Spectate/PlayerGrid.cs | 142 ++++++++++++++++++ .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 78 ++++++++++ .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 19 +++ 3 files changed, 239 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs new file mode 100644 index 0000000000..a8bd1db9dc --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class PlayerGrid : CompositeDrawable + { + private const float player_spacing = 5; + + public Drawable MaximisedFacade => maximisedFacade; + + private readonly PlayerGridFacade maximisedFacade; + private readonly Container paddingContainer; + private readonly FillFlowContainer facadeContainer; + private readonly Container cellContainer; + + public PlayerGrid() + { + InternalChildren = new Drawable[] + { + paddingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(player_spacing), + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Child = facadeContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(player_spacing), + } + }, + maximisedFacade = new PlayerGridFacade { RelativeSizeAxes = Axes.Both } + } + }, + cellContainer = new Container { RelativeSizeAxes = Axes.Both } + }; + } + + public void AddContent(Drawable content) + { + var facade = new PlayerGridFacade(); + facadeContainer.Add(facade); + + var cell = new Cell(content) { ToggleMaximisationState = toggleMaximisationState }; + cell.SetFacade(facade); + + cellContainer.Add(cell); + } + + // A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps. + private float maximisedInstanceDepth; + + private void toggleMaximisationState(Cell target) + { + // Iterate through all cells to ensure only one is maximised at any time. + foreach (var i in cellContainer) + { + if (i == target) + i.IsMaximised = !i.IsMaximised; + else + i.IsMaximised = false; + + if (i.IsMaximised) + { + // Transfer cell to the maximised facade. + i.SetFacade(maximisedFacade); + cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f); + } + else + { + // Transfer cell back to its original facade. + i.SetFacade(facadeContainer[cellContainer.IndexOf(target)]); + } + } + } + + protected override void Update() + { + base.Update(); + + Vector2 cellsPerDimension; + + switch (facadeContainer.Count) + { + case 1: + cellsPerDimension = Vector2.One; + break; + + case 2: + cellsPerDimension = new Vector2(2, 1); + break; + + case 3: + case 4: + cellsPerDimension = new Vector2(2); + break; + + case 5: + case 6: + cellsPerDimension = new Vector2(3, 2); + break; + + case 7: + case 8: + case 9: + // 3 rows / 3 cols. + cellsPerDimension = new Vector2(3); + break; + + case 10: + case 11: + case 12: + // 3 rows / 4 cols. + cellsPerDimension = new Vector2(4, 3); + break; + + default: + // 4 rows / 4 cols. + cellsPerDimension = new Vector2(4); + break; + } + + // Total spacing between cells + Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One); + + Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing; + Vector2 cellSize = Vector2.Divide(fullSize, new Vector2(cellsPerDimension.X, cellsPerDimension.Y)); + + foreach (var cell in facadeContainer) + cell.Size = cellSize; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs new file mode 100644 index 0000000000..1f6e718aa7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class PlayerGrid + { + private class Cell : CompositeDrawable + { + public Action ToggleMaximisationState; + public bool IsMaximised; + + private PlayerGridFacade facade; + private bool isTracking = true; + + public Cell(Drawable content) + { + Origin = Anchor.Centre; + + InternalChild = content; + } + + protected override void Update() + { + base.Update(); + + if (isTracking) + { + Position = getFinalPosition(); + Size = getFinalSize(); + } + } + + public void SetFacade([NotNull] PlayerGridFacade newFacade) + { + PlayerGridFacade lastFacade = facade; + facade = newFacade; + + 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; + }); + } + + private Vector2 getFinalPosition() + { + var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero)); + return topLeft + facade.DrawSize / 2; + } + + private Vector2 getFinalSize() => facade.DrawSize; + + // Todo: Temporary? + protected override bool ShouldBeConsideredForInput(Drawable child) => false; + + protected override bool OnClick(ClickEvent e) + { + ToggleMaximisationState(this); + return true; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs new file mode 100644 index 0000000000..c565e2fec6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . 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 + { + private class PlayerGridFacade : Drawable + { + public PlayerGridFacade() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } + } +} From 024adb699c44b5748eebc77a33c2010eb841f562 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 00:06:14 +0900 Subject: [PATCH 2/3] Add test and fix several issues --- ...TestSceneMultiplayerSpectatorPlayerGrid.cs | 115 ++++++++++++++++++ .../Multiplayer/Spectate/PlayerGrid.cs | 30 +++-- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 26 +++- .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 7 +- 4 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs new file mode 100644 index 0000000000..c0958c7fe8 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene + { + private PlayerGrid grid; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }; + }); + + [Test] + public void TestMaximiseAndMinimise() + { + addCells(2); + + assertMaximisation(0, false, true); + assertMaximisation(1, false, true); + + clickCell(0); + assertMaximisation(0, true); + assertMaximisation(1, false, true); + clickCell(0); + assertMaximisation(0, false); + assertMaximisation(1, false, true); + + clickCell(1); + assertMaximisation(1, true); + assertMaximisation(0, false, true); + clickCell(1); + assertMaximisation(1, false); + assertMaximisation(0, false, true); + } + + [Test] + public void TestClickBothCellsSimultaneously() + { + addCells(2); + + AddStep("click cell 0 then 1", () => + { + InputManager.MoveMouseTo(grid.Content.ElementAt(0)); + InputManager.Click(MouseButton.Left); + + InputManager.MoveMouseTo(grid.Content.ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + + assertMaximisation(1, true); + assertMaximisation(0, false); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(9)] + [TestCase(11)] + [TestCase(12)] + [TestCase(15)] + [TestCase(16)] + public void TestCellCount(int count) + { + addCells(count); + AddWaitStep("wait for display", 2); + } + + private void addCells(int count) => AddStep($"add {count} grid cells", () => + { + for (int i = 0; i < count; i++) + grid.Add(new GridContent()); + }); + + private void clickCell(int index) => AddStep($"click cell index {index}", () => + { + InputManager.MoveMouseTo(grid.Content.ElementAt(index)); + InputManager.Click(MouseButton.Left); + }); + + private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false) + { + string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised"; + + if (instant) + AddAssert(assertionText, checkAction); + else + AddUntilStep(assertionText, checkAction); + + bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised; + } + + private class GridContent : Box + { + public GridContent() + { + RelativeSizeAxes = Axes.Both; + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index a8bd1db9dc..f41948217c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -13,9 +13,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public Drawable MaximisedFacade => maximisedFacade; - private readonly PlayerGridFacade maximisedFacade; + private readonly Facade maximisedFacade; private readonly Container paddingContainer; - private readonly FillFlowContainer facadeContainer; + private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; public PlayerGrid() @@ -31,38 +31,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate new Container { RelativeSizeAxes = Axes.Both, - Child = facadeContainer = new FillFlowContainer + Child = facadeContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(player_spacing), } }, - maximisedFacade = new PlayerGridFacade { RelativeSizeAxes = Axes.Both } + maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both } } }, cellContainer = new Container { RelativeSizeAxes = Axes.Both } }; } - public void AddContent(Drawable content) + /// + /// Adds a new cell with content to this grid. + /// + /// The content the cell should contain. + /// If more than 16 cells are added. + public void Add(Drawable content) { - var facade = new PlayerGridFacade(); + int index = cellContainer.Count; + + var facade = new Facade(); facadeContainer.Add(facade); - var cell = new Cell(content) { ToggleMaximisationState = toggleMaximisationState }; + var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState }; cell.SetFacade(facade); cellContainer.Add(cell); } + /// + /// The content added to this grid. + /// + public IEnumerable Content => cellContainer.OrderBy(c => c.FacadeIndex).Select(c => c.Content); + // A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps. private float maximisedInstanceDepth; private void toggleMaximisationState(Cell target) { // Iterate through all cells to ensure only one is maximised at any time. - foreach (var i in cellContainer) + foreach (var i in cellContainer.ToList()) { if (i == target) i.IsMaximised = !i.IsMaximised; @@ -78,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate else { // Transfer cell back to its original facade. - i.SetFacade(facadeContainer[cellContainer.IndexOf(target)]); + i.SetFacade(facadeContainer[i.FacadeIndex]); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 1f6e718aa7..c2ac190d40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -14,17 +14,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { private class Cell : CompositeDrawable { + /// + /// The index of the original facade of this cell. + /// + public readonly int FacadeIndex; + + /// + /// The contained content. + /// + public readonly Drawable Content; + public Action ToggleMaximisationState; public bool IsMaximised; - private PlayerGridFacade facade; + private Facade facade; private bool isTracking = true; - public Cell(Drawable content) + public Cell(int facadeIndex, Drawable content) { - Origin = Anchor.Centre; + FacadeIndex = facadeIndex; - InternalChild = content; + Origin = Anchor.Centre; + InternalChild = Content = content; } protected override void Update() @@ -38,9 +49,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - public void SetFacade([NotNull] PlayerGridFacade newFacade) + /// + /// Makes this cell track a new facade. + /// + public void SetFacade([NotNull] Facade newFacade) { - PlayerGridFacade lastFacade = facade; + Facade lastFacade = facade; facade = newFacade; if (lastFacade == null || lastFacade == newFacade) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs index c565e2fec6..6b363c6040 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs @@ -7,9 +7,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public partial class PlayerGrid { - private class PlayerGridFacade : Drawable + /// + /// A facade of the grid which is used as a dummy object to store the required position/size of cells. + /// + private class Facade : Drawable { - public PlayerGridFacade() + public Facade() { Anchor = Anchor.Centre; Origin = Anchor.Centre; From 5dc939c2f365d09395f0fe1880bb88a3d8dbda21 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 00:06:32 +0900 Subject: [PATCH 3/3] More documentation --- .../OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs | 15 ++++++++++++++- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index f41948217c..830378f129 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -1,16 +1,25 @@ // Copyright (c) ppy Pty Ltd . 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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { + /// + /// A grid of players playing the multiplayer match. + /// public partial class PlayerGrid : CompositeDrawable { private const float player_spacing = 5; + /// + /// The currently-maximised facade. + /// public Drawable MaximisedFacade => maximisedFacade; private readonly Facade maximisedFacade; @@ -52,6 +61,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// If more than 16 cells are added. public void Add(Drawable content) { + if (cellContainer.Count == 16) + throw new InvalidOperationException("Only 16 cells are supported."); + int index = cellContainer.Count; var facade = new Facade(); @@ -99,6 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); + // Different layouts are used for varying cell counts in order to maximise dimensions. Vector2 cellsPerDimension; switch (facadeContainer.Count) @@ -141,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate break; } - // Total spacing between cells + // Total inter-cell spacing. Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One); Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index c2ac190d40..37d88693ee 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -12,6 +12,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public partial class PlayerGrid { + /// + /// A cell of the grid. Contains the content and tracks to the linked facade. + /// private class Cell : CompositeDrawable { /// @@ -24,7 +27,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public readonly Drawable Content; + /// + /// An action that toggles the maximisation state of this cell. + /// public Action ToggleMaximisationState; + + /// + /// Whether this cell is currently maximised. + /// public bool IsMaximised; private Facade facade;