mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:07:52 +08:00
Merge pull request #23976 from peppy/gameplay-sample-trigger-source-correctness
Adjust `GameplaySampleTriggerSource` to only switch samples when close enough to the next hit object
This commit is contained in:
commit
25842105ce
@ -6,7 +6,6 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -17,14 +16,13 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Taiko.UI;
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
{
|
{
|
||||||
public partial class TestSceneDrumSampleTriggerSource : OsuTestScene
|
public partial class TestSceneDrumSampleTriggerSource : OsuTestScene
|
||||||
{
|
{
|
||||||
private readonly ManualClock manualClock = new ManualClock();
|
|
||||||
|
|
||||||
[Cached(typeof(IScrollingInfo))]
|
[Cached(typeof(IScrollingInfo))]
|
||||||
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
|
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
|
||||||
{
|
{
|
||||||
@ -34,23 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
private ScrollingHitObjectContainer hitObjectContainer = null!;
|
private ScrollingHitObjectContainer hitObjectContainer = null!;
|
||||||
private TestDrumSampleTriggerSource triggerSource = null!;
|
private TestDrumSampleTriggerSource triggerSource = null!;
|
||||||
|
private readonly ManualClock manualClock = new TestManualClock();
|
||||||
|
private GameplayClockContainer gameplayClock = null!;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
hitObjectContainer = new ScrollingHitObjectContainer();
|
gameplayClock = new GameplayClockContainer(manualClock)
|
||||||
manualClock.CurrentTime = 0;
|
|
||||||
|
|
||||||
Child = new Container
|
|
||||||
{
|
{
|
||||||
Clock = new FramedClock(manualClock),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
hitObjectContainer,
|
hitObjectContainer = new ScrollingHitObjectContainer(),
|
||||||
triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer)
|
triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
gameplayClock.Reset(0);
|
||||||
|
|
||||||
|
hitObjectContainer.Clock = gameplayClock;
|
||||||
|
Child = gameplayClock;
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
@ -103,12 +103,67 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBetweenHits()
|
||||||
|
{
|
||||||
|
Hit first = null!, second = null!;
|
||||||
|
|
||||||
|
AddStep("add hit with normal samples", () =>
|
||||||
|
{
|
||||||
|
first = new Hit
|
||||||
|
{
|
||||||
|
StartTime = 100,
|
||||||
|
Samples = new List<HitSampleInfo>
|
||||||
|
{
|
||||||
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
first.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
var drawableHit = new DrawableHit(first);
|
||||||
|
hitObjectContainer.Add(drawableHit);
|
||||||
|
});
|
||||||
|
AddStep("add hit with soft samples", () =>
|
||||||
|
{
|
||||||
|
second = new Hit
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Samples = new List<HitSampleInfo>
|
||||||
|
{
|
||||||
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT),
|
||||||
|
new HitSampleInfo(HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
second.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
var drawableHit = new DrawableHit(second);
|
||||||
|
hitObjectContainer.Add(drawableHit);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
|
seekTo(120);
|
||||||
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
|
seekTo(480);
|
||||||
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
|
seekTo(700);
|
||||||
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDrumStrongHit()
|
public void TestDrumStrongHit()
|
||||||
{
|
{
|
||||||
@ -128,11 +183,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
hitObjectContainer.Add(drawableHit);
|
hitObjectContainer.Add(drawableHit);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is strong nested hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
|
|
||||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
@ -161,12 +216,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
@ -195,12 +250,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
@ -226,16 +281,16 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
hitObjectContainer.Add(drawableDrumRoll);
|
hitObjectContainer.Add(drawableDrumRoll);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
|
|
||||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
|
|
||||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
@ -260,16 +315,19 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
hitObjectContainer.Add(drawableSwell);
|
hitObjectContainer.Add(drawableSwell);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
|
||||||
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
@ -294,16 +352,19 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
hitObjectContainer.Add(drawableSwell);
|
hitObjectContainer.Add(drawableSwell);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
|
||||||
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
|
|
||||||
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
|
|
||||||
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||||
@ -316,6 +377,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
||||||
|
|
||||||
private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource
|
private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource
|
||||||
{
|
{
|
||||||
public ISampleInfo[]? LastPlayedSamples { get; private set; }
|
public ISampleInfo[]? LastPlayedSamples { get; private set; }
|
||||||
@ -331,7 +394,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
LastPlayedSamples = samples;
|
LastPlayedSamples = samples;
|
||||||
}
|
}
|
||||||
|
|
||||||
public new HitObject GetMostValidObject() => base.GetMostValidObject();
|
public new HitObject? GetMostValidObject() => base.GetMostValidObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestManualClock : ManualClock, IAdjustableClock
|
||||||
|
{
|
||||||
|
public TestManualClock()
|
||||||
|
{
|
||||||
|
IsRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start() => IsRunning = true;
|
||||||
|
|
||||||
|
public void Stop() => IsRunning = false;
|
||||||
|
|
||||||
|
public bool Seek(double position)
|
||||||
|
{
|
||||||
|
CurrentTime = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSpeedAdjustments()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -62,25 +63,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
new HitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
StartTime = t += spacing,
|
StartTime = t += spacing,
|
||||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||||
},
|
},
|
||||||
new HitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
StartTime = t += spacing,
|
StartTime = t += spacing,
|
||||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
|
||||||
},
|
},
|
||||||
new HitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
StartTime = t += spacing,
|
StartTime = t += spacing,
|
||||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
|
||||||
},
|
},
|
||||||
new HitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
StartTime = t += spacing,
|
StartTime = t += spacing,
|
||||||
},
|
},
|
||||||
new Slider
|
new Slider
|
||||||
{
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
StartTime = t += spacing,
|
StartTime = t += spacing,
|
||||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
||||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
|
||||||
@ -101,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
|
|
||||||
AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
|
AddStep("Add trigger source", () => Player.GameplayClockContainer.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -131,7 +137,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
|
AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
|
||||||
|
|
||||||
checkValidObjectIndex(1);
|
// next object is too far away, so we still use the already hit object.
|
||||||
|
checkValidObjectIndex(0);
|
||||||
|
|
||||||
|
// still too far away.
|
||||||
|
seekBeforeIndex(1, 400);
|
||||||
|
checkValidObjectIndex(0);
|
||||||
|
|
||||||
// Still object 1 as it's not hit yet.
|
// Still object 1 as it's not hit yet.
|
||||||
seekBeforeIndex(1);
|
seekBeforeIndex(1);
|
||||||
@ -168,9 +179,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
checkValidObjectIndex(4);
|
checkValidObjectIndex(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seekBeforeIndex(int index)
|
private void seekBeforeIndex(int index, double amount = 100)
|
||||||
{
|
{
|
||||||
AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100));
|
AddStep($"seek to {amount} ms before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - amount));
|
||||||
waitForCatchUp();
|
waitForCatchUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public new HitObject GetMostValidObject() => base.GetMostValidObject();
|
public new HitObject? GetMostValidObject() => base.GetMostValidObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
using System.Collections.Generic;
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.UI
|
namespace osu.Game.Rulesets.UI
|
||||||
@ -28,6 +29,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
private readonly Container<SkinnableSound> hitSounds;
|
private readonly Container<SkinnableSound> hitSounds;
|
||||||
|
|
||||||
|
private HitObjectLifetimeEntry? mostValidObject;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IGameplayClock? gameplayClock { get; set; }
|
||||||
|
|
||||||
public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
|
public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
|
||||||
{
|
{
|
||||||
this.hitObjectContainer = hitObjectContainer;
|
this.hitObjectContainer = hitObjectContainer;
|
||||||
@ -39,14 +45,12 @@ namespace osu.Game.Rulesets.UI
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private HitObjectLifetimeEntry fallbackObject;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Play the most appropriate hit sound for the current point in time.
|
/// Play the most appropriate hit sound for the current point in time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Play()
|
public virtual void Play()
|
||||||
{
|
{
|
||||||
var nextObject = GetMostValidObject();
|
HitObject? nextObject = GetMostValidObject();
|
||||||
|
|
||||||
if (nextObject == null)
|
if (nextObject == null)
|
||||||
return;
|
return;
|
||||||
@ -65,64 +69,61 @@ namespace osu.Game.Rulesets.UI
|
|||||||
hitSound.Play();
|
hitSound.Play();
|
||||||
});
|
});
|
||||||
|
|
||||||
protected HitObject GetMostValidObject()
|
protected HitObject? GetMostValidObject()
|
||||||
{
|
{
|
||||||
// The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
|
if (mostValidObject == null || isAlreadyHit(mostValidObject))
|
||||||
var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true);
|
|
||||||
|
|
||||||
if (drawableHitObject != null)
|
|
||||||
{
|
|
||||||
// A hit object may have a more valid nested object.
|
|
||||||
drawableHitObject = getMostValidNestedDrawable(drawableHitObject);
|
|
||||||
|
|
||||||
return drawableHitObject.HitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
|
|
||||||
// This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
|
|
||||||
if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
|
|
||||||
{
|
{
|
||||||
// We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
|
// We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
|
||||||
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
|
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
|
||||||
fallbackObject = hitObjectContainer.Entries
|
var candidate =
|
||||||
.Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime);
|
// Use alive entries first as an optimisation.
|
||||||
|
hitObjectContainer.AliveEntries.Select(tuple => tuple.Entry).Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime)
|
||||||
if (fallbackObject != null)
|
?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime);
|
||||||
return getEarliestNestedObject(fallbackObject.HitObject);
|
|
||||||
|
|
||||||
// In the case there are no non-judged objects, the last hit object should be used instead.
|
// In the case there are no non-judged objects, the last hit object should be used instead.
|
||||||
fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
|
if (candidate == null)
|
||||||
|
{
|
||||||
|
mostValidObject = hitObjectContainer.Entries.LastOrDefault();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (isCloseEnoughToCurrentTime(candidate.HitObject))
|
||||||
|
{
|
||||||
|
mostValidObject = candidate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mostValidObject ??= hitObjectContainer.Entries.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fallbackObject == null)
|
if (mostValidObject == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
bool fallbackHasResult = fallbackObject.Result?.HasResult == true;
|
|
||||||
|
|
||||||
// If the fallback has been judged then we want the sample from the object itself.
|
// If the fallback has been judged then we want the sample from the object itself.
|
||||||
if (fallbackHasResult)
|
if (isAlreadyHit(mostValidObject))
|
||||||
return fallbackObject.HitObject;
|
return mostValidObject.HitObject;
|
||||||
|
|
||||||
// Else we want the earliest (including nested).
|
// Else we want the earliest valid nested.
|
||||||
// In cases of nested objects, they will always have earlier sample data than their parent object.
|
// In cases of nested objects, they will always have earlier sample data than their parent object.
|
||||||
return getEarliestNestedObject(fallbackObject.HitObject);
|
return getAllNested(mostValidObject.HitObject).OrderBy(h => h.GetEndTime()).SkipWhile(h => h.GetEndTime() <= getReferenceTime()).FirstOrDefault() ?? mostValidObject.HitObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o)
|
private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true;
|
||||||
|
private bool isCloseEnoughToCurrentTime(HitObject h) => getReferenceTime() >= h.StartTime - h.HitWindows.WindowFor(HitResult.Miss) * 2;
|
||||||
|
|
||||||
|
private double getReferenceTime() => gameplayClock?.CurrentTime ?? Clock.CurrentTime;
|
||||||
|
|
||||||
|
private IEnumerable<HitObject> getAllNested(HitObject hitObject)
|
||||||
{
|
{
|
||||||
var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true);
|
foreach (var h in hitObject.NestedHitObjects)
|
||||||
|
{
|
||||||
|
yield return h;
|
||||||
|
|
||||||
if (nestedWithoutResult == null)
|
foreach (var n in getAllNested(h))
|
||||||
return o;
|
yield return n;
|
||||||
|
|
||||||
return getMostValidNestedDrawable(nestedWithoutResult);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HitObject getEarliestNestedObject(HitObject hitObject)
|
|
||||||
{
|
|
||||||
var nested = hitObject.NestedHitObjects.FirstOrDefault();
|
|
||||||
|
|
||||||
return nested != null ? getEarliestNestedObject(nested) : hitObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SkinnableSound getNextSample()
|
private SkinnableSound getNextSample()
|
||||||
|
Loading…
Reference in New Issue
Block a user