mirror of
https://github.com/osukey/osukey.git
synced 2025-05-30 01:47:30 +09:00
Merge branch 'master' into settings-reduce-visual-clutter
This commit is contained in:
commit
c0a9d88a14
@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
|
|
||||||
public void UpdateResult() => base.UpdateResult(true);
|
public void UpdateResult() => base.UpdateResult(true);
|
||||||
|
|
||||||
|
protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject.HitWindows != null);
|
Debug.Assert(HitObject.HitWindows != null);
|
||||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
|
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
|
||||||
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
maniaObject.CheckHittable = hitPolicy.IsHittable;
|
||||||
|
|
||||||
HitObjectContainer.Add(hitObject);
|
base.Add(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Remove(DrawableHitObject h)
|
public override bool Remove(DrawableHitObject h)
|
||||||
|
@ -9,9 +9,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
@ -61,13 +59,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
PositionBindable.BindTo(HitObject.PositionBindable);
|
PositionBindable.BindTo(HitObject.PositionBindable);
|
||||||
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
||||||
ScaleBindable.BindTo(HitObject.ScaleBindable);
|
ScaleBindable.BindTo(HitObject.ScaleBindable);
|
||||||
|
|
||||||
// Manually set to reduce the number of future alive objects to a bare minimum.
|
|
||||||
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
|
|
||||||
|
|
||||||
// Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
|
|
||||||
// An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
|
|
||||||
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFree()
|
protected override void OnFree()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
public class DrawableSpinnerTick : DrawableOsuHitObject
|
public class DrawableSpinnerTick : DrawableOsuHitObject
|
||||||
@ -17,6 +19,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DrawableSpinner drawableSpinner;
|
||||||
|
|
||||||
|
protected override void OnParentReceived(DrawableHitObject parent)
|
||||||
|
{
|
||||||
|
base.OnParentReceived(parent);
|
||||||
|
drawableSpinner = (DrawableSpinner)parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply a judgement result.
|
/// Apply a judgement result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -176,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
public OsuHitObjectLifetimeEntry(HitObject hitObject)
|
public OsuHitObjectLifetimeEntry(HitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
|
// Prevent past objects in idles states from remaining alive as their end times are skipped in non-frame-stable contexts.
|
||||||
|
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt;
|
protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt;
|
||||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
Filled = HitObject.FirstTick
|
Filled = HitObject.FirstTick
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected override double MaximumJudgementOffset => HitObject.HitWindow;
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (!userTriggered)
|
if (!userTriggered)
|
||||||
|
@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Legacy;
|
|
||||||
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.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -46,6 +46,50 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() => testClock.CurrentTime = 0);
|
public void Setup() => Schedule(() => testClock.CurrentTime = 0);
|
||||||
|
|
||||||
|
[TestCase("pooled")]
|
||||||
|
[TestCase("non-pooled")]
|
||||||
|
public void TestHitObjectLifetime(string pooled)
|
||||||
|
{
|
||||||
|
var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
|
createTest(beatmap);
|
||||||
|
|
||||||
|
assertPosition(0, 0f);
|
||||||
|
assertDead(3);
|
||||||
|
|
||||||
|
setTime(3 * time_range);
|
||||||
|
assertPosition(3, 0f);
|
||||||
|
assertDead(0);
|
||||||
|
|
||||||
|
setTime(0 * time_range);
|
||||||
|
assertPosition(0, 0f);
|
||||||
|
assertDead(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("pooled")]
|
||||||
|
[TestCase("non-pooled")]
|
||||||
|
public void TestNestedHitObject(string pooled)
|
||||||
|
{
|
||||||
|
var beatmap = createBeatmap(i =>
|
||||||
|
{
|
||||||
|
var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject();
|
||||||
|
h.Duration = 300;
|
||||||
|
h.ChildTimeOffset = i % 3 * 100;
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
||||||
|
createTest(beatmap);
|
||||||
|
|
||||||
|
assertPosition(0, 0f);
|
||||||
|
assertHeight(0);
|
||||||
|
assertChildPosition(0);
|
||||||
|
|
||||||
|
setTime(5 * time_range);
|
||||||
|
assertPosition(5, 0f);
|
||||||
|
assertHeight(5);
|
||||||
|
assertChildPosition(5);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
||||||
{
|
{
|
||||||
@ -147,8 +191,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
assertPosition(1, 1);
|
assertPosition(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a <see cref="DrawableTestHitObject" /> corresponding to the <paramref name="index"/>'th <see cref="TestHitObject"/>.
|
||||||
|
/// When the hit object is not alive, `null` is returned.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
private DrawableTestHitObject getDrawableHitObject(int index)
|
||||||
|
{
|
||||||
|
var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index);
|
||||||
|
return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight;
|
||||||
|
|
||||||
|
private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null);
|
||||||
|
|
||||||
|
private void assertHeight(int index) => AddAssert($"hitobject {index} height", () =>
|
||||||
|
{
|
||||||
|
var d = getDrawableHitObject(index);
|
||||||
|
return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () =>
|
||||||
|
{
|
||||||
|
var d = getDrawableHitObject(index);
|
||||||
|
return d is DrawableTestParentHitObject && Precision.AlmostEquals(
|
||||||
|
d.NestedHitObjects.First().DrawPosition.Y,
|
||||||
|
yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f);
|
||||||
|
});
|
||||||
|
|
||||||
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
|
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
|
||||||
() => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
|
() => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY));
|
||||||
|
|
||||||
private void setTime(double time)
|
private void setTime(double time)
|
||||||
{
|
{
|
||||||
@ -160,12 +233,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
||||||
private IBeatmap createBeatmap()
|
private IBeatmap createBeatmap(Func<int, TestHitObject> createAction = null)
|
||||||
{
|
{
|
||||||
var beatmap = new Beatmap<HitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
var beatmap = new Beatmap<TestHitObject> { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
|
{
|
||||||
|
var h = createAction?.Invoke(i) ?? new TestHitObject();
|
||||||
|
h.StartTime = i * time_range;
|
||||||
|
beatmap.HitObjects.Add(h);
|
||||||
|
}
|
||||||
|
|
||||||
return beatmap;
|
return beatmap;
|
||||||
}
|
}
|
||||||
@ -225,7 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
TimeRange.Value = time_range;
|
TimeRange.Value = time_range;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h);
|
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h)
|
||||||
|
{
|
||||||
|
switch (h)
|
||||||
|
{
|
||||||
|
case TestPooledHitObject _:
|
||||||
|
case TestPooledParentHitObject _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case TestParentHitObject p:
|
||||||
|
return new DrawableTestParentHitObject(p);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new DrawableTestHitObject(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
|
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
|
||||||
|
|
||||||
@ -265,6 +356,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RegisterPool<TestPooledHitObject, DrawableTestPooledHitObject>(1);
|
||||||
|
RegisterPool<TestPooledParentHitObject, DrawableTestPooledParentHitObject>(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,30 +371,46 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
public override bool CanConvert() => true;
|
public override bool CanConvert() => true;
|
||||||
|
|
||||||
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) =>
|
||||||
{
|
throw new NotImplementedException();
|
||||||
yield return new TestHitObject
|
|
||||||
{
|
|
||||||
StartTime = original.StartTime,
|
|
||||||
Duration = (original as IHasDuration)?.Duration ?? 100
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region HitObject
|
#region HitObject
|
||||||
|
|
||||||
private class TestHitObject : ConvertHitObject, IHasDuration
|
private class TestHitObject : HitObject, IHasDuration
|
||||||
{
|
{
|
||||||
public double EndTime => StartTime + Duration;
|
public double EndTime => StartTime + Duration;
|
||||||
|
|
||||||
public double Duration { get; set; }
|
public double Duration { get; set; } = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPooledHitObject : TestHitObject
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestParentHitObject : TestHitObject
|
||||||
|
{
|
||||||
|
public double ChildTimeOffset;
|
||||||
|
|
||||||
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPooledParentHitObject : TestParentHitObject
|
||||||
|
{
|
||||||
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DrawableTestHitObject : DrawableHitObject<TestHitObject>
|
private class DrawableTestHitObject : DrawableHitObject<TestHitObject>
|
||||||
{
|
{
|
||||||
public DrawableTestHitObject(TestHitObject hitObject)
|
public DrawableTestHitObject([CanBeNull] TestHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre;
|
Anchor = Anchor.TopCentre;
|
||||||
@ -324,6 +434,52 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update() => LifetimeEnd = HitObject.EndTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DrawableTestPooledHitObject : DrawableTestHitObject
|
||||||
|
{
|
||||||
|
public DrawableTestPooledHitObject()
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
InternalChildren[0].Colour = Color4.LightSkyBlue;
|
||||||
|
InternalChildren[1].Colour = Color4.Blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DrawableTestParentHitObject : DrawableTestHitObject
|
||||||
|
{
|
||||||
|
private readonly Container<DrawableHitObject> container;
|
||||||
|
|
||||||
|
public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject)
|
||||||
|
: base(hitObject)
|
||||||
|
{
|
||||||
|
InternalChildren[0].Colour = Color4.LightYellow;
|
||||||
|
InternalChildren[1].Colour = Color4.Yellow;
|
||||||
|
|
||||||
|
AddInternal(container = new Container<DrawableHitObject>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) =>
|
||||||
|
new DrawableTestHitObject((TestHitObject)hitObject);
|
||||||
|
|
||||||
|
protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject);
|
||||||
|
|
||||||
|
protected override void ClearNestedHitObjects() => container.Clear(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
|
||||||
|
{
|
||||||
|
public DrawableTestPooledParentHitObject()
|
||||||
|
: base(null)
|
||||||
|
{
|
||||||
|
InternalChildren[0].Colour = Color4.LightSeaGreen;
|
||||||
|
InternalChildren[1].Colour = Color4.Green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
foreach (var item in ScrollableContent)
|
foreach (var item in Scroll.Children)
|
||||||
{
|
{
|
||||||
yield return item;
|
yield return item;
|
||||||
|
|
||||||
|
@ -12,7 +12,19 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.Containers
|
namespace osu.Game.Graphics.Containers
|
||||||
{
|
{
|
||||||
public class OsuScrollContainer : ScrollContainer<Drawable>
|
public class OsuScrollContainer : OsuScrollContainer<Drawable>
|
||||||
|
{
|
||||||
|
public OsuScrollContainer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public OsuScrollContainer(Direction direction)
|
||||||
|
: base(direction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
|
||||||
{
|
{
|
||||||
public const float SCROLL_BAR_HEIGHT = 10;
|
public const float SCROLL_BAR_HEIGHT = 10;
|
||||||
public const float SCROLL_BAR_PADDING = 3;
|
public const float SCROLL_BAR_PADDING = 3;
|
||||||
|
@ -710,6 +710,18 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
UpdateResult(false);
|
UpdateResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
|
||||||
|
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
|
||||||
|
/// <para>
|
||||||
|
/// Defaults to the miss window of <see cref="HitObject"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
|
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
|
||||||
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
|
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
|
||||||
@ -749,14 +761,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
|
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
|
Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
|
||||||
var endTime = HitObject.GetEndTime();
|
|
||||||
|
|
||||||
Result.TimeOffset = Time.Current - endTime;
|
|
||||||
|
|
||||||
double missWindow = HitObject.HitWindows.WindowFor(HitResult.Miss);
|
|
||||||
if (missWindow > 0)
|
|
||||||
Result.TimeOffset = Math.Min(Result.TimeOffset, missWindow);
|
|
||||||
|
|
||||||
if (Result.HasResult)
|
if (Result.HasResult)
|
||||||
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
||||||
@ -778,8 +783,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
if (Judged)
|
if (Judged)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var endTime = HitObject.GetEndTime();
|
CheckForResult(userTriggered, Time.Current - HitObject.GetEndTime());
|
||||||
CheckForResult(userTriggered, Time.Current - endTime);
|
|
||||||
|
|
||||||
return Judged;
|
return Judged;
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
bindStartTime(drawable);
|
bindStartTime(drawable);
|
||||||
AddInternal(drawableMap[entry] = drawable, false);
|
AddInternal(drawableMap[entry] = drawable, false);
|
||||||
|
OnAdd(drawable);
|
||||||
|
|
||||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||||
}
|
}
|
||||||
@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
drawableMap.Remove(entry);
|
drawableMap.Remove(entry);
|
||||||
|
|
||||||
|
OnRemove(drawable);
|
||||||
unbindStartTime(drawable);
|
unbindStartTime(drawable);
|
||||||
RemoveInternal(drawable);
|
RemoveInternal(drawable);
|
||||||
|
|
||||||
@ -147,10 +149,12 @@ namespace osu.Game.Rulesets.UI
|
|||||||
hitObject.OnRevertResult += onRevertResult;
|
hitObject.OnRevertResult += onRevertResult;
|
||||||
|
|
||||||
AddInternal(hitObject);
|
AddInternal(hitObject);
|
||||||
|
OnAdd(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool Remove(DrawableHitObject hitObject)
|
public virtual bool Remove(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
OnRemove(hitObject);
|
||||||
if (!RemoveInternal(hitObject))
|
if (!RemoveInternal(hitObject))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -178,6 +182,26 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> is added to this container.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a <see cref="DrawableHitObject"/> is removed from this container.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual void OnRemove(DrawableHitObject drawableHitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void Clear(bool disposeChildren = true)
|
public virtual void Clear(bool disposeChildren = true)
|
||||||
{
|
{
|
||||||
lifetimeManager.ClearEntries();
|
lifetimeManager.ClearEntries();
|
||||||
|
@ -135,10 +135,8 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// <param name="h">The DrawableHitObject to add.</param>
|
/// <param name="h">The DrawableHitObject to add.</param>
|
||||||
public virtual void Add(DrawableHitObject h)
|
public virtual void Add(DrawableHitObject h)
|
||||||
{
|
{
|
||||||
if (h.IsInitialized)
|
if (!h.IsInitialized)
|
||||||
throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead.");
|
onNewDrawableHitObject(h);
|
||||||
|
|
||||||
onNewDrawableHitObject(h);
|
|
||||||
|
|
||||||
HitObjectContainer.Add(h);
|
HitObjectContainer.Add(h);
|
||||||
OnHitObjectAdded(h.HitObject);
|
OnHitObjectAdded(h.HitObject);
|
||||||
|
@ -2,13 +2,10 @@
|
|||||||
// 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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Layout;
|
using osu.Framework.Layout;
|
||||||
using osu.Framework.Threading;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -19,7 +16,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
{
|
{
|
||||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||||
private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hit objects which require lifetime computation in the next update call.
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<DrawableHitObject> toComputeLifetime = new HashSet<DrawableHitObject>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A set containing all <see cref="HitObjectContainer.AliveObjects"/> which have an up-to-date layout.
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<DrawableHitObject> layoutComputed = new HashSet<DrawableHitObject>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IScrollingInfo scrollingInfo { get; set; }
|
private IScrollingInfo scrollingInfo { get; set; }
|
||||||
@ -27,10 +33,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
|
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
|
||||||
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
||||||
|
|
||||||
// A combined cache across all hit object states to reduce per-update iterations.
|
|
||||||
// When invalidated, one or more (but not necessarily all) hitobject states must be re-validated.
|
|
||||||
private readonly Cached combinedObjCache = new Cached();
|
|
||||||
|
|
||||||
public ScrollingHitObjectContainer()
|
public ScrollingHitObjectContainer()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -48,37 +50,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
timeRange.ValueChanged += _ => layoutCache.Invalidate();
|
timeRange.ValueChanged += _ => layoutCache.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Add(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
combinedObjCache.Invalidate();
|
|
||||||
hitObject.DefaultsApplied += onDefaultsApplied;
|
|
||||||
base.Add(hitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Remove(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
var result = base.Remove(hitObject);
|
|
||||||
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
combinedObjCache.Invalidate();
|
|
||||||
hitObjectInitialStateCache.Remove(hitObject);
|
|
||||||
|
|
||||||
hitObject.DefaultsApplied -= onDefaultsApplied;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Clear(bool disposeChildren = true)
|
public override void Clear(bool disposeChildren = true)
|
||||||
{
|
{
|
||||||
foreach (var h in Objects)
|
|
||||||
h.DefaultsApplied -= onDefaultsApplied;
|
|
||||||
|
|
||||||
base.Clear(disposeChildren);
|
base.Clear(disposeChildren);
|
||||||
|
|
||||||
combinedObjCache.Invalidate();
|
toComputeLifetime.Clear();
|
||||||
hitObjectInitialStateCache.Clear();
|
layoutComputed.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -173,15 +150,40 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDefaultsApplied(DrawableHitObject drawableObject)
|
protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject);
|
||||||
|
|
||||||
|
protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject);
|
||||||
|
|
||||||
|
private void onAddRecursive(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
|
invalidateHitObject(hitObject);
|
||||||
// In such a case, combinedObjCache will take care of updating the hitobject.
|
|
||||||
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state))
|
hitObject.DefaultsApplied += invalidateHitObject;
|
||||||
{
|
|
||||||
combinedObjCache.Invalidate();
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
state.Cache.Invalidate();
|
onAddRecursive(nested);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onRemoveRecursive(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
toComputeLifetime.Remove(hitObject);
|
||||||
|
layoutComputed.Remove(hitObject);
|
||||||
|
|
||||||
|
hitObject.DefaultsApplied -= invalidateHitObject;
|
||||||
|
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
onRemoveRecursive(nested);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make this <see cref="DrawableHitObject"/> lifetime and layout computed in next update.
|
||||||
|
/// </summary>
|
||||||
|
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
// Lifetime computation is delayed until next update because
|
||||||
|
// when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed.
|
||||||
|
toComputeLifetime.Add(hitObject);
|
||||||
|
layoutComputed.Remove(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float scrollLength;
|
private float scrollLength;
|
||||||
@ -192,17 +194,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
|
|
||||||
if (!layoutCache.IsValid)
|
if (!layoutCache.IsValid)
|
||||||
{
|
{
|
||||||
foreach (var state in hitObjectInitialStateCache.Values)
|
toComputeLifetime.Clear();
|
||||||
state.Cache.Invalidate();
|
|
||||||
combinedObjCache.Invalidate();
|
foreach (var hitObject in Objects)
|
||||||
|
{
|
||||||
|
if (hitObject.HitObject != null)
|
||||||
|
toComputeLifetime.Add(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutComputed.Clear();
|
||||||
|
|
||||||
scrollingInfo.Algorithm.Reset();
|
scrollingInfo.Algorithm.Reset();
|
||||||
|
|
||||||
layoutCache.Validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!combinedObjCache.IsValid)
|
|
||||||
{
|
|
||||||
switch (direction.Value)
|
switch (direction.Value)
|
||||||
{
|
{
|
||||||
case ScrollingDirection.Up:
|
case ScrollingDirection.Up:
|
||||||
@ -215,32 +218,24 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var obj in Objects)
|
layoutCache.Validate();
|
||||||
{
|
|
||||||
if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
|
|
||||||
state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
|
|
||||||
|
|
||||||
if (state.Cache.IsValid)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
state.ScheduledComputation?.Cancel();
|
|
||||||
state.ScheduledComputation = computeInitialStateRecursive(obj);
|
|
||||||
|
|
||||||
computeLifetimeStartRecursive(obj);
|
|
||||||
|
|
||||||
state.Cache.Validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedObjCache.Validate();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
|
foreach (var hitObject in toComputeLifetime)
|
||||||
{
|
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
|
||||||
|
|
||||||
foreach (var obj in hitObject.NestedHitObjects)
|
toComputeLifetime.Clear();
|
||||||
computeLifetimeStartRecursive(obj);
|
|
||||||
|
// only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes).
|
||||||
|
foreach (var obj in AliveObjects)
|
||||||
|
{
|
||||||
|
if (layoutComputed.Contains(obj))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updateLayoutRecursive(obj);
|
||||||
|
|
||||||
|
layoutComputed.Add(obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
|
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
|
||||||
@ -271,7 +266,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
|
private void updateLayoutRecursive(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
if (hitObject.HitObject is IHasDuration e)
|
if (hitObject.HitObject is IHasDuration e)
|
||||||
{
|
{
|
||||||
@ -291,12 +286,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
|
|
||||||
foreach (var obj in hitObject.NestedHitObjects)
|
foreach (var obj in hitObject.NestedHitObjects)
|
||||||
{
|
{
|
||||||
computeInitialStateRecursive(obj);
|
updateLayoutRecursive(obj);
|
||||||
|
|
||||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||||
updatePosition(obj, hitObject.HitObject.StartTime);
|
updatePosition(obj, hitObject.HitObject.StartTime);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildrenLife()
|
protected override void UpdateAfterChildrenLife()
|
||||||
{
|
{
|
||||||
@ -328,19 +323,5 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InitialState
|
|
||||||
{
|
|
||||||
[NotNull]
|
|
||||||
public readonly Cached Cache;
|
|
||||||
|
|
||||||
[CanBeNull]
|
|
||||||
public ScheduledDelegate ScheduledComputation;
|
|
||||||
|
|
||||||
public InitialState(Cached cache)
|
|
||||||
{
|
|
||||||
Cache = cache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
{
|
{
|
||||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||||
|
|
||||||
|
public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
protected IScrollingInfo ScrollingInfo { get; private set; }
|
protected IScrollingInfo ScrollingInfo { get; private set; }
|
||||||
|
|
||||||
@ -27,14 +29,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a position in screen space, return the time within this column.
|
/// Given a position in screen space, return the time within this column.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => HitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||||
((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a time, return the screen space position within this column.
|
/// Given a time, return the screen space position within this column.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual Vector2 ScreenSpacePositionAtTime(double time)
|
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
|
||||||
=> ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time);
|
|
||||||
|
|
||||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
|
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool BeatmapSetsLoaded { get; private set; }
|
public bool BeatmapSetsLoaded { get; private set; }
|
||||||
|
|
||||||
private readonly CarouselScrollContainer scroll;
|
protected readonly CarouselScrollContainer Scroll;
|
||||||
|
|
||||||
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
|
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
|
||||||
|
|
||||||
@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select
|
|||||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
||||||
selectedBeatmapSet = null;
|
selectedBeatmapSet = null;
|
||||||
|
|
||||||
ScrollableContent.Clear(false);
|
Scroll.Clear(false);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
scrollPositionCache.Invalidate();
|
ScrollToSelected();
|
||||||
|
|
||||||
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
|
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
|
||||||
FlushPendingFilterOperations();
|
FlushPendingFilterOperations();
|
||||||
@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select
|
|||||||
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||||
|
|
||||||
private readonly Cached itemsCache = new Cached();
|
private readonly Cached itemsCache = new Cached();
|
||||||
private readonly Cached scrollPositionCache = new Cached();
|
private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
|
||||||
|
|
||||||
protected readonly Container<DrawableCarouselItem> ScrollableContent;
|
|
||||||
|
|
||||||
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
|
||||||
|
|
||||||
@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select
|
|||||||
InternalChild = new OsuContextMenuContainer
|
InternalChild = new OsuContextMenuContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = scroll = new CarouselScrollContainer
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Masking = false,
|
setPool,
|
||||||
RelativeSizeAxes = Axes.Both,
|
Scroll = new CarouselScrollContainer
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
setPool,
|
RelativeSizeAxes = Axes.Both,
|
||||||
ScrollableContent = new Container<DrawableCarouselItem>
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select
|
|||||||
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
|
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
|
||||||
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
|
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
|
||||||
|
|
||||||
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue;
|
RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
|
||||||
RightClickScrollingEnabled.TriggerChange();
|
RightClickScrollingEnabled.TriggerChange();
|
||||||
|
|
||||||
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||||
@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the lower visible bound with respect to the current scroll position.
|
/// The position of the lower visible bound with respect to the current scroll position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom;
|
private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the upper visible bound with respect to the current scroll position.
|
/// The position of the upper visible bound with respect to the current scroll position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float visibleUpperBound => scroll.Current - BleedTop;
|
private float visibleUpperBound => Scroll.Current - BleedTop;
|
||||||
|
|
||||||
public void FlushPendingFilterOperations()
|
public void FlushPendingFilterOperations()
|
||||||
{
|
{
|
||||||
@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select
|
|||||||
root.Filter(activeCriteria);
|
root.Filter(activeCriteria);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
|
|
||||||
if (alwaysResetScrollPosition || !scroll.UserScrolling)
|
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
|
||||||
ScrollToSelected();
|
ScrollToSelected(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scroll to the current <see cref="SelectedBeatmap"/>.
|
/// Scroll to the current <see cref="SelectedBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ScrollToSelected() => scrollPositionCache.Invalidate();
|
/// <param name="immediate">
|
||||||
|
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
|
||||||
|
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
|
||||||
|
/// </param>
|
||||||
|
public void ScrollToSelected(bool immediate = false) =>
|
||||||
|
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
|
||||||
|
|
||||||
#region Key / button selection logic
|
#region Key / button selection logic
|
||||||
|
|
||||||
@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
case Key.Left:
|
case Key.Left:
|
||||||
if (!e.Repeat)
|
if (!e.Repeat)
|
||||||
beginRepeatSelection(() => SelectNext(-1, true), e.Key);
|
beginRepeatSelection(() => SelectNext(-1), e.Key);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case Key.Right:
|
case Key.Right:
|
||||||
if (!e.Repeat)
|
if (!e.Repeat)
|
||||||
beginRepeatSelection(() => SelectNext(1, true), e.Key);
|
beginRepeatSelection(() => SelectNext(), e.Key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select
|
|||||||
if (revalidateItems)
|
if (revalidateItems)
|
||||||
updateYPositions();
|
updateYPositions();
|
||||||
|
|
||||||
|
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
|
||||||
|
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
|
||||||
|
if (pendingScrollOperation != PendingScrollOperation.None)
|
||||||
|
updateScrollPosition();
|
||||||
|
|
||||||
// This data is consumed to find the currently displayable range.
|
// This data is consumed to find the currently displayable range.
|
||||||
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
|
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
|
||||||
var newDisplayRange = getDisplayRange();
|
var newDisplayRange = getDisplayRange();
|
||||||
@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
|
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
|
||||||
|
|
||||||
foreach (var panel in ScrollableContent.Children)
|
foreach (var panel in Scroll.Children)
|
||||||
{
|
{
|
||||||
if (toDisplay.Remove(panel.Item))
|
if (toDisplay.Remove(panel.Item))
|
||||||
{
|
{
|
||||||
@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select
|
|||||||
panel.Depth = item.CarouselYPosition;
|
panel.Depth = item.CarouselYPosition;
|
||||||
panel.Y = item.CarouselYPosition;
|
panel.Y = item.CarouselYPosition;
|
||||||
|
|
||||||
ScrollableContent.Add(panel);
|
Scroll.Add(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, if the filtered items have changed, animate drawables to their new locations.
|
|
||||||
// This is common if a selected/collapsed state has changed.
|
|
||||||
if (revalidateItems)
|
|
||||||
{
|
|
||||||
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
|
|
||||||
{
|
|
||||||
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update externally controlled state of currently visible items (e.g. x-offset and opacity).
|
// Update externally controlled state of currently visible items (e.g. x-offset and opacity).
|
||||||
// This is a per-frame update on all drawable panels.
|
// This is a per-frame update on all drawable panels.
|
||||||
foreach (DrawableCarouselItem item in ScrollableContent.Children)
|
foreach (DrawableCarouselItem item in Scroll.Children)
|
||||||
{
|
{
|
||||||
updateItem(item);
|
updateItem(item);
|
||||||
|
|
||||||
@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select
|
|||||||
return (firstIndex, lastIndex);
|
return (firstIndex, lastIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
|
||||||
{
|
|
||||||
base.UpdateAfterChildren();
|
|
||||||
|
|
||||||
if (!scrollPositionCache.IsValid)
|
|
||||||
updateScrollPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||||
{
|
{
|
||||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||||
@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentY += visibleHalfHeight;
|
currentY += visibleHalfHeight;
|
||||||
ScrollableContent.Height = currentY;
|
|
||||||
|
Scroll.ScrollContent.Height = currentY;
|
||||||
|
|
||||||
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
|
||||||
{
|
{
|
||||||
@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select
|
|||||||
if (firstScroll)
|
if (firstScroll)
|
||||||
{
|
{
|
||||||
// reduce movement when first displaying the carousel.
|
// reduce movement when first displaying the carousel.
|
||||||
scroll.ScrollTo(scrollTarget.Value - 200, false);
|
Scroll.ScrollTo(scrollTarget.Value - 200, false);
|
||||||
firstScroll = false;
|
firstScroll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll.ScrollTo(scrollTarget.Value);
|
switch (pendingScrollOperation)
|
||||||
scrollPositionCache.Validate();
|
{
|
||||||
|
case PendingScrollOperation.Standard:
|
||||||
|
Scroll.ScrollTo(scrollTarget.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PendingScrollOperation.Immediate:
|
||||||
|
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
|
||||||
|
// we take the difference in scroll height and apply to all visible panels.
|
||||||
|
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
|
||||||
|
// to enter clamp-special-case mode where it animates completely differently to normal.
|
||||||
|
float scrollChange = scrollTarget.Value - Scroll.Current;
|
||||||
|
|
||||||
|
Scroll.ScrollTo(scrollTarget.Value, false);
|
||||||
|
|
||||||
|
foreach (var i in Scroll.Children)
|
||||||
|
i.Y += scrollChange;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingScrollOperation = PendingScrollOperation.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select
|
|||||||
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
|
/// <param name="parent">For nested items, the parent of the item to be updated.</param>
|
||||||
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
|
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
|
||||||
{
|
{
|
||||||
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
|
||||||
float itemDrawY = posInScroll.Y - visibleUpperBound;
|
float itemDrawY = posInScroll.Y - visibleUpperBound;
|
||||||
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
|
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
|
||||||
|
|
||||||
@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select
|
|||||||
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum PendingScrollOperation
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Standard,
|
||||||
|
Immediate,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A carousel item strictly used for binary search purposes.
|
/// A carousel item strictly used for binary search purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CarouselScrollContainer : OsuScrollContainer
|
protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
|
||||||
{
|
{
|
||||||
private bool rightMouseScrollBlocked;
|
private bool rightMouseScrollBlocked;
|
||||||
|
|
||||||
@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UserScrolling { get; private set; }
|
public bool UserScrolling { get; private set; }
|
||||||
|
|
||||||
|
public CarouselScrollContainer()
|
||||||
|
{
|
||||||
|
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
||||||
|
ScrollContent.AutoSizeAxes = Axes.None;
|
||||||
|
}
|
||||||
|
|
||||||
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
|
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
|
||||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
||||||
{
|
{
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Collections;
|
using osu.Game.Collections;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// position updates should not occur if the item is filtered away.
|
||||||
|
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
|
||||||
|
if (!Item.Visible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float targetY = Item.CarouselYPosition;
|
||||||
|
|
||||||
|
if (Precision.AlmostEquals(targetY, Y))
|
||||||
|
Y = targetY;
|
||||||
|
else
|
||||||
|
// algorithm for this is taken from ScrollContainer.
|
||||||
|
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
|
||||||
|
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
|
||||||
|
}
|
||||||
|
|
||||||
protected override void UpdateItem()
|
protected override void UpdateItem()
|
||||||
{
|
{
|
||||||
base.UpdateItem();
|
base.UpdateItem();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user