1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 22:22:55 +08:00

Merge branch 'master' into master

This commit is contained in:
Ryan Zmuda 2020-11-30 21:42:15 -05:00 committed by GitHub
commit 8f6607ffec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 888 additions and 395 deletions

View File

@ -1,9 +1,10 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK; using osuTK;
@ -17,34 +18,49 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
base.LoadComplete(); base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
AddStep("show banana", () => SetContents(createDrawableBanana));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
} }
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmap = indexInBeatmap,
HyperDashBindable = { Value = hyperdash }
}));
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); private Drawable createDrawableBanana() =>
SetProperties(new DrawableBanana(new Banana()));
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); private Drawable createDrawableDroplet(bool hyperdash = false) =>
SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { Value = hyperdash }
}));
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet()));
protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d)
{ {
var hitObject = d.HitObject; var hitObject = d.HitObject;
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 });
hitObject.StartTime = 1000000000000; hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f; hitObject.Scale = 1.5f;
if (hyperdash)
((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana();
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None; d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero; d.Position = Vector2.Zero;
@ -55,15 +71,5 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
return d; return d;
} }
public class TestCatchFruit : Fruit
{
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
} }
} }

View File

@ -0,0 +1,32 @@
// 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.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneFruitVisualChange : TestSceneFruitObjects
{
private readonly Bindable<int> indexInBeatmap = new Bindable<int>();
private readonly Bindable<bool> hyperDash = new Bindable<bool>();
protected override void LoadComplete()
{
AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true);
Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true);
}
}
}

View File

@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public int BananaIndex; public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };

View File

@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public class BananaShower : CatchHitObject, IHasDuration public class BananaShower : CatchHitObject, IHasDuration
{ {
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override bool LastInCombo => true; public override bool LastInCombo => true;
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();

View File

@ -16,27 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
private float x; // This value is after XOffset applied.
public readonly Bindable<float> XBindable = new Bindable<float>();
// This value is before XOffset applied.
private float originalX;
/// <summary> /// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary> /// </summary>
public float X public float X
{ {
get => x + XOffset; // TODO: I don't like this asymmetry.
set => x = value; get => XBindable.Value;
// originalX is set by `XBindable.BindValueChanged`
set => XBindable.Value = value + xOffset;
} }
private float xOffset;
/// <summary> /// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>. /// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary> /// </summary>
internal float XOffset { get; set; } internal float XOffset
{
get => xOffset;
set
{
xOffset = value;
XBindable.Value = originalX + xOffset;
}
}
public double TimePreempt = 1000; public double TimePreempt = 1000;
public int IndexInBeatmap { get; set; } public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); public int IndexInBeatmap
{
get => IndexInBeatmapBindable.Value;
set => IndexInBeatmapBindable.Value = value;
}
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
@ -69,7 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value; set => LastInComboBindable.Value = value;
} }
public float Scale { get; set; } = 1; public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public float Scale
{
get => ScaleBindable.Value;
set => ScaleBindable.Value = value;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
@ -81,14 +107,10 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
public enum FruitVisualRepresentation protected CatchHitObject()
{ {
Pear, XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
Grape, }
Pineapple,
Raspberry,
Banana // banananananannaanana
} }
} }

View File

@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableBanana : DrawableFruit public class DrawableBanana : DrawableFruit
{ {
protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
public DrawableBanana(Banana h) public DrawableBanana(Banana h)
: base(h) : base(h)
{ {

View File

@ -2,6 +2,8 @@
// 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;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -10,19 +12,34 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
public readonly Bindable<float> XBindable = new Bindable<float>();
protected override double InitialLifetimeOffset => HitObject.TimePreempt; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
X = hitObject.X;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
protected override void OnApply()
{
base.OnApply();
XBindable.BindTo(HitObject.XBindable);
}
protected override void OnFree()
{
base.OnFree();
XBindable.UnbindFrom(HitObject.XBindable);
}
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;
public bool IsOnPlate; public bool IsOnPlate;

View File

@ -21,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece
{
HyperDash = { BindTarget = HyperDash }
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -11,6 +12,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
public DrawableFruit(CatchHitObject h) public DrawableFruit(CatchHitObject h)
: base(h) : base(h)
{ {
@ -19,10 +24,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
}, true);
VisualRepresentation.BindValueChanged(_ => updatePiece());
HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(VisualRepresentation.Value)),
_ => new FruitPiece
{
VisualRepresentation = { BindTarget = VisualRepresentation },
HyperDash = { BindTarget = HyperDash },
});
} }
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
@ -49,4 +70,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
} }
} }
} }
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
Banana // banananananannaanana
}
} }

