Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first pieces of editor timing UI #18339

Merged
merged 21 commits into from May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMuted.cs
Expand Up @@ -22,7 +22,7 @@ public class TestSceneOsuModMuted : OsuModTestScene
MuteComboCount = { Value = 0 },
},
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
Player.ChildrenOfType<MetronomeBeat>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
});

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
Expand Up @@ -339,7 +339,7 @@ private void randomizeCirclePos(IReadOnlyList<OsuHitObject> hitObjects)

public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
}

#endregion
Expand Down
116 changes: 116 additions & 0 deletions osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
@@ -0,0 +1,116 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
using osuTK;

namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneTapTimingControl : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;

[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);

[Cached]
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();

private TapTimingControl control;

public TestSceneTapTimingControl()
{
var playableBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);

// Ensure time doesn't end while testing
playableBeatmap.BeatmapInfo.Length = 1200000;

editorBeatmap = new EditorBeatmap(playableBeatmap);

selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
}

protected override void LoadComplete()
{
base.LoadComplete();

Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Disabled = true;

Children = new Drawable[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 400,
Scale = new Vector2(1.5f),
Child = control = new TapTimingControl(),
}
};
}

[Test]
public void TestTapThenReset()
{
AddStep("click tap button", () =>
{
control.ChildrenOfType<RoundedButton>()
.Last()
.TriggerClick();
});

AddUntilStep("wait for track playing", () => Clock.IsRunning);

AddStep("click reset button", () =>
{
control.ChildrenOfType<RoundedButton>()
.First()
.TriggerClick();
});

AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}

[Test]
public void TestBasic()
{
AddStep("set low bpm", () =>
{
editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 1000;
});

AddStep("click tap button", () =>
{
control.ChildrenOfType<RoundedButton>()
.Last()
.TriggerClick();
});

AddSliderStep("BPM", 30, 400, 60, bpm => editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm);
}

protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
base.Dispose(isDisposing);
}
}
}
39 changes: 17 additions & 22 deletions osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
Expand Down Expand Up @@ -82,11 +83,15 @@ public void TestDisallowMistimedEventFiring(bool allowMistimed)

if (!allowMistimed)
{
AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
AddAssert("trigger is near beat length",
() => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value,
BeatSyncedContainer.MISTIMED_ALLOWANCE));
}
else
{
AddAssert("trigger is not near beat length", () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
AddAssert("trigger is not near beat length",
() => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength,
lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
}
}

Expand Down Expand Up @@ -258,24 +263,7 @@ public TestBeatSyncedContainer()
};
}

protected override void LoadComplete()
{
base.LoadComplete();

Beatmap.BindValueChanged(_ =>
{
timingPointCount.Value = 0;
currentTimingPoint.Value = 0;
beatCount.Value = 0;
currentBeat.Value = 0;
beatsPerMinute.Value = 0;
adjustedBeatLength.Value = 0;
timeUntilNextBeat.Value = 0;
timeSinceLastBeat.Value = 0;
}, true);
}

private List<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
private List<TimingControlPoint> timingPoints => BeatSyncSource.ControlPoints?.TimingPoints.ToList();

private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{
Expand All @@ -292,17 +280,24 @@ private int calculateBeatCount(TimingControlPoint current)
if (timingPoints.Count == 0) return 0;

if (timingPoints[^1] == current)
return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength);
{
Debug.Assert(BeatSyncSource.Clock != null);

return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength);
}

return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
}

protected override void Update()
{
base.Update();

Debug.Assert(BeatSyncSource.Clock != null);

timeUntilNextBeat.Value = TimeUntilNextBeat;
timeSinceLastBeat.Value = TimeSinceLastBeat;
currentTime.Value = BeatSyncClock.CurrentTime;
currentTime.Value = BeatSyncSource.Clock.CurrentTime;
}

public Action<int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes> NewBeat;
Expand Down
27 changes: 27 additions & 0 deletions osu.Game/Beatmaps/IBeatSyncProvider.cs
@@ -0,0 +1,27 @@
// 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.

#nullable enable

using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;

namespace osu.Game.Beatmaps
{
/// <summary>
/// Provides various data sources which allow for synchronising visuals to a known beat.
/// Primarily intended for use with <see cref="BeatSyncedContainer"/>.
/// </summary>
[Cached]
public interface IBeatSyncProvider
{
ControlPointInfo? ControlPoints { get; }

IClock? Clock { get; }

ChannelAmplitudes? Amplitudes { get; }
}
}
53 changes: 12 additions & 41 deletions osu.Game/Graphics/Containers/BeatSyncedContainer.cs
Expand Up @@ -5,9 +5,7 @@
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
Expand Down Expand Up @@ -74,65 +72,38 @@ public class BeatSyncedContainer : Container
/// </summary>
protected bool IsBeatSyncedWithTrack { get; private set; }

protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
}

[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }

[Resolved(canBeNull: true)]
protected GameplayClock GameplayClock { get; private set; }
protected IBeatSyncProvider BeatSyncSource { get; private set; }

protected IClock BeatSyncClock
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
get
{
if (GameplayClock != null)
return GameplayClock;

if (Beatmap.Value.TrackLoaded)
return Beatmap.Value.Track;

return null;
}
}

protected override void Update()
{
ITrack track = null;
IBeatmap beatmap = null;

TimingControlPoint timingPoint;
EffectControlPoint effectPoint;

IClock clock = BeatSyncClock;

if (clock == null)
return;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true;

double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds;

if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded)
{
track = Beatmap.Value.Track;
beatmap = Beatmap.Value.Beatmap;
}

IsBeatSyncedWithTrack = beatmap != null && clock.IsRunning && track?.Length > 0;
double currentTrackTime;

if (IsBeatSyncedWithTrack)
{
Debug.Assert(beatmap != null);
Debug.Assert(BeatSyncSource.ControlPoints != null);
Debug.Assert(BeatSyncSource.Clock != null);

timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime);
effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime);
currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds;

timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime);
effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime);
}
else
{
// this may be the case where the beat syncing clock has been paused.
// we still want to show an idle animation, so use this container's time instead.
currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds;

timingPoint = TimingControlPoint.DEFAULT;
effectPoint = EffectControlPoint.DEFAULT;
}
Expand Down Expand Up @@ -162,7 +133,7 @@ protected override void Update()
if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE)
{
using (BeginDelayedSequence(-TimeSinceLastBeat))
OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty);
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty);
}

lastBeat = beatIndex;
Expand Down
3 changes: 2 additions & 1 deletion osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
Expand Up @@ -41,7 +41,8 @@ public abstract class LabelledDrawable<T> : CompositeDrawable

protected const float CONTENT_PADDING_VERTICAL = 10;
protected const float CONTENT_PADDING_HORIZONTAL = 15;
protected const float CORNER_RADIUS = 15;

public const float CORNER_RADIUS = 15;

/// <summary>
/// The component that is being displayed.
Expand Down
9 changes: 8 additions & 1 deletion osu.Game/OsuGameBase.cs
Expand Up @@ -10,6 +10,7 @@
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Extensions;
Expand All @@ -21,8 +22,10 @@
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Database;
Expand Down Expand Up @@ -52,7 +55,7 @@ namespace osu.Game
/// Unlike <see cref="OsuGame"/>, this class will not load any kind of UI, allowing it to be used
/// for provide dependencies to test cases without interfering with them.
/// </summary>
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
public const string OSU_PROTOCOL = "osu://";

Expand Down Expand Up @@ -552,5 +555,9 @@ protected override void Dispose(bool isDisposing)
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
}

ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo;
IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null;
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null;
}
}