1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 16:12:57 +08:00

Merge pull request #20157 from peppy/true-gameplay-rate

Refactor `TrueGameplayRate` to account for only gameplay adjustments, no matter what
This commit is contained in:
Dan Balasescu 2022-09-08 19:54:32 +09:00 committed by GitHub
commit 9aab502adc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 126 additions and 89 deletions

View File

@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate));
}
private void resetState(DrawableHitObject obj)

View File

@ -1,8 +1,9 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
@ -13,21 +14,20 @@ namespace osu.Game.Tests.NonVisual
{
[TestCase(0)]
[TestCase(1)]
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
{
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
var gameplayClock = new TestGameplayClockContainer(framedClock);
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
}
private class TestGameplayClockContainer : GameplayClockContainer
{
public override IEnumerable<double> NonGameplayAdjustments => new[] { 0.0 };
public TestGameplayClockContainer(IFrameBasedClock underlyingClock)
: base(underlyingClock)
{
AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0));
}
}
}

View File

@ -370,7 +370,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void confirmNoTrackAdjustments()
{
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
AddUntilStep("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value, () => Is.EqualTo(1));
}
private void restart() => AddStep("restart", () => Player.Restart());

View File

@ -13,9 +13,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
@ -332,6 +334,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000);
}
[Test]
public void TestGameplayRateAdjust()
{
start(getPlayerIds(4), mods: new[] { new APIMod(new OsuModDoubleTime()) });
loadSpectateScreen();
sendFrames(getPlayerIds(4), 300);
AddUntilStep("wait for correct track speed", () => Beatmap.Value.Track.Rate, () => Is.EqualTo(1.5));
}
[Test]
public void TestPlayersLeaveWhileSpectating()
{
@ -420,7 +434,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null)
private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null)
{
AddStep("start play", () =>
{
@ -429,10 +443,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var user = new MultiplayerRoomUser(id)
{
User = new APIUser { Id = id },
Mods = mods ?? Array.Empty<APIMod>(),
};
OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true);
SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId);
OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);
SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId, mods);
playingUsers.Add(user);
}

View File

@ -2,15 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Input.Handlers;
using osu.Game.Screens.Play;
@ -263,27 +261,11 @@ namespace osu.Game.Rulesets.UI
public FrameTimeInfo TimeInfo => framedClock.TimeInfo;
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (double adjustment in NonGameplayAdjustments)
{
if (Precision.AlmostEquals(adjustment, 0))
return 0;
baseRate /= adjustment;
}
return baseRate;
}
}
public double StartTime => parentGameplayClock?.StartTime ?? 0;
public IEnumerable<double> NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<double>();
private readonly AudioAdjustments gameplayAdjustments = new AudioAdjustments();
public IAdjustableAudioComponent AdjustmentsFromMods => parentGameplayClock?.AdjustmentsFromMods ?? gameplayAdjustments;
#endregion

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@ -13,6 +14,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public class MultiSpectatorPlayer : SpectatorPlayer
{
/// <summary>
/// All adjustments applied to the clock of this <see cref="MultiSpectatorPlayer"/> which come from mods.
/// </summary>
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
private readonly SpectatorPlayerClock spectatorPlayerClock;
/// <summary>
@ -53,6 +60,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new GameplayClockContainer(spectatorPlayerClock);
{
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock);
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
return gameplayClockContainer;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -43,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
private IAggregateAudioAdjustment? boundAdjustments;
private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer = null!;
private SpectatorSyncManager syncManager = null!;
@ -157,6 +160,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete();
masterClockContainer.Reset();
// Start with adjustments from the first player to keep a sane state.
bindAudioAdjustments(instances.First());
}
protected override void Update()
@ -169,11 +175,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
.OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime))
.FirstOrDefault();
// Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio.
if (currentAudioSource != null)
bindAudioAdjustments(currentAudioSource);
foreach (var instance in instances)
instance.Mute = instance != currentAudioSource;
}
}
private void bindAudioAdjustments(PlayerArea first)
{
if (boundAdjustments != null)
masterClockContainer.AdjustmentsFromMods.UnbindAdjustments(boundAdjustments);
boundAdjustments = first.ClockAdjustmentsFromMods;
masterClockContainer.AdjustmentsFromMods.BindAdjustments(boundAdjustments);
}
private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;

