1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-07 21:32:57 +08:00

Use IApplicableToRate in osu! auto generator

This commit is contained in:
Bartłomiej Dach 2021-01-31 17:56:03 +01:00
parent 3fabe247b0
commit 0e1ec703d3
2 changed files with 62 additions and 27 deletions

View File

@ -35,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Constants #region Constants
/// <summary>
/// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
private readonly double reactionTime;
private readonly HitWindows defaultHitWindows; private readonly HitWindows defaultHitWindows;
/// <summary> /// <summary>
@ -54,9 +49,6 @@ namespace osu.Game.Rulesets.Osu.Replays
public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods) public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap, mods) : base(beatmap, mods)
{ {
// Already superhuman, but still somewhat realistic
reactionTime = ApplyModsToRate(100);
defaultHitWindows = new OsuHitWindows(); defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
} }
@ -242,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1]; OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note. // Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
if (waitTime > lastFrame.Time) if (waitTime > lastFrame.Time)
{ {
@ -252,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 lastPosition = lastFrame.Position; Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time); double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks if (timeDifference > 0 && // Sanity checks
@ -260,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{ {
// Perform eased movement // Perform eased movement
for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{ {
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
@ -274,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays
} }
} }
/// <summary>
/// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
/// <remarks>
/// Already superhuman, but still somewhat realistic.
/// </remarks>
private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100);
// Add frames to click the hitobject // Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{ {
@ -343,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t; double t;
double previousFrame = h.StartTime;
for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay) for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{ {
t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
angle += (float)t / 20;
Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
previousFrame = nextFrame;
} }
t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection; t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); angle += (float)t / 20;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action));
@ -361,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays
break; break;
case Slider slider: case Slider slider:
for (double j = FrameDelay; j < slider.Duration; j += FrameDelay) for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j))
{ {
Vector2 pos = slider.StackedPositionAt(j / slider.Duration); Vector2 pos = slider.StackedPositionAt(j / slider.Duration);
AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action));

View File

@ -5,6 +5,7 @@ using osuTK;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -23,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays
public const float SPIN_RADIUS = 50; public const float SPIN_RADIUS = 50;
/// <summary>
/// The time in ms between each ReplayFrame.
/// </summary>
protected readonly double FrameDelay;
#endregion #endregion
#region Construction / Initialisation #region Construction / Initialisation
protected Replay Replay; protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames; protected List<ReplayFrame> Frames => Replay.Frames;
private readonly IReadOnlyList<IApplicableToRate> timeAffectingMods;
protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList<Mod> mods) protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay(); Replay = new Replay();
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. timeAffectingMods = mods.OfType<IApplicableToRate>().ToList();
FrameDelay = ApplyModsToRate(1000.0 / 60.0);
} }
#endregion #endregion
#region Utilities #region Utilities
protected double ApplyModsToTime(double v) => v; /// <summary>
protected double ApplyModsToRate(double v) => v; /// Returns the real duration of time between <paramref name="startTime"/> and <paramref name="endTime"/>
/// after applying rate-affecting mods.
/// </summary>
/// <remarks>
/// This method should only be used when <paramref name="startTime"/> and <paramref name="endTime"/> are very close.
/// That is because the track rate might be changing with time,
/// and the method used here is a rough instantaneous approximation.
/// </remarks>
/// <param name="startTime">The start time of the time delta, in original track time.</param>
/// <param name="endTime">The end time of the time delta, in original track time.</param>
protected double ApplyModsToTimeDelta(double startTime, double endTime)
{
double delta = endTime - startTime;
foreach (var mod in timeAffectingMods)
delta /= mod.ApplyToRate(startTime);
return delta;
}
protected double ApplyModsToRate(double time, double rate)
{
foreach (var mod in timeAffectingMods)
rate = mod.ApplyToRate(time, rate);
return rate;
}
/// <summary>
/// Calculates the interval after which the next <see cref="ReplayFrame"/> should be generated,
/// in milliseconds.
/// </summary>
/// <param name="time">The time of the previous frame.</param>
protected double GetFrameDelay(double time)
=> ApplyModsToRate(time, 1000.0 / 60);
private class ReplayFrameComparer : IComparer<ReplayFrame> private class ReplayFrameComparer : IComparer<ReplayFrame>
{ {