1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 03:39:53 +08:00

Merge pull request #35878 from frenzibyte/vote-to-skip-design-2

Update multiplayer vote-to-skip button design
This commit is contained in:
Dean Herbert
2025-12-05 21:40:16 +09:00
committed by GitHub
Unverified
3 changed files with 346 additions and 83 deletions
@@ -1,9 +1,11 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
@@ -33,6 +35,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
Children = new Drawable[]
{
new MultiplayerSkipOverlay(120000)
{
RequestSkip = () => MultiplayerClient.VoteToSkipIntro().WaitSafely(),
}
},
};
@@ -47,26 +52,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
int userId = i;
AddStep($"join user {i2}", () =>
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = i2,
Username = $"User {i2}"
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing);
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely());
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("local user votes", () => this.ChildrenOfType<MultiplayerSkipOverlay.Button>().Single().TriggerClick());
AddStep("user 1 votes", () => MultiplayerClient.UserVoteToSkipIntro(1).WaitSafely());
}
[Test]
public void TestLeavingBeforeLocalVote()
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely());
int userId = i;
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 }));
AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 }));
AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 }));
}
[Test]
public void TestLeavingAfterLocalVote()
{
for (int i = 0; i < 4; i++)
{
int userId = i;
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => this.ChildrenOfType<MultiplayerSkipOverlay.Button>().Single().TriggerClick());
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 }));
AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 }));
AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 }));
}
public partial class TestMultiplayerSkipOverlay : MultiplayerSkipOverlay
{
public TestMultiplayerSkipOverlay()
: base(120000)
{
}
}
}
@@ -4,15 +4,24 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osuTK;
using osuTK.Graphics;
@@ -23,90 +32,49 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Drawable votedIcon = null!;
private OsuSpriteText countText = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
private Button skipButton = null!;
public MultiplayerSkipOverlay(double startTime)
: base(startTime)
{
}
[BackgroundDependencyLoader]
private void load()
protected override OsuClickableContainer CreateButton() => skipButton = new Button
{
FadingContent.AddRange(
[
votedIcon = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(50, 0),
Size = new Vector2(20),
Alpha = 0,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Green
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.5f),
Icon = FontAwesome.Solid.Check
}
}
},
countText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
Position = new Vector2(0.75f, 0),
Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold)
}
]);
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
protected override void LoadComplete()
{
base.LoadComplete();
skipButton.Enabled.BindValueChanged(e =>
{
RemainingTimeBox.Colour = e.NewValue ? colours.Orange3 : Button.COLOUR_GRAY;
}, true);
client.UserLeft += onUserLeft;
client.UserStateChanged += onUserStateChanged;
client.UserVotedToSkipIntro += onUserVotedToSkipIntro;
updateText();
updateCount();
}
private void onUserLeft(MultiplayerRoomUser user)
{
Schedule(updateText);
}
private void onUserLeft(MultiplayerRoomUser user) => Schedule(updateCount);
private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state)
{
Schedule(updateText);
}
private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) => Schedule(updateCount);
private void onUserVotedToSkipIntro(int userId) => Schedule(() =>
{
updateText();
countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine);
if (userId == client.LocalUser?.UserID)
{
votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine);
votedIcon.FadeInFromZero(100);
}
FadingContent.TriggerShow();
updateCount();
});
private void updateText()
private void updateCount()
{
if (client.Room == null || client.Room.Settings.AutoSkip)
return;
@@ -115,7 +83,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro);
int countRequired = countTotal / 2 + 1;
countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}";
skipButton.SkippedCount.Value = Math.Min(countRequired, countSkipped);
skipButton.RequiredCount.Value = countRequired;
}
protected override void Dispose(bool isDisposing)
@@ -129,5 +98,218 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.UserVotedToSkipIntro -= onUserVotedToSkipIntro;
}
}
public partial class Button : OsuClickableContainer
{
private const float chevron_y = 0.4f;
private const float secondary_y = 0.7f;
public static readonly Color4 COLOUR_GRAY = OsuColour.Gray(0.4f);
private Box background = null!;
private Box box = null!;
private TrianglesV2 triangles = null!;
private OsuSpriteText countText = null!;
private OsuSpriteText skipText = null!;
private AspectContainer aspect = null!;
private FillFlowContainer chevrons = null!;
private Sample sampleConfirm = null!;
public readonly BindableInt SkippedCount = new BindableInt();
public readonly BindableInt RequiredCount = new BindableInt();
[Resolved]
private OsuColour colours { get; set; } = null!;
public Button()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleConfirm = audio.Samples.Get(@"UI/submit-select");
Children = new Drawable[]
{
background = new Box
{
Alpha = 0.2f,
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
aspect = new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Height = 0.6f,
Masking = true,
CornerRadius = 15,
Children = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
},
triangles = new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
},
countText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
RelativePositionAxes = Axes.Y,
Y = 0.35f,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24),
Origin = Anchor.Centre,
},
chevrons = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
RelativePositionAxes = Axes.Y,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Children = new[]
{
new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight },
new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight },
new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight },
}
},
skipText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
RelativePositionAxes = Axes.Y,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
Origin = Anchor.Centre,
Text = @"SKIP",
Y = secondary_y,
},
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
SkippedCount.BindValueChanged(_ => updateCount());
RequiredCount.BindValueChanged(_ => updateCount(), true);
Enabled.BindValueChanged(_ => updateColours(), true);
FinishTransforms(true);
}
private void updateChevronsSpacing()
{
if (SkippedCount.Value > 0 && RequiredCount.Value > 1)
chevrons.TransformSpacingTo(new Vector2(-5f), 500, Easing.OutQuint);
else
chevrons.TransformSpacingTo(IsHovered ? new Vector2(5f) : new Vector2(0f), 500, Easing.OutQuint);
}
private void updateCount()
{
if (SkippedCount.Value > 0 && RequiredCount.Value > 1)
{
countText.FadeIn(300, Easing.OutQuint);
countText.Text = $"{SkippedCount.Value} / {RequiredCount.Value}";
chevrons.ScaleTo(0.5f, 300, Easing.OutQuint)
.MoveTo(new Vector2(-11, secondary_y), 300, Easing.OutQuint);
skipText.MoveToX(11f, 300, Easing.OutQuint);
}
else
{
countText.FadeOut(300, Easing.OutQuint);
chevrons.ScaleTo(1f, 300, Easing.OutQuint)
.MoveTo(new Vector2(0, chevron_y), 300, Easing.OutQuint);
skipText.MoveToX(0f, 300, Easing.OutQuint);
}
updateChevronsSpacing();
updateColours();
}
private void updateColours()
{
if (!Enabled.Value)
{
box.FadeColour(COLOUR_GRAY, 500, Easing.OutQuint);
triangles.FadeColour(ColourInfo.GradientVertical(COLOUR_GRAY.Lighten(0.2f), COLOUR_GRAY), 500, Easing.OutQuint);
}
else
{
box.FadeColour(IsHovered ? colours.Orange3.Lighten(0.2f) : colours.Orange3, 500, Easing.OutQuint);
triangles.FadeColour(ColourInfo.GradientVertical(colours.Orange3.Lighten(0.2f), colours.Orange3), 500, Easing.OutQuint);
}
}
protected override bool OnHover(HoverEvent e)
{
if (Enabled.Value)
{
updateChevronsSpacing();
updateColours();
background.FadeTo(0.4f, 500, Easing.OutQuint);
}
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateChevronsSpacing();
updateColours();
background.FadeTo(0.2f, 500, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (Enabled.Value)
aspect.ScaleTo(0.75f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (Enabled.Value)
aspect.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
return false;
sampleConfirm.Play();
box.FlashColour(Color4.White, 500, Easing.OutQuint);
aspect.ScaleTo(1.2f, 2000, Easing.OutQuint);
base.OnClick(e);
Enabled.Value = false;
return true;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
countText.Scale = new Vector2(Math.Min(0.85f * aspect.DrawWidth / countText.DrawWidth, 1));
}
}
}
}
+33 -14
View File
@@ -10,7 +10,9 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@@ -19,6 +21,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
@@ -41,12 +44,18 @@ namespace osu.Game.Screens.Play
protected FadeContainer FadingContent { get; private set; }
private Button button;
private OsuClickableContainer button;
private ButtonContainer buttonContainer;
private Circle remainingTimeBox;
protected Circle RemainingTimeBox { get; private set; }
private double displayTime;
/// <summary>
/// Becomes <see langword="false"/> when the overlay starts fading out.
/// </summary>
private bool isClickable;
private bool skipQueued;
[Resolved]
@@ -83,17 +92,13 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
button = new Button
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
remainingTimeBox = new Circle
button = CreateButton(),
RemainingTimeBox = new Circle
{
Height = 5,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = colours.Yellow,
Colour = colours.Orange3,
RelativeSizeAxes = Axes.X
}
}
@@ -101,6 +106,12 @@ namespace osu.Game.Screens.Play
};
}
protected virtual OsuClickableContainer CreateButton() => new Button
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
private const double fade_time = 300;
private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME;
@@ -174,10 +185,13 @@ namespace osu.Game.Screens.Play
double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime));
remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
RemainingTimeBox.Width = (float)Interpolation.Lerp(RemainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
isClickable = progress > 0;
button.Enabled.Value = isClickable;
if (!isClickable)
button.Enabled.Value = false;
buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden;
}
@@ -220,7 +234,7 @@ namespace osu.Game.Screens.Play
float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime);
float newWidth = 1 - Math.Clamp(progress, 0, 1);
remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint);
RemainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint);
}
public partial class FadeContainer : Container, IStateful<Visibility>
@@ -328,8 +342,8 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
colourNormal = colours.Yellow;
colourHover = colours.YellowDark;
colourNormal = colours.Orange3;
colourHover = colours.Orange3.Lighten(0.2f);
sampleConfirm = audio.Samples.Get(@"UI/submit-select");
@@ -356,6 +370,11 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both,
Colour = colourNormal,
},
new TrianglesV2
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(colourNormal.Lighten(0.2f), colourNormal)
},
flow = new FillFlowContainer
{
Anchor = Anchor.TopCentre,