View File

@ -42,6 +42,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public readonly SpectatorPlayerClock SpectatorPlayerClock;
/// <summary>
/// The clock adjustments applied by the <see cref="Player"/> loaded in this area.
/// </summary>
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
/// <summary>
/// The currently-loaded score.
/// </summary>
@ -50,6 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer;
@ -97,6 +103,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
var player = new MultiSpectatorPlayer(Score, SpectatorPlayerClock);
player.OnGameplayStarted += () => OnGameplayStarted?.Invoke();
clockAdjustmentsFromMods.BindAdjustments(player.ClockAdjustmentsFromMods);
return player;
}));

View File

@ -2,15 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Play
@ -46,7 +44,7 @@ namespace osu.Game.Screens.Play
/// </remarks>
public double StartTime { get; protected set; }
public virtual IEnumerable<double> NonGameplayAdjustments => Enumerable.Empty<double>();
public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments();
private readonly BindableBool isPaused = new BindableBool(true);
@ -196,7 +194,9 @@ namespace osu.Game.Screens.Play
void IAdjustableClock.Reset() => Reset();
public void ResetSpeedAdjustments() => throw new NotImplementedException();
public virtual void ResetSpeedAdjustments()
{
}
double IAdjustableClock.Rate
{
@ -222,23 +222,5 @@ namespace osu.Game.Screens.Play
public double FramesPerSecond => GameplayClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo;
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (double adjustment in NonGameplayAdjustments)
{
if (Precision.AlmostEquals(adjustment, 0))
return 0;
baseRate /= adjustment;
}
return baseRate;
}
}
}
}

View File

@ -0,0 +1,24 @@
// 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;
namespace osu.Game.Screens.Play
{
public static class GameplayClockExtensions
{
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
public static double GetTrueGameplayRate(this IGameplayClock clock)
{
// To handle rewind, we still want to maintain the same direction as the underlying clock.
double rate = clock.Rate == 0 ? 1 : Math.Sign(clock.Rate);
return rate
* clock.AdjustmentsFromMods.AggregateFrequency.Value
* clock.AdjustmentsFromMods.AggregateTempo.Value;
}
}
}

View File

@ -1,7 +1,7 @@
// 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.Collections.Generic;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Timing;
@ -9,12 +9,6 @@ namespace osu.Game.Screens.Play
{
public interface IGameplayClock : IFrameBasedClock
{
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
double TrueGameplayRate { get; }
/// <summary>
/// The time from which the clock should start. Will be seeked to on calling <see cref="GameplayClockContainer.Reset"/>.
/// </summary>
@ -25,9 +19,9 @@ namespace osu.Game.Screens.Play
double StartTime { get; }
/// <summary>
/// All adjustments applied to this clock which don't come from gameplay or mods.
/// All adjustments applied to this clock which come from mods.
/// </summary>
IEnumerable<double> NonGameplayAdjustments { get; }
IAdjustableAudioComponent AdjustmentsFromMods { get; }
IBindable<bool> IsPaused { get; }
}

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
namespace osu.Game.Screens.Play
{
@ -41,9 +42,9 @@ namespace osu.Game.Screens.Play
private readonly WorkingBeatmap beatmap;
private readonly double skipTargetTime;
private readonly Track track;
private readonly List<Bindable<double>> nonGameplayAdjustments = new List<Bindable<double>>();
private readonly double skipTargetTime;
/// <summary>
/// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered.
@ -56,7 +57,8 @@ namespace osu.Game.Screens.Play
/// </summary>
private double? actualStopTime;
public override IEnumerable<double> NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value);
[Resolved]
private MusicController musicController { get; set; } = null!;
/// <summary>
/// Create a new master gameplay clock container.
@ -69,6 +71,8 @@ namespace osu.Game.Screens.Play
this.beatmap = beatmap;
this.skipTargetTime = skipTargetTime;
track = beatmap.Track;
StartTime = findEarliestStartTime();
}
@ -195,15 +199,12 @@ namespace osu.Game.Screens.Play
if (speedAdjustmentsApplied)
return;
if (SourceClock is not Track track)
return;
musicController.ResetTrackAdjustments();
track.BindAdjustments(AdjustmentsFromMods);
track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
nonGameplayAdjustments.Add(GameplayClock.ExternalPauseFrequencyAdjust);
nonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true;
}
@ -212,15 +213,10 @@ namespace osu.Game.Screens.Play
if (!speedAdjustmentsApplied)
return;
if (SourceClock is not Track track)
return;
track.UnbindAdjustments(AdjustmentsFromMods);
track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
nonGameplayAdjustments.Remove(GameplayClock.ExternalPauseFrequencyAdjust);
nonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false;
}