View File

@ -1,7 +1,9 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osuTK; using osuTK;
@ -12,6 +14,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
/// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale.
/// </summary>
protected virtual float ScaleFactor => 1;
/// <summary> /// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary> /// </summary>
@ -19,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected readonly Container ScaleContainer; protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject(CatchHitObject h) protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
: base(h) : base(h)
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -36,7 +49,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Scale = new Vector2(HitObject.Scale); XBindable.BindValueChanged(x =>
{
if (!IsOnPlate) X = x.NewValue;
}, true);
ScaleBindable.BindValueChanged(scale =>
{
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
}, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
}
protected override void OnApply()
{
base.OnApply();
HyperDash.BindTo(HitObject.HyperDashBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
}
protected override void OnFree()
{
HyperDash.UnbindFrom(HitObject.HyperDashBindable);
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
base.OnFree();
} }
} }
} }

View File

@ -1,21 +1,15 @@
// 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.Framework.Allocation;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableTinyDroplet : DrawableDroplet public class DrawableTinyDroplet : DrawableDroplet
{ {
protected override float ScaleFactor => base.ScaleFactor / 2;
public DrawableTinyDroplet(TinyDroplet h) public DrawableTinyDroplet(TinyDroplet h)
: base(h) : base(h)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Scale /= 2;
}
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class DropletPiece : CompositeDrawable public class DropletPiece : CompositeDrawable
{ {
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public DropletPiece() public DropletPiece()
{ {
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
@ -19,15 +22,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
InternalChild = new Pulp InternalChild = new Pulp
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour } AccentColour = { BindTarget = drawableObject.AccentColour }
}; };
if (drawableCatchObject.HitObject.HyperDash) if (HyperDash.Value)
{ {
AddInternal(new HyperDropletBorderPiece()); AddInternal(new HyperDropletBorderPiece());
} }

View File

@ -2,7 +2,9 @@
// 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;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,36 +18,39 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
/// </summary> /// </summary>
public const float RADIUS_ADJUST = 1.1f; public const float RADIUS_ADJUST = 1.1f;
private BorderPiece border; public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
private PalpableCatchHitObject hitObject; public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[CanBeNull]
private DrawableCatchHitObject drawableHitObject;
[CanBeNull]
private BorderPiece borderPiece;
public FruitPiece() public FruitPiece()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(permitNulls: true)]
private void load(DrawableHitObject drawableObject) private void load([CanBeNull] DrawableHitObject drawable)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; drawableHitObject = (DrawableCatchHitObject)drawable;
hitObject = drawableCatchObject.HitObject;
AddRangeInternal(new[] AddInternal(getFruitFor(VisualRepresentation.Value));
{
getFruitFor(hitObject.VisualRepresentation),
border = new BorderPiece(),
});
if (hitObject.HyperDash) // if it is not part of a DHO, the border is always invisible.
{ if (drawableHitObject != null)
AddInternal(borderPiece = new BorderPiece());
if (HyperDash.Value)
AddInternal(new HyperBorderPiece()); AddInternal(new HyperBorderPiece());
} }
}
protected override void Update() protected override void Update()
{ {
base.Update(); if (borderPiece != null && drawableHitObject?.HitObject != null)
border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1); borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
} }
private Drawable getFruitFor(FruitVisualRepresentation representation) private Drawable getFruitFor(FruitVisualRepresentation representation)

View File

@ -2,6 +2,7 @@
// 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 osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics; using osuTK.Graphics;
@ -20,15 +21,27 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public float DistanceToHyperDash { get; set; } public float DistanceToHyperDash { get; set; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>();
/// <summary> /// <summary>
/// Whether this fruit can initiate a hyperdash. /// Whether this fruit can initiate a hyperdash.
/// </summary> /// </summary>
public bool HyperDash => HyperDashTarget != null; public bool HyperDash => HyperDashBindable.Value;
private CatchHitObject hyperDashTarget;
/// <summary> /// <summary>
/// The target fruit if we are to initiate a hyperdash. /// The target fruit if we are to initiate a hyperdash.
/// </summary> /// </summary>
public CatchHitObject HyperDashTarget; public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
set
{
hyperDashTarget = value;
HyperDashBindable.Value = value != null;
}
}
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
} }

