mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 12:17:46 +08:00
Merge pull request #10797 from smoogipoo/hitobject-pooling-base
Implement top-level hitobject pooling
This commit is contained in:
commit
8b2f7434a6
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
Position = new Vector2(128, 128),
|
Position = new Vector2(128, 128),
|
||||||
ComboIndex = 1,
|
ComboIndex = 1,
|
||||||
})));
|
}), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private HitCircle prepareObject(HitCircle circle)
|
private HitCircle prepareObject(HitCircle circle)
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new Vector2(300, 0),
|
new Vector2(300, 0),
|
||||||
}),
|
}),
|
||||||
RepeatCount = 1
|
RepeatCount = 1
|
||||||
})));
|
}), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Slider prepareObject(Slider slider)
|
private Slider prepareObject(Slider slider)
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
Position = new Vector2(256, 192),
|
Position = new Vector2(256, 192),
|
||||||
ComboIndex = 1,
|
ComboIndex = 1,
|
||||||
Duration = 1000,
|
Duration = 1000,
|
||||||
})));
|
}), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Spinner prepareObject(Spinner circle)
|
private Spinner prepareObject(Spinner circle)
|
||||||
|
@ -51,9 +51,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
|
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
|
||||||
|
|
||||||
state.BindTo(drawableObject.State);
|
state.BindTo(drawableObject.State);
|
||||||
state.BindValueChanged(updateState, true);
|
|
||||||
|
|
||||||
accentColour.BindTo(drawableObject.AccentColour);
|
accentColour.BindTo(drawableObject.AccentColour);
|
||||||
|
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
state.BindValueChanged(updateState, true);
|
||||||
accentColour.BindValueChanged(colour =>
|
accentColour.BindValueChanged(colour =>
|
||||||
{
|
{
|
||||||
explode.Colour = colour.NewValue;
|
explode.Colour = colour.NewValue;
|
||||||
@ -61,7 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
circle.Colour = colour.NewValue;
|
circle.Colour = colour.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
|
||||||
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
|
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
247
osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
Normal file
247
osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
public class TestScenePoolingRuleset : OsuTestScene
|
||||||
|
{
|
||||||
|
private const double time_between_objects = 1000;
|
||||||
|
|
||||||
|
private TestDrawablePoolingRuleset drawableRuleset;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReusedWithHitObjectsSpacedFarApart()
|
||||||
|
{
|
||||||
|
ManualClock clock = null;
|
||||||
|
|
||||||
|
createTest(new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitObject(),
|
||||||
|
new HitObject { StartTime = time_between_objects }
|
||||||
|
}
|
||||||
|
}, 1, () => new FramedClock(clock = new ManualClock()));
|
||||||
|
|
||||||
|
DrawableTestHitObject firstObject = null;
|
||||||
|
AddUntilStep("first object shown", () => this.ChildrenOfType<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]);
|
||||||
|
AddStep("get DHO", () => firstObject = this.ChildrenOfType<DrawableTestHitObject>().Single());
|
||||||
|
|
||||||
|
AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime);
|
||||||
|
|
||||||
|
AddUntilStep("second object shown", () => this.ChildrenOfType<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]);
|
||||||
|
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().Single() == firstObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotReusedWithHitObjectsSpacedClose()
|
||||||
|
{
|
||||||
|
ManualClock clock = null;
|
||||||
|
|
||||||
|
createTest(new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitObject(),
|
||||||
|
new HitObject { StartTime = 250 }
|
||||||
|
}
|
||||||
|
}, 2, () => new FramedClock(clock = new ManualClock()));
|
||||||
|
|
||||||
|
AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime);
|
||||||
|
|
||||||
|
AddUntilStep("two DHOs shown", () => this.ChildrenOfType<DrawableTestHitObject>().Count() == 2);
|
||||||
|
AddAssert("DHOs have different hitobjects",
|
||||||
|
() => this.ChildrenOfType<DrawableTestHitObject>().ElementAt(0).HitObject != this.ChildrenOfType<DrawableTestHitObject>().ElementAt(1).HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestManyHitObjects()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap();
|
||||||
|
|
||||||
|
for (int i = 0; i < 500; i++)
|
||||||
|
beatmap.HitObjects.Add(new HitObject { StartTime = i * 10 });
|
||||||
|
|
||||||
|
createTest(beatmap, 100);
|
||||||
|
|
||||||
|
AddUntilStep("any DHOs shown", () => this.ChildrenOfType<DrawableTestHitObject>().Any());
|
||||||
|
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null) => AddStep("create test", () =>
|
||||||
|
{
|
||||||
|
var ruleset = new TestPoolingRuleset();
|
||||||
|
|
||||||
|
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||||
|
drawableRuleset.FrameStablePlayback = true;
|
||||||
|
drawableRuleset.PoolSize = poolSize;
|
||||||
|
|
||||||
|
Child = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Clock = createClock?.Invoke() ?? new FramedOffsetClock(Clock, false) { Offset = -Clock.CurrentTime },
|
||||||
|
Child = drawableRuleset
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
#region Ruleset
|
||||||
|
|
||||||
|
private class TestPoolingRuleset : Ruleset
|
||||||
|
{
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new TestDrawablePoolingRuleset(this, beatmap, mods);
|
||||||
|
|
||||||
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, this);
|
||||||
|
|
||||||
|
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override string Description { get; } = string.Empty;
|
||||||
|
|
||||||
|
public override string ShortName { get; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDrawablePoolingRuleset : DrawableRuleset<TestHitObject>
|
||||||
|
{
|
||||||
|
public int PoolSize;
|
||||||
|
|
||||||
|
public TestDrawablePoolingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||||
|
: base(ruleset, beatmap, mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
RegisterPool<TestHitObject, DrawableTestHitObject>(PoolSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
|
||||||
|
|
||||||
|
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h) => null;
|
||||||
|
|
||||||
|
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
|
||||||
|
|
||||||
|
protected override Playfield CreatePlayfield() => new TestPlayfield();
|
||||||
|
|
||||||
|
private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry
|
||||||
|
{
|
||||||
|
public TestHitObjectLifetimeEntry(HitObject hitObject)
|
||||||
|
: base(hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double InitialLifetimeOffset => 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPlayfield : Playfield
|
||||||
|
{
|
||||||
|
public TestPlayfield()
|
||||||
|
{
|
||||||
|
AddInternal(HitObjectContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override GameplayCursorContainer CreateCursor() => null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestBeatmapConverter : BeatmapConverter<TestHitObject>
|
||||||
|
{
|
||||||
|
public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||||
|
: base(beatmap, ruleset)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanConvert() => true;
|
||||||
|
|
||||||
|
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
yield return new TestHitObject
|
||||||
|
{
|
||||||
|
StartTime = original.StartTime,
|
||||||
|
Duration = 250
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region HitObject
|
||||||
|
|
||||||
|
private class TestHitObject : ConvertHitObject
|
||||||
|
{
|
||||||
|
public double EndTime => StartTime + Duration;
|
||||||
|
|
||||||
|
public double Duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DrawableTestHitObject : DrawableHitObject<TestHitObject>
|
||||||
|
{
|
||||||
|
public DrawableTestHitObject()
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
|
||||||
|
Size = new Vector2(50, 50);
|
||||||
|
|
||||||
|
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddInternal(new Circle
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
|
{
|
||||||
|
if (timeOffset > HitObject.Duration)
|
||||||
|
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||||
|
{
|
||||||
|
base.UpdateHitStateTransforms(state);
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ArmedState.Hit:
|
||||||
|
case ArmedState.Miss:
|
||||||
|
this.FadeOut(250);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
@ -108,7 +109,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
protected virtual float SamplePlaybackPosition => 0.5f;
|
protected virtual float SamplePlaybackPosition => 0.5f;
|
||||||
|
|
||||||
private readonly Bindable<double> startTimeBindable = new Bindable<double>();
|
public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
|
||||||
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
|
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
|
||||||
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
|
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
|
||||||
private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
|
private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
|
||||||
@ -128,6 +129,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool hasHitObjectApplied;
|
private bool hasHitObjectApplied;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the currently-attached <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
private HitObjectLifetimeEntry lifetimeEntry;
|
||||||
|
|
||||||
|
private Container<PausableSkinnableSound> samplesContainer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="DrawableHitObject"/>.
|
/// Creates a new <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -144,6 +153,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
|
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
|
||||||
|
|
||||||
|
// Explicit non-virtual function call.
|
||||||
|
base.AddInternal(samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadAsyncComplete()
|
protected override void LoadAsyncComplete()
|
||||||
@ -151,14 +163,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
base.LoadAsyncComplete();
|
base.LoadAsyncComplete();
|
||||||
|
|
||||||
if (HitObject != null)
|
if (HitObject != null)
|
||||||
Apply(HitObject);
|
Apply(HitObject, lifetimeEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
|
|
||||||
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
|
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
|
||||||
|
|
||||||
updateState(ArmedState.Idle, true);
|
updateState(ArmedState.Idle, true);
|
||||||
@ -168,16 +179,33 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
|
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
|
/// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
|
||||||
public void Apply(HitObject hitObject)
|
/// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
|
||||||
|
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
|
||||||
{
|
{
|
||||||
free();
|
free();
|
||||||
|
|
||||||
HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}.");
|
HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}.");
|
||||||
|
|
||||||
|
this.lifetimeEntry = lifetimeEntry;
|
||||||
|
|
||||||
|
if (lifetimeEntry != null)
|
||||||
|
{
|
||||||
|
// Transfer lifetime from the entry.
|
||||||
|
LifetimeStart = lifetimeEntry.LifetimeStart;
|
||||||
|
LifetimeEnd = lifetimeEntry.LifetimeEnd;
|
||||||
|
|
||||||
|
// Copy any existing result from the entry (required for rewind / judgement revert).
|
||||||
|
Result = lifetimeEntry.Result;
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure this DHO has a result.
|
// Ensure this DHO has a result.
|
||||||
Result ??= CreateResult(HitObject.CreateJudgement())
|
Result ??= CreateResult(HitObject.CreateJudgement())
|
||||||
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||||
|
|
||||||
|
// Copy back the result to the entry for potential future retrieval.
|
||||||
|
if (lifetimeEntry != null)
|
||||||
|
lifetimeEntry.Result = Result;
|
||||||
|
|
||||||
foreach (var h in HitObject.NestedHitObjects)
|
foreach (var h in HitObject.NestedHitObjects)
|
||||||
{
|
{
|
||||||
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
|
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
|
||||||
@ -190,7 +218,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
AddNestedHitObject(drawableNested);
|
AddNestedHitObject(drawableNested);
|
||||||
}
|
}
|
||||||
|
|
||||||
startTimeBindable.BindTo(HitObject.StartTimeBindable);
|
StartTimeBindable.BindTo(HitObject.StartTimeBindable);
|
||||||
|
StartTimeBindable.BindValueChanged(onStartTimeChanged);
|
||||||
|
|
||||||
if (HitObject is IHasComboInformation combo)
|
if (HitObject is IHasComboInformation combo)
|
||||||
comboIndexBindable.BindTo(combo.ComboIndexBindable);
|
comboIndexBindable.BindTo(combo.ComboIndexBindable);
|
||||||
|
|
||||||
@ -217,12 +247,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
if (!hasHitObjectApplied)
|
if (!hasHitObjectApplied)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
startTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||||
if (HitObject is IHasComboInformation combo)
|
if (HitObject is IHasComboInformation combo)
|
||||||
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
|
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
|
||||||
|
|
||||||
samplesBindable.UnbindFrom(HitObject.SamplesBindable);
|
samplesBindable.UnbindFrom(HitObject.SamplesBindable);
|
||||||
|
|
||||||
|
// Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.
|
||||||
|
StartTimeBindable.ValueChanged -= onStartTimeChanged;
|
||||||
|
|
||||||
// When a new hitobject is applied, the samples will be cleared before re-populating.
|
// When a new hitobject is applied, the samples will be cleared before re-populating.
|
||||||
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
|
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
|
||||||
samplesBindable.CollectionChanged -= onSamplesChanged;
|
samplesBindable.CollectionChanged -= onSamplesChanged;
|
||||||
@ -245,6 +277,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
OnFree(HitObject);
|
OnFree(HitObject);
|
||||||
|
|
||||||
HitObject = null;
|
HitObject = null;
|
||||||
|
lifetimeEntry = null;
|
||||||
|
|
||||||
hasHitObjectApplied = false;
|
hasHitObjectApplied = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,11 +314,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void LoadSamples()
|
protected virtual void LoadSamples()
|
||||||
{
|
{
|
||||||
if (Samples != null)
|
samplesContainer.Clear();
|
||||||
{
|
|
||||||
RemoveInternal(Samples);
|
|
||||||
Samples = null;
|
Samples = null;
|
||||||
}
|
|
||||||
|
|
||||||
var samples = GetSamples().ToArray();
|
var samples = GetSamples().ToArray();
|
||||||
|
|
||||||
@ -297,12 +328,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
|
samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
|
||||||
AddInternal(Samples);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
|
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
|
||||||
|
|
||||||
|
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => updateState(State.Value, true);
|
||||||
|
|
||||||
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
|
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
|
||||||
|
|
||||||
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
|
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result);
|
||||||
@ -311,7 +343,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
|
|
||||||
private void onDefaultsApplied(HitObject hitObject)
|
private void onDefaultsApplied(HitObject hitObject)
|
||||||
{
|
{
|
||||||
Apply(hitObject);
|
Apply(hitObject, lifetimeEntry);
|
||||||
DefaultsApplied?.Invoke(this);
|
DefaultsApplied?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,15 +590,27 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
||||||
|
|
||||||
private double? lifetimeStart;
|
|
||||||
|
|
||||||
public override double LifetimeStart
|
public override double LifetimeStart
|
||||||
{
|
{
|
||||||
get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset);
|
get => base.LifetimeStart;
|
||||||
set
|
set => setLifetime(value, LifetimeEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double LifetimeEnd
|
||||||
{
|
{
|
||||||
lifetimeStart = value;
|
get => base.LifetimeEnd;
|
||||||
base.LifetimeStart = value;
|
set => setLifetime(LifetimeStart, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLifetime(double lifetimeStart, double lifetimeEnd)
|
||||||
|
{
|
||||||
|
base.LifetimeStart = lifetimeStart;
|
||||||
|
base.LifetimeEnd = lifetimeEnd;
|
||||||
|
|
||||||
|
if (lifetimeEntry != null)
|
||||||
|
{
|
||||||
|
lifetimeEntry.LifetimeStart = lifetimeStart;
|
||||||
|
lifetimeEntry.LifetimeEnd = lifetimeEnd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
110
osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs
Normal file
110
osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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 osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics.Performance;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="LifetimeEntry"/> that stores the lifetime for a <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class HitObjectLifetimeEntry : LifetimeEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly HitObject HitObject;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The result that <see cref="HitObject"/> was judged with.
|
||||||
|
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
|
||||||
|
/// </summary>
|
||||||
|
internal JudgementResult Result;
|
||||||
|
|
||||||
|
private readonly IBindable<double> startTimeBindable = new BindableDouble();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="HitObjectLifetimeEntry"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to store the lifetime of.</param>
|
||||||
|
public HitObjectLifetimeEntry(HitObject hitObject)
|
||||||
|
{
|
||||||
|
HitObject = hitObject;
|
||||||
|
|
||||||
|
startTimeBindable.BindTo(HitObject.StartTimeBindable);
|
||||||
|
startTimeBindable.BindValueChanged(onStartTimeChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lifetime start, as set by the hitobject.
|
||||||
|
private double realLifetimeStart = double.MinValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which the <see cref="HitObject"/> should become alive.
|
||||||
|
/// </summary>
|
||||||
|
public new double LifetimeStart
|
||||||
|
{
|
||||||
|
get => realLifetimeStart;
|
||||||
|
set => setLifetime(realLifetimeStart = value, LifetimeEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lifetime end, as set by the hitobject.
|
||||||
|
private double realLifetimeEnd = double.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which the <see cref="HitObject"/> should become dead.
|
||||||
|
/// </summary>
|
||||||
|
public new double LifetimeEnd
|
||||||
|
{
|
||||||
|
get => realLifetimeEnd;
|
||||||
|
set => setLifetime(LifetimeStart, realLifetimeEnd = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLifetime(double start, double end)
|
||||||
|
{
|
||||||
|
if (keepAlive)
|
||||||
|
{
|
||||||
|
start = double.MinValue;
|
||||||
|
end = double.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.LifetimeStart = start;
|
||||||
|
base.LifetimeEnd = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool keepAlive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the <see cref="HitObject"/> should be kept always alive.
|
||||||
|
/// </summary>
|
||||||
|
internal bool KeepAlive
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (keepAlive == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
keepAlive = value;
|
||||||
|
setLifetime(realLifetimeStart, realLifetimeEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A safe offset prior to the start time of <see cref="HitObject"/> at which it may begin displaying contents.
|
||||||
|
/// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is only used as an optimisation to delay the initial update of the <see cref="HitObject"/> and may be tuned more aggressively if required.
|
||||||
|
/// It is indirectly used to decide the automatic transform offset provided to <see cref="DrawableHitObject.UpdateInitialTransforms"/>.
|
||||||
|
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual double InitialLifetimeOffset => 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets <see cref="LifetimeStart"/> according to the change in start time of the <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,10 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Pooling;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -38,9 +41,8 @@ namespace osu.Game.Rulesets.UI
|
|||||||
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter
|
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter
|
||||||
where TObject : HitObject
|
where TObject : HitObject
|
||||||
{
|
{
|
||||||
public override event Action<JudgementResult> OnNewResult;
|
public override event Action<JudgementResult> NewResult;
|
||||||
|
public override event Action<JudgementResult> RevertResult;
|
||||||
public override event Action<JudgementResult> OnRevertResult;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The selected variant.
|
/// The selected variant.
|
||||||
@ -92,11 +94,8 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
protected IRulesetConfigManager Config { get; private set; }
|
protected IRulesetConfigManager Config { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The mods which are to be applied.
|
|
||||||
/// </summary>
|
|
||||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||||
protected readonly IReadOnlyList<Mod> Mods;
|
protected override IReadOnlyList<Mod> Mods { get; }
|
||||||
|
|
||||||
private FrameStabilityContainer frameStabilityContainer;
|
private FrameStabilityContainer frameStabilityContainer;
|
||||||
|
|
||||||
@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
KeyBindingInputManager = CreateInputManager();
|
KeyBindingInputManager = CreateInputManager();
|
||||||
playfield = new Lazy<Playfield>(CreatePlayfield);
|
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
|
||||||
|
{
|
||||||
|
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
||||||
|
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
||||||
|
}));
|
||||||
|
|
||||||
IsPaused.ValueChanged += paused =>
|
IsPaused.ValueChanged += paused =>
|
||||||
{
|
{
|
||||||
@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
RegenerateAutoplay();
|
RegenerateAutoplay();
|
||||||
|
|
||||||
loadObjects(cancellationToken);
|
loadObjects(cancellationToken ?? default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegenerateAutoplay()
|
public void RegenerateAutoplay()
|
||||||
@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and adds drawable representations of hit objects to the play field.
|
/// Creates and adds drawable representations of hit objects to the play field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void loadObjects(CancellationToken? cancellationToken)
|
private void loadObjects(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (TObject h in Beatmap.HitObjects)
|
foreach (TObject h in Beatmap.HitObjects)
|
||||||
{
|
{
|
||||||
cancellationToken?.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
addHitObject(h);
|
AddHitObject(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken?.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Playfield.PostProcess();
|
Playfield.PostProcess();
|
||||||
|
|
||||||
@ -231,22 +234,58 @@ namespace osu.Game.Rulesets.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates and adds the visual representation of a <typeparamref name="TObject"/> to this <see cref="DrawableRuleset{TObject}"/>.
|
/// Adds a <see cref="HitObject"/> to this <see cref="DrawableRuleset"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <typeparamref name="TObject"/> to add the visual representation for.</param>
|
/// <remarks>
|
||||||
private void addHitObject(TObject hitObject)
|
/// This does not add the <see cref="HitObject"/> to the beatmap.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
|
||||||
|
public void AddHitObject(TObject hitObject)
|
||||||
{
|
{
|
||||||
var drawableObject = CreateDrawableRepresentation(hitObject);
|
var drawableRepresentation = CreateDrawableRepresentation(hitObject);
|
||||||
|
|
||||||
if (drawableObject == null)
|
// If a drawable representation exists, use it, otherwise assume the hitobject is being pooled.
|
||||||
return;
|
if (drawableRepresentation != null)
|
||||||
|
Playfield.Add(drawableRepresentation);
|
||||||
drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r);
|
else
|
||||||
drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r);
|
Playfield.Add(GetLifetimeEntry(hitObject));
|
||||||
|
|
||||||
Playfield.Add(drawableObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a <see cref="HitObject"/> from this <see cref="DrawableRuleset"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This does not remove the <see cref="HitObject"/> from the beatmap.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
|
||||||
|
public bool RemoveHitObject(TObject hitObject)
|
||||||
|
{
|
||||||
|
var entry = GetLifetimeEntry(hitObject);
|
||||||
|
|
||||||
|
// May have been newly-created by the above call - remove it anyway.
|
||||||
|
RemoveLifetimeEntry(hitObject);
|
||||||
|
|
||||||
|
if (Playfield.Remove(entry))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal.
|
||||||
|
var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject);
|
||||||
|
if (drawableObject != null)
|
||||||
|
return Playfield.Remove(drawableObject);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject)
|
||||||
|
{
|
||||||
|
if (!(hitObject is TObject tHitObject))
|
||||||
|
throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}");
|
||||||
|
|
||||||
|
return CreateLifetimeEntry(tHitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject);
|
||||||
|
|
||||||
public override void SetRecordTarget(Replay recordingReplay)
|
public override void SetRecordTarget(Replay recordingReplay)
|
||||||
{
|
{
|
||||||
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
|
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
|
||||||
@ -285,10 +324,15 @@ namespace osu.Game.Rulesets.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a DrawableHitObject from a HitObject.
|
/// Creates a <see cref="DrawableHitObject{TObject}"/> to represent a <see cref="HitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="h">The HitObject to make drawable.</param>
|
/// <remarks>
|
||||||
/// <returns>The DrawableHitObject.</returns>
|
/// If this method returns <c>null</c>, then this <see cref="DrawableRuleset"/> will assume the requested <see cref="HitObject"/> type is being pooled,
|
||||||
|
/// and will instead attempt to retrieve the <see cref="DrawableHitObject"/>s at the point they should become alive via pools registered through
|
||||||
|
/// <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(int, int?)"/> or <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(DrawablePool{TDrawable})"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="h">The <see cref="HitObject"/> to represent.</param>
|
||||||
|
/// <returns>The representing <see cref="DrawableHitObject{TObject}"/>.</returns>
|
||||||
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
|
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
|
||||||
|
|
||||||
public void Attach(KeyCounterDisplay keyCounter) =>
|
public void Attach(KeyCounterDisplay keyCounter) =>
|
||||||
@ -361,20 +405,20 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// Displays an interactive ruleset gameplay instance.
|
/// Displays an interactive ruleset gameplay instance.
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This type is required only for adding non-generic type to the draw hierarchy.
|
/// This type is required only for adding non-generic type to the draw hierarchy.
|
||||||
/// Once IDrawable is a thing, this can also become an interface.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Cached(typeof(DrawableRuleset))]
|
||||||
public abstract class DrawableRuleset : CompositeDrawable
|
public abstract class DrawableRuleset : CompositeDrawable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when a <see cref="JudgementResult"/> has been applied by a <see cref="DrawableHitObject"/>.
|
/// Invoked when a <see cref="JudgementResult"/> has been applied by a <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract event Action<JudgementResult> OnNewResult;
|
public abstract event Action<JudgementResult> NewResult;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when a <see cref="JudgementResult"/> is being reverted by a <see cref="DrawableHitObject"/>.
|
/// Invoked when a <see cref="JudgementResult"/> is being reverted by a <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract event Action<JudgementResult> OnRevertResult;
|
public abstract event Action<JudgementResult> RevertResult;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a replay is currently loaded.
|
/// Whether a replay is currently loaded.
|
||||||
@ -406,6 +450,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract IFrameStableClock FrameStableClock { get; }
|
public abstract IFrameStableClock FrameStableClock { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mods which are to be applied.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract IReadOnlyList<Mod> Mods { get; }
|
||||||
|
|
||||||
/// <summary>~
|
/// <summary>~
|
||||||
/// The associated ruleset.
|
/// The associated ruleset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -500,6 +549,99 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// Invoked when the user requests to pause while the resume overlay is active.
|
/// Invoked when the user requests to pause while the resume overlay is active.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void CancelResume();
|
public abstract void CancelResume();
|
||||||
|
|
||||||
|
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
|
||||||
|
private readonly Dictionary<HitObject, HitObjectLifetimeEntry> lifetimeEntries = new Dictionary<HitObject, HitObjectLifetimeEntry>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
|
||||||
|
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
|
||||||
|
/// <param name="maximumSize">
|
||||||
|
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
|
||||||
|
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
|
||||||
|
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
|
||||||
|
/// </param>
|
||||||
|
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
||||||
|
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
||||||
|
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
||||||
|
where TObject : HitObject
|
||||||
|
where TDrawable : DrawableHitObject, new()
|
||||||
|
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
|
||||||
|
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
|
||||||
|
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
||||||
|
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
||||||
|
protected void RegisterPool<TObject, TDrawable>([NotNull] DrawablePool<TDrawable> pool)
|
||||||
|
where TObject : HitObject
|
||||||
|
where TDrawable : DrawableHitObject, new()
|
||||||
|
{
|
||||||
|
pools[typeof(TObject)] = pool;
|
||||||
|
AddInternal(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve the poolable <see cref="DrawableHitObject"/> representation of a <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve the <see cref="DrawableHitObject"/> representation of.</param>
|
||||||
|
/// <returns>The <see cref="DrawableHitObject"/> representing <see cref="HitObject"/>, or <c>null</c> if no poolable representation exists.</returns>
|
||||||
|
[CanBeNull]
|
||||||
|
public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject)
|
||||||
|
{
|
||||||
|
if (!pools.TryGetValue(hitObject.GetType(), out var pool))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (DrawableHitObject)pool.Get(d =>
|
||||||
|
{
|
||||||
|
var dho = (DrawableHitObject)d;
|
||||||
|
|
||||||
|
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods.
|
||||||
|
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
||||||
|
if (!dho.IsLoaded)
|
||||||
|
{
|
||||||
|
foreach (var m in Mods.OfType<IApplicableToDrawableHitObjects>())
|
||||||
|
m.ApplyToDrawableHitObjects(dho.Yield());
|
||||||
|
}
|
||||||
|
|
||||||
|
dho.Apply(hitObject, GetLifetimeEntry(hitObject));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
|
||||||
|
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
|
||||||
|
[NotNull]
|
||||||
|
protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves or creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve or create the <see cref="HitObjectLifetimeEntry"/> for.</param>
|
||||||
|
/// <returns>The <see cref="HitObjectLifetimeEntry"/> for <paramref name="hitObject"/>.</returns>
|
||||||
|
[NotNull]
|
||||||
|
protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject)
|
||||||
|
{
|
||||||
|
if (lifetimeEntries.TryGetValue(hitObject, out var entry))
|
||||||
|
return entry;
|
||||||
|
|
||||||
|
return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the <see cref="HitObjectLifetimeEntry"/> for a <see cref="HitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to remove the <see cref="HitObjectLifetimeEntry"/> for.</param>
|
||||||
|
internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BeatmapInvalidForRulesetException : ArgumentException
|
public class BeatmapInvalidForRulesetException : ArgumentException
|
||||||
|
@ -1,33 +1,120 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Performance;
|
using osu.Framework.Graphics.Performance;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.UI
|
namespace osu.Game.Rulesets.UI
|
||||||
{
|
{
|
||||||
public class HitObjectContainer : LifetimeManagementContainer
|
public class HitObjectContainer : LifetimeManagementContainer
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All currently in-use <see cref="DrawableHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All currently in-use <see cref="DrawableHitObject"/>s that are alive.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this is equivalent to <see cref="Objects"/>.
|
||||||
|
/// </remarks>
|
||||||
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||||
|
|
||||||
private readonly Dictionary<DrawableHitObject, (IBindable<double> bindable, double timeAtAdd)> startTimeMap = new Dictionary<DrawableHitObject, (IBindable<double>, double)>();
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<DrawableHitObject, JudgementResult> NewResult;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<DrawableHitObject, JudgementResult> RevertResult;
|
||||||
|
|
||||||
|
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
|
||||||
|
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> drawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||||
|
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private DrawableRuleset drawableRuleset { get; set; }
|
||||||
|
|
||||||
public HitObjectContainer()
|
public HitObjectContainer()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||||
|
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadAsyncComplete()
|
||||||
|
{
|
||||||
|
base.LoadAsyncComplete();
|
||||||
|
|
||||||
|
// Application of hitobjects during load() may have changed their start times, so ensure the correct sorting order.
|
||||||
|
SortInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Pooling support
|
||||||
|
|
||||||
|
public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
|
||||||
|
|
||||||
|
public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
|
||||||
|
|
||||||
|
private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry);
|
||||||
|
|
||||||
|
private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry);
|
||||||
|
|
||||||
|
private void addDrawable(HitObjectLifetimeEntry entry)
|
||||||
|
{
|
||||||
|
Debug.Assert(!drawableMap.ContainsKey(entry));
|
||||||
|
|
||||||
|
var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject);
|
||||||
|
if (drawable == null)
|
||||||
|
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
|
||||||
|
|
||||||
|
drawable.OnNewResult += onNewResult;
|
||||||
|
drawable.OnRevertResult += onRevertResult;
|
||||||
|
|
||||||
|
bindStartTime(drawable);
|
||||||
|
AddInternal(drawableMap[entry] = drawable, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeDrawable(HitObjectLifetimeEntry entry)
|
||||||
|
{
|
||||||
|
Debug.Assert(drawableMap.ContainsKey(entry));
|
||||||
|
|
||||||
|
var drawable = drawableMap[entry];
|
||||||
|
drawable.OnNewResult -= onNewResult;
|
||||||
|
drawable.OnRevertResult -= onRevertResult;
|
||||||
|
drawable.OnKilled();
|
||||||
|
|
||||||
|
drawableMap.Remove(entry);
|
||||||
|
|
||||||
|
unbindStartTime(drawable);
|
||||||
|
RemoveInternal(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Non-pooling support
|
||||||
|
|
||||||
public virtual void Add(DrawableHitObject hitObject)
|
public virtual void Add(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
// Added first for the comparer to remain ordered during AddInternal
|
bindStartTime(hitObject);
|
||||||
startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime);
|
|
||||||
startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject));
|
hitObject.OnNewResult += onNewResult;
|
||||||
|
hitObject.OnRevertResult += onRevertResult;
|
||||||
|
|
||||||
AddInternal(hitObject);
|
AddInternal(hitObject);
|
||||||
}
|
}
|
||||||
@ -37,54 +124,16 @@ namespace osu.Game.Rulesets.UI
|
|||||||
if (!RemoveInternal(hitObject))
|
if (!RemoveInternal(hitObject))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Removed last for the comparer to remain ordered during RemoveInternal
|
hitObject.OnNewResult -= onNewResult;
|
||||||
startTimeMap[hitObject].bindable.UnbindAll();
|
hitObject.OnRevertResult -= onRevertResult;
|
||||||
startTimeMap.Remove(hitObject);
|
|
||||||
|
unbindStartTime(hitObject);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
unbindStartTimeMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void Clear(bool disposeChildren = true)
|
|
||||||
{
|
|
||||||
ClearInternal(disposeChildren);
|
|
||||||
unbindStartTimeMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unbindStartTimeMap()
|
|
||||||
{
|
|
||||||
foreach (var kvp in startTimeMap)
|
|
||||||
kvp.Value.bindable.UnbindAll();
|
|
||||||
startTimeMap.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
|
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
|
||||||
|
|
||||||
private void onStartTimeChanged(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
if (!RemoveInternal(hitObject))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Update the stored time, preserving the existing bindable
|
|
||||||
startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime);
|
|
||||||
AddInternal(hitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override int Compare(Drawable x, Drawable y)
|
|
||||||
{
|
|
||||||
if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj))
|
|
||||||
return base.Compare(x, y);
|
|
||||||
|
|
||||||
// Put earlier hitobjects towards the end of the list, so they handle input first
|
|
||||||
int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd);
|
|
||||||
return i == 0 ? CompareReverseChildID(x, y) : i;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
|
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
|
||||||
{
|
{
|
||||||
if (!(e.Child is DrawableHitObject hitObject))
|
if (!(e.Child is DrawableHitObject hitObject))
|
||||||
@ -96,5 +145,71 @@ namespace osu.Game.Rulesets.UI
|
|||||||
hitObject.OnKilled();
|
hitObject.OnKilled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public virtual void Clear(bool disposeChildren = true)
|
||||||
|
{
|
||||||
|
lifetimeManager.ClearEntries();
|
||||||
|
|
||||||
|
ClearInternal(disposeChildren);
|
||||||
|
unbindAllStartTimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CheckChildrenLife()
|
||||||
|
{
|
||||||
|
bool aliveChanged = base.CheckChildrenLife();
|
||||||
|
aliveChanged |= lifetimeManager.Update(Time.Current, Time.Current);
|
||||||
|
return aliveChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
|
||||||
|
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);
|
||||||
|
|
||||||
|
#region Comparator + StartTime tracking
|
||||||
|
|
||||||
|
private void bindStartTime(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
var bindable = hitObject.StartTimeBindable.GetBoundCopy();
|
||||||
|
|
||||||
|
bindable.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (LoadState >= LoadState.Ready)
|
||||||
|
SortInternal();
|
||||||
|
});
|
||||||
|
|
||||||
|
startTimeMap[hitObject] = bindable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unbindStartTime(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
startTimeMap[hitObject].UnbindAll();
|
||||||
|
startTimeMap.Remove(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unbindAllStartTimes()
|
||||||
|
{
|
||||||
|
foreach (var kvp in startTimeMap)
|
||||||
|
kvp.Value.UnbindAll();
|
||||||
|
startTimeMap.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int Compare(Drawable x, Drawable y)
|
||||||
|
{
|
||||||
|
if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj))
|
||||||
|
return base.Compare(x, y);
|
||||||
|
|
||||||
|
// Put earlier hitobjects towards the end of the list, so they handle input first
|
||||||
|
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
|
||||||
|
return i == 0 ? CompareReverseChildID(x, y) : i;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
unbindAllStartTimes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,25 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.UI
|
namespace osu.Game.Rulesets.UI
|
||||||
{
|
{
|
||||||
public abstract class Playfield : CompositeDrawable
|
public abstract class Playfield : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<DrawableHitObject, JudgementResult> NewResult;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<DrawableHitObject, JudgementResult> RevertResult;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
|
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -72,7 +84,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
hitObjectContainerLazy = new Lazy<HitObjectContainer>(CreateHitObjectContainer);
|
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
|
||||||
|
{
|
||||||
|
h.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
||||||
|
h.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
@ -101,13 +117,71 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// Adds a DrawableHitObject to this Playfield.
|
/// Adds a DrawableHitObject to this Playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="h">The DrawableHitObject to add.</param>
|
/// <param name="h">The DrawableHitObject to add.</param>
|
||||||
public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h);
|
public virtual void Add(DrawableHitObject h)
|
||||||
|
{
|
||||||
|
HitObjectContainer.Add(h);
|
||||||
|
OnHitObjectAdded(h.HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remove a DrawableHitObject from this Playfield.
|
/// Remove a DrawableHitObject from this Playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="h">The DrawableHitObject to remove.</param>
|
/// <param name="h">The DrawableHitObject to remove.</param>
|
||||||
public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
|
public virtual bool Remove(DrawableHitObject h)
|
||||||
|
{
|
||||||
|
if (!HitObjectContainer.Remove(h))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
OnHitObjectRemoved(h.HitObject);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
|
||||||
|
public virtual void Add(HitObjectLifetimeEntry entry)
|
||||||
|
{
|
||||||
|
HitObjectContainer.Add(entry);
|
||||||
|
OnHitObjectAdded(entry.HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
|
||||||
|
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
|
||||||
|
public virtual bool Remove(HitObjectLifetimeEntry entry)
|
||||||
|
{
|
||||||
|
if (HitObjectContainer.Remove(entry))
|
||||||
|
{
|
||||||
|
OnHitObjectRemoved(entry.HitObject);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removedFromNested = false;
|
||||||
|
|
||||||
|
if (nestedPlayfields.IsValueCreated)
|
||||||
|
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry));
|
||||||
|
|
||||||
|
return removedFromNested;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="Playfield"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The added <see cref="HitObject"/>.</param>
|
||||||
|
protected virtual void OnHitObjectAdded(HitObject hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="Playfield"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The removed <see cref="HitObject"/>.</param>
|
||||||
|
protected virtual void OnHitObjectRemoved(HitObject hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
|
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
|
||||||
@ -131,6 +205,10 @@ namespace osu.Game.Rulesets.UI
|
|||||||
protected void AddNested(Playfield otherPlayfield)
|
protected void AddNested(Playfield otherPlayfield)
|
||||||
{
|
{
|
||||||
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
|
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
|
||||||
|
|
||||||
|
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
||||||
|
otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
|
||||||
|
|
||||||
nestedPlayfields.Value.Add(otherPlayfield);
|
nestedPlayfields.Value.Add(otherPlayfield);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play
|
|||||||
// bind clock into components that require it
|
// bind clock into components that require it
|
||||||
DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
|
DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
|
||||||
|
|
||||||
DrawableRuleset.OnNewResult += r =>
|
DrawableRuleset.NewResult += r =>
|
||||||
{
|
{
|
||||||
HealthProcessor.ApplyResult(r);
|
HealthProcessor.ApplyResult(r);
|
||||||
ScoreProcessor.ApplyResult(r);
|
ScoreProcessor.ApplyResult(r);
|
||||||
gameplayBeatmap.ApplyResult(r);
|
gameplayBeatmap.ApplyResult(r);
|
||||||
};
|
};
|
||||||
|
|
||||||
DrawableRuleset.OnRevertResult += r =>
|
DrawableRuleset.RevertResult += r =>
|
||||||
{
|
{
|
||||||
HealthProcessor.RevertResult(r);
|
HealthProcessor.RevertResult(r);
|
||||||
ScoreProcessor.RevertResult(r);
|
ScoreProcessor.RevertResult(r);
|
||||||
|
Loading…
Reference in New Issue
Block a user