Path: blob/master/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs
4914 views
// Copyright (c) ppy Pty Ltd <[email protected]>. 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.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { /// <summary> /// A component which maintains the layout of the players in a matchmaking room. /// Can be controlled to display the panels in a certain location and in multiple styles. /// </summary> public partial class PlayerPanelOverlay : CompositeDrawable { [Resolved] private MultiplayerClient client { get; set; } = null!; private Container<PlayerPanel> panels = null!; private PlayerPanelCellContainer gridLayout = null!; private PlayerPanelCellContainer splitLayoutLeft = null!; private PlayerPanelCellContainer splitLayoutRight = null!; private PanelDisplayStyle displayStyle; private Drawable? displayArea; private bool isAnimatingToDisplayArea; [BackgroundDependencyLoader] private void load() { InternalChildren = new Drawable[] { gridLayout = new PlayerPanelCellContainer { RelativeSizeAxes = Axes.Both, Spacing = new Vector2(20), }, splitLayoutLeft = new PlayerPanelCellContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, Spacing = new Vector2(5), }, splitLayoutRight = new PlayerPanelCellContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, Spacing = new Vector2(5), }, panels = new Container<PlayerPanel> { RelativeSizeAxes = Axes.Both } }; } protected override void LoadComplete() { base.LoadComplete(); // Set position/size so we don't initially animate. Position = getFinalPosition(); Size = getFinalSize(); client.MatchRoomStateChanged += onRoomStateChanged; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; client.UserKicked += onUserLeft; if (client.Room != null) { onRoomStateChanged(client.Room.MatchState); foreach (var user in client.Room.Users) onUserJoined(user); } updateDisplay(); } public PanelDisplayStyle DisplayStyle { set { displayStyle = value; if (IsLoaded) updateDisplay(); } } public Drawable? DisplayArea { set { displayArea = value; isAnimatingToDisplayArea = true; } } private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { panels.Add(new PlayerPanel(user) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(0.8f) }); updateDisplay(); }); private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true; updateDisplay(); }); private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(updateDisplay); private void updateDisplay() { gridLayout.ReleasePanels(); splitLayoutLeft.ReleasePanels(); splitLayoutRight.ReleasePanels(); switch (displayStyle) { case PanelDisplayStyle.Grid: foreach (var panel in panels) { panel.FadeTo(1, 200); panel.DisplayMode = PlayerPanelDisplayMode.Vertical; } gridLayout.AcquirePanels(panels.ToArray()); break; case PanelDisplayStyle.Split: foreach (var panel in panels) { panel.FadeTo(1, 200); panel.DisplayMode = PlayerPanelDisplayMode.Horizontal; } int leftCount = (int)Math.Ceiling(panels.Count / 2f); splitLayoutLeft.AcquirePanels(panels.Take(leftCount).ToArray()); splitLayoutRight.AcquirePanels(panels.Skip(leftCount).ToArray()); break; case PanelDisplayStyle.Hidden: foreach (var panel in panels) panel.FadeTo(0, 200); return; } } protected override void Update() { base.Update(); var targetPos = getFinalPosition(); var targetSize = getFinalSize(); double duration = isAnimatingToDisplayArea ? 60 : 0; if (Time.Elapsed > 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. isAnimatingToDisplayArea &= !Precision.AlmostEquals(Size, targetSize, 0.5f); } private Vector2 getFinalPosition() => displayArea == null ? Vector2.Zero : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); private Vector2 getFinalSize() => displayArea == null ? Parent!.DrawSize : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.BottomRight) - Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) { client.MatchRoomStateChanged -= onRoomStateChanged; client.UserJoined -= onUserJoined; client.UserLeft -= onUserLeft; client.UserKicked -= onUserLeft; } } private partial class PlayerPanelCellContainer : FillFlowContainer<PlayerPanelCell> { [Resolved] private MultiplayerClient client { get; set; } = null!; public void AcquirePanels(PlayerPanel[] panels) { while (Count < panels.Length) { Add(new PlayerPanelCell { Anchor = Anchor.Centre, Origin = Anchor.Centre }); } while (Count > panels.Length) Remove(Children[^1], true); for (int i = 0; i < panels.Length; i++) { // We'll invalidate the layout position to represent the new placements and the re-flow will happen in UpdateAfterChildren(). // But the cells expect their positions to be valid as they're updated, which won't be the case until the re-flow happens. int i2 = i; ScheduleAfterChildren(() => Children[i2].AcquirePanel(panels[i2])); if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) continue; if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user) && user.Placement != null) SetLayoutPosition(Children[i], user.Placement.Value); else SetLayoutPosition(Children[i], float.MaxValue); } } public void ReleasePanels() { // Matches the schedule in AcquirePanels. ScheduleAfterChildren(() => { foreach (var panel in Children) panel.ReleasePanel(); }); } } private partial class PlayerPanelCell : Drawable { private PlayerPanel? panel; private bool isAnimating; public void AcquirePanel(PlayerPanel panel) { this.panel = panel; isAnimating = true; } public void ReleasePanel() { panel = null; } protected override void Update() { base.Update(); if (panel?.Parent == null) return; Size = panel.Size * panel.Scale; var targetPos = getFinalPosition(); double duration = isAnimating ? 60 : 0; if (Time.Elapsed > 0) { panel.Position = new Vector2( (float)Interpolation.DampContinuously(panel.Position.X, targetPos.X, duration, Time.Elapsed), (float)Interpolation.DampContinuously(panel.Position.Y, targetPos.Y, duration, Time.Elapsed) ); } // If we don't track the animating state, the animation will also occur when resizing the window. isAnimating &= !Precision.AlmostEquals(panel.Position, targetPos, 0.5f); Vector2 getFinalPosition() => panel.Parent.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; } } } public enum PanelDisplayStyle { Grid, Split, Hidden } }