View File

@ -55,7 +55,13 @@ namespace osu.Game.Rulesets.Catch.UI
HitObjectContainer, HitObjectContainer,
CatcherArea, CatcherArea,
}; };
}
protected override void LoadComplete()
{
base.LoadComplete();
// these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case).
NewResult += onNewResult; NewResult += onNewResult;
RevertResult += onRevertResult; RevertResult += onRevertResult;
} }

View File

@ -78,9 +78,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime; private double originalStartTime;
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
if (PlacementActive) if (PlacementActive)
{ {

View File

@ -48,9 +48,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return true; return true;
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
if (!PlacementActive) if (!PlacementActive)
Column = result.Playfield as Column; Column = result.Playfield as Column;

View File

@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre }; InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
if (result.Playfield != null) if (result.Playfield != null)
{ {

View File

@ -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);

View File

@ -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)

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneObjectBeatSnap : TestSceneOsuEditor
{
private OsuPlayfield playfield;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
}
[Test]
public void TestBeatSnapHitCircle()
{
double firstTimingPointTime() => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time;
AddStep("seek some milliseconds forward", () => EditorClock.Seek(firstTimingPointTime() + 10));
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
AddAssert("ensure object snapped back to correct time", () => EditorBeatmap.HitObjects.First().StartTime == firstTimingPointTime());
}
}
}

View File

@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First()); AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
AddStep("seek to first control point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
} }
[TestCase(true)] [TestCase(true)]
@ -66,13 +67,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); AddStep("start slider placement", () => InputManager.Click(MouseButton.Left));
AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0))); AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.225f, 0)));
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left)); AddStep("place second object", () => InputManager.Click(MouseButton.Left));

View File

@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
} }
} }

View File

@ -67,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
switch (state) switch (state)
{ {

View File

@ -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()

View File

@ -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>

View File

@ -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;

View File

@ -43,10 +43,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return false; return false;
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
piece.Position = ToLocalSpace(result.ScreenSpacePosition); piece.Position = ToLocalSpace(result.ScreenSpacePosition);
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
} }
} }
} }

View File

@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
EndPlacement(true); EndPlacement(true);
} }
public override void UpdatePosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
base.UpdatePosition(result); base.UpdateTimeAndPosition(result);
if (PlacementActive) if (PlacementActive)
{ {

View File

@ -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)

View File

@ -2,7 +2,6 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(2));
} }
[Test]
public void TestApplyThenUndoThenApplySameChange()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash;
// undo a change without saving
handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(2));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
}
[Test] [Test]
public void TestSaveSameStateDoesNotSave() public void TestSaveSameStateDoesNotSave()
{ {
@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing
private void addArbitraryChange(EditorBeatmap beatmap) private void addArbitraryChange(EditorBeatmap beatmap)
{ {
beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); beatmap.Add(new HitCircle { StartTime = 2760 });
} }
} }
} }

View File

@ -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

View File

@ -3,7 +3,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -14,44 +13,41 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneStarCounter : OsuTestScene public class TestSceneStarCounter : OsuTestScene
{ {
private readonly StarCounter starCounter;
private readonly OsuSpriteText starsLabel;
public TestSceneStarCounter() public TestSceneStarCounter()
{ {
StarCounter stars = new StarCounter starCounter = new StarCounter
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Current = 5,
}; };
Add(stars); Add(starCounter);
SpriteText starsLabel = new OsuSpriteText starsLabel = new OsuSpriteText
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Scale = new Vector2(2), Scale = new Vector2(2),
Y = 50, Y = 50,
Text = stars.Current.ToString("0.00"),
}; };
Add(starsLabel); Add(starsLabel);
AddRepeatStep(@"random value", delegate setStars(5);
{
stars.Current = RNG.NextSingle() * (stars.StarCount + 1);
starsLabel.Text = stars.Current.ToString("0.00");
}, 10);
AddStep(@"Stop animation", delegate AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10);
{ AddSliderStep("exact value", 0f, 10f, 5f, setStars);
stars.StopAnimation(); AddStep("stop animation", () => starCounter.StopAnimation());
}); AddStep("reset", () => setStars(0));
}
AddStep(@"Reset", delegate private void setStars(float stars)
{ {
stars.Current = 0; starCounter.Current = stars;
starsLabel.Text = stars.Current.ToString("0.00"); starsLabel.Text = starCounter.Current.ToString("0.00");
});
} }
} }
} }