View File

@ -996,12 +996,8 @@ namespace osu.Game.Screens.Play
foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>())
mod.ApplyToHUD(HUDOverlay);
// Our mods are local copies of the global mods so they need to be re-applied to the track.
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
musicController.ResetTrackAdjustments();
foreach (var mod in GameplayState.Mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(musicController.CurrentTrack);
mod.ApplyToTrack(GameplayClockContainer.AdjustmentsFromMods);
updateGameplayState();

View File

@ -81,13 +81,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void Disconnect() => isConnected.Value = false;
public MultiplayerRoomUser AddUser(APIUser user, bool markAsPlaying = false)
{
var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
=> AddUser(new MultiplayerRoomUser(user.Id) { User = user }, markAsPlaying);
public MultiplayerRoomUser AddUser(MultiplayerRoomUser roomUser, bool markAsPlaying = false)
{
addUser(roomUser);
if (markAsPlaying)
PlayingUserIds.Add(user.Id);
PlayingUserIds.Add(roomUser.UserID);
return roomUser;
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -37,6 +38,7 @@ namespace osu.Game.Tests.Visual.Spectator
private readonly Dictionary<int, ReplayFrame> lastReceivedUserFrames = new Dictionary<int, ReplayFrame>();
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
private readonly Dictionary<int, APIMod[]> userModsDictionary = new Dictionary<int, APIMod[]>();
private readonly Dictionary<int, int> userNextFrameDictionary = new Dictionary<int, int>();
[Resolved]
@ -52,9 +54,11 @@ namespace osu.Game.Tests.Visual.Spectator
/// </summary>
/// <param name="userId">The user to start play for.</param>
/// <param name="beatmapId">The playing beatmap id.</param>
public void SendStartPlay(int userId, int beatmapId)
/// <param name="mods">The mods the user has applied.</param>
public void SendStartPlay(int userId, int beatmapId, APIMod[]? mods = null)
{
userBeatmapDictionary[userId] = beatmapId;
userModsDictionary[userId] = mods ?? Array.Empty<APIMod>();
userNextFrameDictionary[userId] = 0;
sendPlayingState(userId);
}
@ -73,10 +77,12 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
Mods = userModsDictionary[userId],
State = state
});
userBeatmapDictionary.Remove(userId);
userModsDictionary.Remove(userId);
}
/// <summary>
@ -125,6 +131,7 @@ namespace osu.Game.Tests.Visual.Spectator
// Track the local user's playing beatmap ID.
Debug.Assert(state.BeatmapID != null);
userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value;
userModsDictionary[api.LocalUser.Value.Id] = state.Mods.ToArray();
return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state);
}
@ -158,6 +165,7 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
Mods = userModsDictionary[userId],
State = SpectatedUserState.Playing
});
}