View File

@ -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;

View File

@ -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;

View File

@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface
public override void DisplayAt(float scale) public override void DisplayAt(float scale)
{ {
scale = Math.Clamp(scale, min_star_scale, 1); scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1));
this.FadeTo(scale, fading_duration); this.FadeTo(scale, fading_duration);
Icon.ScaleTo(scale, scaling_duration, scaling_easing); Icon.ScaleTo(scale, scaling_duration, scaling_easing);

View File

@ -6,13 +6,13 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -44,8 +44,7 @@ namespace osu.Game.Overlays
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = OsuColour.Gray(0.05f),
Alpha = 0.6f
}, },
new OsuScrollContainer new OsuScrollContainer
{ {

View File

@ -27,7 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections
new AudioDevicesSettings(), new AudioDevicesSettings(),
new VolumeSettings(), new VolumeSettings(),
new OffsetSettings(), new OffsetSettings(),
new MainMenuSettings()
}; };
} }
} }

View File

@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[] Children = new Drawable[]
{ {
new GeneralSettings(), new GeneralSettings(),
new SongSelectSettings(),
new ModsSettings(), new ModsSettings(),
}; };
} }

View File

@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections
new RendererSettings(), new RendererSettings(),
new LayoutSettings(), new LayoutSettings(),
new DetailSettings(), new DetailSettings(),
new UserInterfaceSettings(),
}; };
} }
} }

View File

@ -0,0 +1,15 @@
// 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.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
/// <summary>
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
/// </summary>
internal class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
}

View File

@ -54,12 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections
skinDropdown = new SkinSettingsDropdown(), skinDropdown = new SkinSettingsDropdown(),
new ExportSkinButton(), new ExportSkinButton(),
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Gameplay cursor size", LabelText = "Gameplay cursor size",
Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize), Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
@ -136,11 +130,6 @@ namespace osu.Game.Overlays.Settings.Sections
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
} }
private class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo> private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
{ {
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl(); protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();

View File

@ -6,11 +6,11 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Graphics namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class UserInterfaceSettings : SettingsSubsection public class GeneralSettings : SettingsSubsection
{ {
protected override string Header => "User Interface"; protected override string Header => "General";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
@ -22,6 +22,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
LabelText = "Rotate cursor when dragging", LabelText = "Rotate cursor when dragging",
Current = config.GetBindable<bool>(OsuSetting.CursorRotation) Current = config.GetBindable<bool>(OsuSetting.CursorRotation)
}, },
new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Parallax", LabelText = "Parallax",

View File

@ -7,7 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class MainMenuSettings : SettingsSubsection public class MainMenuSettings : SettingsSubsection
{ {

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Gameplay namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class SongSelectSettings : SettingsSubsection public class SongSelectSettings : SettingsSubsection
{ {

View File

@ -0,0 +1,29 @@
// 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.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Settings.Sections.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
public class UserInterfaceSection : SettingsSection
{
public override string Header => "User Interface";
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.LayerGroup
};
public UserInterfaceSection()
{
Children = new Drawable[]
{
new GeneralSettings(),
new MainMenuSettings(),
new SongSelectSettings()
};
}
}
}

View File

@ -1,16 +1,15 @@
// 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 osuTK; using System.Collections.Generic;
using osuTK.Graphics; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System.Collections.Generic; using osuTK.Graphics;
using System.Linq;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -26,7 +25,7 @@ namespace osu.Game.Overlays.Settings
public virtual IEnumerable<string> FilterTerms => new[] { Header }; public virtual IEnumerable<string> FilterTerms => new[] { Header };
private const int header_size = 26; private const int header_size = 26;
private const int header_margin = 25; private const int margin = 20;
private const int border_size = 2; private const int border_size = 2;
public bool MatchingFilter public bool MatchingFilter
@ -38,7 +37,7 @@ namespace osu.Game.Overlays.Settings
protected SettingsSection() protected SettingsSection()
{ {
Margin = new MarginPadding { Top = 20 }; Margin = new MarginPadding { Top = margin };
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -46,10 +45,9 @@ namespace osu.Game.Overlays.Settings
{ {
Margin = new MarginPadding Margin = new MarginPadding
{ {
Top = header_size + header_margin Top = header_size
}, },
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 30),
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}; };
@ -70,7 +68,7 @@ namespace osu.Game.Overlays.Settings
{ {
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = 20 + border_size, Top = margin + border_size,
Bottom = 10, Bottom = 10,
}, },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -82,7 +80,11 @@ namespace osu.Game.Overlays.Settings
Font = OsuFont.GetFont(size: header_size), Font = OsuFont.GetFont(size: header_size),
Text = Header, Text = Header,
Colour = colours.Yellow, Colour = colours.Yellow,
Margin = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS } Margin = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
Right = SettingsPanel.CONTENT_MARGINS
}
}, },
FlowContent FlowContent
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings
FlowContent = new FillFlowContainer FlowContent = new FillFlowContainer
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5), Spacing = new Vector2(0, 8),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
}; };
@ -53,7 +53,7 @@ namespace osu.Game.Overlays.Settings
new OsuSpriteText new OsuSpriteText
{ {
Text = Header.ToUpperInvariant(), Text = Header.ToUpperInvariant(),
Margin = new MarginPadding { Bottom = 10, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS },
Font = OsuFont.GetFont(weight: FontWeight.Bold), Font = OsuFont.GetFont(weight: FontWeight.Bold),
}, },
FlowContent FlowContent

View File

@ -9,9 +9,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Settings
{ {
new Box new Box
{ {
Colour = Color4.Black, Colour = OsuColour.Gray(0.02f),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
new SidebarScrollContainer new SidebarScrollContainer

View File

@ -23,10 +23,11 @@ namespace osu.Game.Overlays
{ {
new GeneralSection(), new GeneralSection(),
new GraphicsSection(), new GraphicsSection(),
new GameplaySection(),
new AudioSection(), new AudioSection(),
new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())), new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
new SkinSection(),
new OnlineSection(), new OnlineSection(),
new MaintenanceSection(), new MaintenanceSection(),
new DebugSection(), new DebugSection(),
@ -61,7 +62,6 @@ namespace osu.Game.Overlays
switch (state.NewValue) switch (state.NewValue)
{ {
case Visibility.Visible: case Visibility.Visible:
Background.FadeTo(0.9f, 300, Easing.OutQuint);
Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint);
SectionsContainer.FadeOut(300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint);
@ -69,7 +69,6 @@ namespace osu.Game.Overlays
break; break;
case Visibility.Hidden: case Visibility.Hidden:
Background.FadeTo(0.6f, 500, Easing.OutQuint);
Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint);
SectionsContainer.FadeIn(500, Easing.OutQuint); SectionsContainer.FadeIn(500, Easing.OutQuint);

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
@ -72,8 +73,8 @@ namespace osu.Game.Overlays
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Scale = new Vector2(2, 1), // over-extend to the left for transitions Scale = new Vector2(2, 1), // over-extend to the left for transitions
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = OsuColour.Gray(0.05f),
Alpha = 0.6f, Alpha = 1,
}, },
SectionsContainer = new SettingsSectionsContainer SectionsContainer = new SettingsSectionsContainer
{ {
@ -214,7 +215,7 @@ namespace osu.Game.Overlays
base.UpdateAfterChildren(); base.UpdateAfterChildren();
// no null check because the usage of this class is strict // no null check because the usage of this class is strict
HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y;
} }
} }
} }

View File

@ -85,10 +85,10 @@ namespace osu.Game.Rulesets.Edit
} }
/// <summary> /// <summary>
/// Updates the position of this <see cref="PlacementBlueprint"/> to a new screen-space position. /// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary> /// </summary>
/// <param name="result">The snap result information.</param> /// <param name="result">The snap result information.</param>
public virtual void UpdatePosition(SnapResult result) public virtual void UpdateTimeAndPosition(SnapResult result)
{ {
if (!PlacementActive) if (!PlacementActive)
HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current; HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current;

View File

@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Judgements
private readonly Container aboveHitObjectsContent; private readonly Container aboveHitObjectsContent;
[Resolved]
private ISkinSource skinSource { get; set; }
/// <summary> /// <summary>
/// Duration of initial fade in. /// Duration of initial fade in.
/// </summary> /// </summary>
@ -78,29 +75,6 @@ namespace osu.Game.Rulesets.Judgements
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
protected override void LoadComplete()
{
base.LoadComplete();
skinSource.SourceChanged += onSkinChanged;
}
private void onSkinChanged()
{
// on a skin change, the child component will update but not get correctly triggered to play its animation.
// we need to trigger a reinitialisation to make things right.
currentDrawableType = null;
PrepareForUse();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skinSource != null)
skinSource.SourceChanged -= onSkinChanged;
}
/// <summary> /// <summary>
/// Apply top-level animations to the current judgement when successfully hit. /// Apply top-level animations to the current judgement when successfully hit.
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required. /// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
@ -142,13 +116,14 @@ namespace osu.Game.Rulesets.Judgements
Debug.Assert(Result != null); Debug.Assert(Result != null);
prepareDrawables();
runAnimation(); runAnimation();
} }
private void runAnimation() private void runAnimation()
{ {
// is a no-op if the drawables are already in a correct state.
prepareDrawables();
// undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state.
ApplyTransformsAt(double.MinValue, true); ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
@ -203,7 +178,6 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null) if (JudgementBody != null)
RemoveInternal(JudgementBody); RemoveInternal(JudgementBody);
aboveHitObjectsContent.Clear();
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ => AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{ {
@ -211,14 +185,29 @@ namespace osu.Game.Rulesets.Judgements
Origin = Anchor.Centre, Origin = Anchor.Centre,
}); });
JudgementBody.OnSkinChanged += () =>
{
// on a skin change, the child component will update but not get correctly triggered to play its animation (or proxy the newly created content).
// we need to trigger a reinitialisation to make things right.
proxyContent();
runAnimation();
};
proxyContent();
currentDrawableType = type;
void proxyContent()
{
aboveHitObjectsContent.Clear();
if (JudgementBody.Drawable is IAnimatableJudgement animatable) if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{ {
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
if (proxiedContent != null) if (proxiedContent != null)
aboveHitObjectsContent.Add(proxiedContent); aboveHitObjectsContent.Add(proxiedContent);
} }
}
currentDrawableType = type;
} }
protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);

View File

@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.LoadComplete(); base.LoadComplete();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true);
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
} }
@ -530,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin, allowFallback);
updateComboColour(); UpdateComboColour();
ApplySkin(skin, allowFallback); ApplySkin(skin, allowFallback);
@ -538,7 +538,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true); updateState(State.Value, true);
} }
private void updateComboColour() protected void UpdateComboColour()
{ {
if (!(HitObject is IHasComboInformation combo)) return; if (!(HitObject is IHasComboInformation combo)) return;
@ -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;
} }

View File

@ -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();

View File

@ -135,9 +135,7 @@ 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);

View File

@ -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(); foreach (var hitObject in toComputeLifetime)
}
}
private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
{
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;
}
}
} }
} }

View File

@ -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();
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } protected EditorClock EditorClock { get; private set; }
[Resolved] [Resolved]
protected EditorBeatmap Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
@ -170,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
return false; return false;
editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); EditorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
return true; return true;
} }
@ -381,7 +381,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
case SelectionState.Selected: case SelectionState.Selected:
// if the editor is playing, we generally don't want to deselect objects even if outside the selection area. // if the editor is playing, we generally don't want to deselect objects even if outside the selection area.
if (!editorClock.IsRunning && !isValidForSelection()) if (!EditorClock.IsRunning && !isValidForSelection())
blueprint.Deselect(); blueprint.Deselect();
break; break;
} }

View File

@ -157,7 +157,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
currentPlacement.UpdatePosition(snapResult); // if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
currentPlacement.UpdateTimeAndPosition(snapResult);
} }
#endregion #endregion

View File

@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit
var newState = stream.ToArray(); var newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state. // if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; if (savedStates.Count > 0 && newState.SequenceEqual(savedStates[currentState])) return;
if (currentState < savedStates.Count - 1) if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);

View File

@ -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>();
@ -154,18 +152,13 @@ namespace osu.Game.Screens.Select
root = new CarouselRoot(this); root = new CarouselRoot(this);
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both,
Child = scroll = new CarouselScrollContainer
{
Masking = false,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
setPool, setPool,
ScrollableContent = new Container<DrawableCarouselItem> Scroll = new CarouselScrollContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
}
} }
} }
}; };
@ -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)
{ {

View File

@ -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();

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual
{ {
base.Update(); base.Update();
currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint)); currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint));
} }
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual
if (drawable is PlacementBlueprint blueprint) if (drawable is PlacementBlueprint blueprint)
{ {
blueprint.Show(); blueprint.Show();
blueprint.UpdatePosition(SnapForBlueprint(blueprint)); blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint));
} }
} }