1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 23:22:55 +08:00

Merge branch 'master' into change-taiko-ez

This commit is contained in:
Bartłomiej Dach 2020-11-22 13:29:02 +01:00 committed by GitHub
commit 827a089810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1606 additions and 626 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1120.0" />
</ItemGroup>
</Project>

View File

@ -139,7 +139,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden;
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });

View File

@ -18,71 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests
base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
private Drawable createDrawableTinyDroplet()
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
{
var droplet = new TestCatchTinyDroplet
{
Scale = 1.5f,
};
var hitObject = d.HitObject;
hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f;
return new DrawableTinyDroplet(droplet)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
if (hyperdash)
hitObject.HyperDashTarget = new Banana();
private Drawable createDrawableDroplet(bool hyperdash = false)
{
var droplet = new TestCatchDroplet
d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero;
d.HitObjectApplied += _ =>
{
Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
};
return new DrawableDroplet(droplet)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
{
Fruit fruit = new TestCatchFruit(rep)
{
Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
};
return new DrawableFruit(fruit)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
d.LifetimeStart = double.NegativeInfinity;
d.LifetimeEnd = double.PositiveInfinity;
};
return d;
}
public class TestCatchFruit : Fruit
@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
StartTime = 1000000000000;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
public class TestCatchDroplet : Droplet
{
public TestCatchDroplet()
{
StartTime = 1000000000000;
}
}
public class TestCatchTinyDroplet : TinyDroplet
{
public TestCatchTinyDroplet()
{
StartTime = 1000000000000;
}
}
}
}

View File

@ -1,7 +1,7 @@
// 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.Allocation;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
/// </summary>
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{
[BackgroundDependencyLoader]
private void load()
[SetUp]
public void SetUp() => Schedule(() =>
{
SetContents(() => new FillFlowContainer
{
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
},
}
});
}
});
protected abstract DrawableManiaHitObject CreateHitObject();
}

View File

@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (hitWindows.IsHitResultAllowed(result))
{
AddStep("Show " + result.GetDescription(), () => SetContents(() =>
new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
});
}
[Test]
public void TestFadeOnMiss()
{
AddStep("miss tick", () =>
{
foreach (var holdNote in holdNotes)
holdNote.ChildrenOfType<DrawableHoldNoteHead>().First().MissForcefully();
});
}
private IEnumerable<DrawableHoldNote> holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType<DrawableHoldNote>());
protected override DrawableManiaHitObject CreateHitObject()
{
var note = new HoldNote { Duration = 1000 };

View File

@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
/// </summary>
public bool HasBroken { get; private set; }
public double? HoldBrokenTime { get; private set; }
/// <summary>
/// Whether the hold note has been released potentially without having caused a break.
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
if (Tail.Judged && !Tail.IsHit)
HasBroken = true;
HoldBrokenTime = Time.Current;
}
public bool OnPressed(ManiaAction action)
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
HasBroken = true;
HoldBrokenTime = Time.Current;
releaseTime = Time.Current;
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
result = HitResult.Meh;
r.Type = result;

View File

@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{
public override bool DisplayResult => false;
/// <summary>
/// References the time at which the user started holding the hold note.
/// </summary>

View File

@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyBodyPiece : LegacyManiaColumnElement
{
private DrawableHoldNote holdNote;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<bool> isHitting = new Bindable<bool>();
/// <summary>
/// Stores the start time of the fade animation that plays when any of the nested
/// hitobjects of the hold note are missed.
/// </summary>
private readonly Bindable<double?> missFadeTime = new Bindable<double?>();
[CanBeNull]
private Drawable bodySprite;
@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{
holdNote = (DrawableHoldNote)drawableObject;
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
{
base.LoadComplete();
direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true);
missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
applyCustomUpdateState(holdNote, holdNote.State.Value);
}
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
{
// ensure that the hold note is also faded out when the head/tail/any tick is missed.
if (state == ArmedState.Miss)
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
}
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
@ -158,10 +183,38 @@ namespace osu.Game.Rulesets.Mania.Skinning
}
}
private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange)
{
if (missFadeTimeChange.NewValue == null)
return;
// this update could come from any nested object of the hold note (or even from an input).
// make sure the transforms are consistent across all affected parts.
using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
{
// colour and duration matches stable
// transforms not applied to entire hold note in order to not affect hit lighting
const double fade_duration = 60;
holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
}
}
protected override void Update()
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (holdNote != null)
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
lightContainer?.Expire();
}
}

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
if (!result.IsHit || !DisplayJudgements.Value)
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));

View File

@ -1,10 +1,11 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -19,22 +20,52 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
[BackgroundDependencyLoader]
private void load()
protected override void ApplyMissAnimations()
{
if (JudgementText != null)
JudgementText.Font = JudgementText.Font.With(size: 25);
}
if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
{
// this is temporary logic until mania's skin transformer returns IAnimatableJudgements
JudgementBody.ScaleTo(1.6f);
JudgementBody.ScaleTo(1, 100, Easing.In);
protected override double FadeInDuration => 50;
JudgementBody.MoveTo(Vector2.Zero);
JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
JudgementBody.RotateTo(0);
JudgementBody.RotateTo(40, 800, Easing.InQuint);
JudgementBody.FadeOutFromOne(800);
LifetimeEnd = JudgementBody.LatestTransformEndTime;
}
base.ApplyMissAnimations();
}
protected override void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
this.Delay(FadeInDuration).FadeOut(200);
JudgementBody.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
}
}
}

View File

@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
// Tick judgements should not display text.
if (judgedObject is DrawableHoldNoteTick)
return;
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{

View File

@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body immediately visible",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha == 1));
AddAssert("hit lighting hidden",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
AddAssert("hit lighting has no transforms", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => !judgement.Lighting.Transforms.Any()));
AddAssert("hit lighting hidden", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
}
[Test]
@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body not immediately visible",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
AddAssert("hit lighting shown",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha > 0));
AddUntilStep("hit lighting shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any(judgement => judgement.Lighting.Alpha > 0));
}
private void showResult(HitResult result)
@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Children = new Drawable[]
{
pool,
pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
pool.Get(j => j.Apply(new JudgementResult(new HitObject
{
StartTime = Time.Current
}, new Judgement())
{
Type = result,
}, null)).With(j =>
{
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestDrawableOsuJudgement : DrawableOsuJudgement
{
public new SkinnableSprite Lighting => base.Lighting;
public new Container JudgementBody => base.JudgementBody;
public new SkinnableDrawable JudgementBody => base.JudgementBody;
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
addMultipleObjectsStep();
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
AddStep("move hitobject", () =>
{
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = getObject(1).HitObject.StartTime;
followPointRenderer.UpdateSubTree();
getObject(2).HitObject.Position = new Vector2(300, 100);
});
assertGroups();
assertDirections();
}
[TestCase(0, 0)] // Start -> Start
@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertGroups()
{
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () =>
{
for (int i = 0; i < hitObjectContainer.Count; i++)
@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
if (getGroup(i).Start != expectedStart.HitObject)
if (getEntry(i).Start != expectedStart.HitObject)
throw new AssertionException($"Object {i} expected to be the start of group {i}.");
if (getGroup(i).End != expectedEnd?.HitObject)
if (getEntry(i).End != expectedEnd?.HitObject)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
}
@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
if (expectedEnd == null)
continue;
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = expectedStart.HitObject.StartTime;
followPointRenderer.UpdateSubTree();
var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray();
if (points.Length == 0)
continue;
@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType<FollowPointConnection>().Single(c => c.Entry == getEntry(index));
private class TestHitObjectContainer : Container<DrawableOsuHitObject>
{

View File

@ -1,17 +1,17 @@
// 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.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
var playfield = new TestOsuPlayfield();
playfield.Add(drawable);
return playfield;
}
private Drawable testStream(float circleSize, bool auto = false)
{
var playfield = new TestOsuPlayfield();
Vector2 pos = new Vector2(-250, 0);
for (int i = 0; i <= 1000; i += 100)
{
playfield.Add(createSingle(circleSize, auto, i, pos));
pos.X += 50;
}
return playfield;
}
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
{
positionOffset ??= Vector2.Zero;
var circle = new HitCircle
{
StartTime = Time.Current + 1000 + timeOffset,
Position = positionOffset.Value,
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
};
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
return drawable;
}
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
private Drawable testStream(float circleSize, bool auto = false)
{
var container = new Container { RelativeSizeAxes = Axes.Both };
Vector2 pos = new Vector2(-250, 0);
for (int i = 0; i <= 1000; i += 100)
{
container.Add(testSingle(circleSize, auto, i, pos));
pos.X += 50;
}
return container;
}
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests
base.CheckForResult(userTriggered, timeOffset);
}
}
protected class TestOsuPlayfield : OsuPlayfield
{
public TestOsuPlayfield()
{
RelativeSizeAxes = Axes.Both;
}
}
}
}

View File

@ -1,8 +1,11 @@
// 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.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneShaking : TestSceneHitCircle
{
private readonly List<ScheduledDelegate> scheduledTasks = new List<ScheduledDelegate>();
protected override IBeatmap CreateBeatmapForSkinProvider()
{
// best way to run cleanup before a new step is run
foreach (var task in scheduledTasks)
task.Cancel();
scheduledTasks.Clear();
return base.CreateBeatmapForSkinProvider();
}
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay);
scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
return drawableHitObject;
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Path path;
private readonly Slider slider;
private readonly int controlPointIndex;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion;
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
this.controlPointIndex = controlPointIndex;
ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
int nextIndex = controlPointIndex + 1;
int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;

View File

@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece> Pieces;
internal readonly Container<PathControlPointConnectionPiece> Connections;
@ -66,6 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.NewStartingIndex)
connection.ControlPointIndex += e.NewItems.Count;
}
}
for (int i = 0; i < e.NewItems.Count; i++)
{
var point = (PathControlPoint)e.NewItems[i];
@ -88,6 +102,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Connections.RemoveAll(c => c.ControlPoint == point);
}
// If removing before the end of the path,
// update indices of connections after remove location
if (e.OldStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.OldStartingIndex)
connection.ControlPointIndex -= e.OldItems.Count;
}
}
break;
}
}

View File

@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
}
private IBindable<int> pathVersion;
protected override void LoadComplete()
{
base.LoadComplete();
pathVersion = HitObject.Path.Version.GetBoundCopy();
controlPoints.BindTo(HitObject.Path.ControlPoints);
pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary>
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPoint : Container, IAnimationTimeReference
public class FollowPoint : PoolableDrawable, IAnimationTimeReference
{
private const float width = 8;
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
Origin = Anchor.Centre;
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,

View File

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
using osuTK;
@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPointConnection : CompositeDrawable
public class FollowPointConnection : PoolableDrawable
{
// Todo: These shouldn't be constants
private const int spacing = 32;
private const double preempt = 800;
public const int SPACING = 32;
public const double PREEMPT = 800;
public override bool RemoveWhenNotAlive => false;
public FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool;
/// <summary>
/// The start time of <see cref="Start"/>.
/// </summary>
public readonly Bindable<double> StartTime = new BindableDouble();
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.
/// </summary>
[NotNull]
public readonly OsuHitObject Start;
/// <summary>
/// Creates a new <see cref="FollowPointConnection"/>.
/// </summary>
/// <param name="start">The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.</param>
public FollowPointConnection([NotNull] OsuHitObject start)
protected override void PrepareForUse()
{
Start = start;
base.PrepareForUse();
RelativeSizeAxes = Axes.Both;
Entry.Invalidated += onEntryInvalidated;
StartTime.BindTo(start.StartTimeBindable);
refreshPoints();
}
protected override void LoadComplete()
protected override void FreeAfterUse()
{
base.LoadComplete();
bindEvents(Start);
base.FreeAfterUse();
Entry.Invalidated -= onEntryInvalidated;
// Return points to the pool.
ClearInternal(false);
Entry = null;
}
private OsuHitObject end;
private void onEntryInvalidated() => refreshPoints();
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
/// </summary>
[CanBeNull]
public OsuHitObject End
private void refreshPoints()
{
get => end;
set
{
end = value;
ClearInternal(false);
if (end != null)
bindEvents(end);
OsuHitObject start = Entry.Start;
OsuHitObject end = Entry.End;
if (IsLoaded)
scheduleRefresh();
else
refresh();
}
}
double startTime = start.GetEndTime();
private void bindEvents(OsuHitObject obj)
{
obj.PositionBindable.BindValueChanged(_ => scheduleRefresh());
obj.DefaultsApplied += _ => scheduleRefresh();
}
private void scheduleRefresh()
{
Scheduler.AddOnce(refresh);
}
private void refresh()
{
double startTime = Start.GetEndTime();
LifetimeStart = startTime;
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
{
// ensure we always set a lifetime for full LifetimeManagementContainer benefits
LifetimeEnd = LifetimeStart;
return;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
double endTime = End.StartTime;
Vector2 startPosition = start.StackedEndPosition;
Vector2 endPosition = end.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime;
double? firstTransformStartTime = null;
double finalTransformEndTime = startTime;
int point = 0;
ClearInternal();
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
{
float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - preempt;
GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
FollowPoint fp;
AddInternal(fp = new FollowPoint());
Debug.Assert(End != null);
AddInternal(fp = Pool.Get());
fp.ClearTransforms();
fp.Position = pointStartPosition;
fp.Rotation = rotation;
fp.Alpha = 0;
fp.Scale = new Vector2(1.5f * End.Scale);
firstTransformStartTime ??= fadeInTime;
fp.Scale = new Vector2(1.5f * end.Scale);
fp.AnimationStartTime = fadeInTime;
using (fp.BeginAbsoluteSequence(fadeInTime))
{
fp.FadeIn(End.TimeFadeIn);
fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn);
fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn);
finalTransformEndTime = fadeOutTime + End.TimeFadeIn;
finalTransformEndTime = fadeOutTime + end.TimeFadeIn;
}
point++;
}
int excessPoints = InternalChildren.Count - point;
for (int i = 0; i < excessPoints; i++)
RemoveInternal(InternalChildren[^1]);
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
LifetimeStart = firstTransformStartTime ?? startTime;
LifetimeEnd = finalTransformEndTime;
Entry.LifetimeEnd = finalTransformEndTime;
}
/// <summary>
/// Computes the fade time of follow point positioned between two hitobjects.
/// </summary>
/// <param name="start">The first <see cref="OsuHitObject"/>, where follow points should originate from.</param>
/// <param name="end">The second <see cref="OsuHitObject"/>, which follow points should target.</param>
/// <param name="fraction">The fractional distance along <paramref name="start"/> and <paramref name="end"/> at which the follow point is to be located.</param>
/// <param name="fadeInTime">The fade-in time of the follow point/</param>
/// <param name="fadeOutTime">The fade-out time of the follow point.</param>
public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime)
{
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
fadeOutTime = startTime + fraction * duration;
fadeInTime = fadeOutTime - PREEMPT;
}
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
public class FollowPointLifetimeEntry : LifetimeEntry
{
public event Action Invalidated;
public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start)
{
Start = start;
LifetimeStart = Start.StartTime;
bindEvents();
}
private OsuHitObject end;
public OsuHitObject End
{
get => end;
set
{
UnbindEvents();
end = value;
bindEvents();
refreshLifetimes();
}
}
private void bindEvents()
{
UnbindEvents();
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
Start.DefaultsApplied += onDefaultsApplied;
Start.PositionBindable.ValueChanged += onPositionChanged;
if (End != null)
{
End.DefaultsApplied += onDefaultsApplied;
End.PositionBindable.ValueChanged += onPositionChanged;
}
}
public void UnbindEvents()
{
if (Start != null)
{
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null)
{
End.DefaultsApplied -= onDefaultsApplied;
End.PositionBindable.ValueChanged -= onPositionChanged;
}
}
private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
private void onPositionChanged(ValueChangedEvent<Vector2> obj) => refreshLifetimes();
private void refreshLifetimes()
{
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
{
LifetimeEnd = LifetimeStart;
return;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
// The lifetime start will match the fade-in time of the first follow point.
float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
LifetimeStart = fadeInTime;
LifetimeEnd = double.MaxValue; // This will be set by the connection.
Invalidated?.Invoke();
}
}
}

View File

@ -2,53 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
/// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary>
public class FollowPointRenderer : LifetimeManagementContainer
public class FollowPointRenderer : CompositeDrawable
{
/// <summary>
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
/// </summary>
internal IReadOnlyList<FollowPointConnection> Connections => connections;
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
public override bool RemoveCompletedTransforms => false;
/// <summary>
/// Adds the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>.
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
public void AddFollowPoints(OsuHitObject hitObject)
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
/// <summary>
/// Removes the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>.
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
private DrawablePool<FollowPointConnection> connectionPool;
private DrawablePool<FollowPoint> pointPool;
/// <summary>
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
private void addConnection(FollowPointConnection connection)
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
public FollowPointRenderer()
{
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) =>
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
connectionPool = new DrawablePoolNoLifetime<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000)
};
}
public void AddFollowPoints(OsuHitObject hitObject)
{
addEntry(hitObject);
var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
startTimeMap[hitObject] = startTimeBindable;
}
public void RemoveFollowPoints(OsuHitObject hitObject)
{
removeEntry(hitObject);
startTimeMap[hitObject].UnbindAll();
startTimeMap.Remove(hitObject);
}
private void addEntry(OsuHitObject hitObject)
{
var newEntry = new FollowPointLifetimeEntry(hitObject);
var index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
{
int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
if (comp != 0)
return comp;
@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
return -1;
}));
if (index < connections.Count - 1)
if (index < lifetimeEntries.Count - 1)
{
// Update the connection's end point to the next connection's start point
// h1 -> -> -> h2
// connection nextGroup
FollowPointConnection nextConnection = connections[index + 1];
connection.End = nextConnection.Start;
FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
newEntry.End = nextEntry.Start;
}
else
{
// The end point may be non-null during re-ordering
connection.End = null;
newEntry.End = null;
}
if (index > 0)
@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2
// prevGroup connection
FollowPointConnection previousConnection = connections[index - 1];
previousConnection.End = connection.Start;
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousEntry.End = newEntry.Start;
}
AddInternal(connection);
lifetimeManager.AddEntry(newEntry);
}
/// <summary>
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
private void removeGroup(FollowPointConnection connection)
private void removeEntry(OsuHitObject hitObject)
{
RemoveInternal(connection);
int index = lifetimeEntries.FindIndex(e => e.Start == hitObject);
int index = connections.IndexOf(connection);
var entry = lifetimeEntries[index];
entry.UnbindEvents();
lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry);
if (index > 0)
{
@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection
FollowPointConnection previousConnection = connections[index - 1];
previousConnection.End = connection.End;
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousEntry.End = entry.End;
}
connections.Remove(connection);
}
private void onStartTimeChanged(FollowPointConnection connection)
protected override bool CheckChildrenLife()
{
// Naive but can be improved if performance becomes an issue
removeGroup(connection);
addConnection(connection);
bool anyAliveChanged = base.CheckChildrenLife();
anyAliveChanged |= lifetimeManager.Update(Time.Current);
return anyAliveChanged;
}
private void onEntryBecameAlive(LifetimeEntry entry)
{
var connection = connectionPool.Get(c =>
{
c.Entry = (FollowPointLifetimeEntry)entry;
c.Pool = pointPool;
});
connectionsInUse[entry] = connection;
AddInternal(connection);
}
private void onEntryBecameDead(LifetimeEntry entry)
{
RemoveInternal(connectionsInUse[entry]);
connectionsInUse.Remove(entry);
}
private void onStartTimeChanged(OsuHitObject hitObject)
{
removeEntry(hitObject);
addEntry(hitObject);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var entry in lifetimeEntries)
entry.UnbindEvents();
lifetimeEntries.Clear();
}
private class DrawablePoolNoLifetime<T> : DrawablePool<T>
where T : PoolableDrawable, new()
{
public override bool RemoveWhenNotAlive => false;
public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
}
}
}
}

View File

@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.Delay(800).FadeOut();
break;
}
Expire();
}
public Drawable ProxiedLayer => ApproachCircle;

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindTo(HitObject.PositionBindable);
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
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(HitObject hitObject)
@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// Manually set to reduce the number of future alive objects to a bare minimum.
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
}
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>

View File

@ -4,9 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osuTK;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved]
private OsuConfigManager config { get; set; }
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
}
public DrawableOsuJudgement()
{
}
[BackgroundDependencyLoader]
private void load()
{
@ -39,48 +30,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
{
base.Apply(result, judgedObject);
if (judgedObject?.HitObject is OsuHitObject osuObject)
{
Position = osuObject.StackedPosition;
Scale = new Vector2(osuObject.Scale);
}
}
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
}
private double fadeOutDelay;
protected override double FadeOutDelay => fadeOutDelay;
if (JudgedObject?.HitObject is OsuHitObject osuObject)
{
Position = osuObject.StackedPosition;
Scale = new Vector2(osuObject.Scale);
}
}
protected override void ApplyHitAnimations()
{
bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting);
if (hitLightingEnabled)
{
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
Lighting.Alpha = 0;
if (hitLightingEnabled && Lighting.Drawable != null)
{
// todo: this animation changes slightly based on new/old legacy skin versions.
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
}
else
{
JudgementBody.Alpha = 1;
// extend the lifetime to cover lighting fade
LifetimeEnd = Lighting.LatestTransformEndTime;
}
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
private class OsuJudgementPiece : DefaultJudgementPiece
{
public OsuJudgementPiece(HitResult result)
: base(result)
{
}
public override void PlayAnimation()
{
base.PlayAnimation();
if (Result != HitResult.Miss)
JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
}
}
}
}

View File

@ -193,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
public readonly Bindable<bool> Tracking = new Bindable<bool>();
protected override void Update()
@ -273,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples();
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
this.FadeOut(fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;

View File

@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
this.FadeOut(160);
this.FadeOut(160).Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();

View File

@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class CirclePiece : CompositeDrawable
{
[Resolved]
private DrawableHitObject drawableObject { get; set; }
private TrianglesPiece triangles;
public CirclePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
private void load(TextureStore textures, DrawableHitObject drawableHitObject)
private void load(TextureStore textures)
{
InternalChildren = new Drawable[]
{
@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
new TrianglesPiece(drawableHitObject.GetHashCode())
triangles = new TrianglesPiece
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Alpha = 0.5f,
}
};
drawableObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableObject);
}
private void onHitObjectApplied(DrawableHitObject obj)
{
if (obj.HitObject == null)
return;
triangles.Reset((int)obj.HitObject.StartTime);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}

View File

@ -1,14 +1,21 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ExplodePiece : Container
{
[Resolved]
private DrawableHitObject drawableObject { get; set; }
private TrianglesPiece triangles;
public ExplodePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Blending = BlendingParameters.Additive;
Alpha = 0;
}
Child = new TrianglesPiece
[BackgroundDependencyLoader]
private void load()
{
Child = triangles = new TrianglesPiece
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
};
drawableObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableObject);
}
private void onHitObjectApplied(DrawableHitObject obj)
{
if (obj.HitObject == null)
return;
triangles.Reset((int)obj.HitObject.StartTime);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}

View File

@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class TrianglesPiece : Triangles
{
protected override bool ExpireOffScreenTriangles => false;
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@ -20,7 +19,6 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
@ -40,44 +38,21 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
private readonly Bindable<bool> playfieldBorderStyle = new BindableBool();
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
Depth = 3
},
spinnerProxies = new ProxyContainer
{
RelativeSizeAxes = Axes.Both
},
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
// Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
// Todo: Remove when hitobjects are properly pooled
new SkinProvidingContainer(null)
{
Child = HitObjectContainer,
},
approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
HitObjectContainer,
judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
@ -86,13 +61,18 @@ namespace osu.Game.Rulesets.Osu.UI
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result));
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
AddRangeInternal(poolDictionary.Values);
NewResult += onNewResult;
}
private void onJudgmentLoaded(DrawableOsuJudgement judgement)
{
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
@ -178,11 +158,13 @@ namespace osu.Game.Rulesets.Osu.UI
private class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
{
private readonly HitResult result;
private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result)
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10)
{
this.result = result;
this.onLoaded = onLoaded;
}
protected override DrawableOsuJudgement CreateNewDrawable()
@ -192,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.UI
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
onLoaded?.Invoke(judgement);
return judgement;
}
}

View File

@ -1,12 +1,9 @@
// 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.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
switch (Result.Type)
{
case HitResult.Ok:
JudgementBody.Colour = colours.GreenLight;
break;
case HitResult.Great:
JudgementBody.Colour = colours.BlueLight;
break;
}
}
protected override void ApplyHitAnimations()
{
this.MoveToY(-100, 500);

View File

@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background
private class FadeAccessibleResults : ResultsScreen
{
public FadeAccessibleResults(ScoreInfo score)
: base(score)
: base(score, true)
{
}

View File

@ -0,0 +1,39 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneParticleExplosion : OsuTestScene
{
private ParticleExplosion explosion;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
AddStep("create initial", () =>
{
Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(400)
};
});
AddWaitStep("wait for playback", 5);
AddRepeatStep(@"restart animation", () =>
{
explosion.Restart();
}, 10);
}
}
}

View File

@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score)
: base(score)
: base(score, true)
{
}
@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score)
: base(score, true)
{
Score.Beatmap.OnlineBeatmapID = 0;
Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;

View File

@ -11,12 +11,12 @@ using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePaginatedContainerHeader : OsuTestScene
public class TestSceneProfileSubsectionHeader : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private PaginatedContainerHeader header;
private ProfileSubsectionHeader header;
[Test]
public void TestHiddenCounter()
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
{
Clear();
Add(header = new PaginatedContainerHeader(text, state)
Add(header = new ProfileSubsectionHeader(text, state)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -15,103 +14,93 @@ namespace osu.Game.Database
{
public class UserLookupCache : MemoryCachingComponent<int, User>
{
private readonly HashSet<int> nextTaskIDs = new HashSet<int>();
[Resolved]
private IAPIProvider api { get; set; }
private readonly object taskAssignmentLock = new object();
private Task<List<User>> pendingRequest;
/// <summary>
/// Whether <see cref="pendingRequest"/> has already grabbed its IDs.
/// </summary>
private bool pendingRequestConsumedIDs;
public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
var users = await getQueryTaskForUser(lookup);
return users.FirstOrDefault(u => u.Id == lookup);
}
=> await queryUser(lookup);
/// <summary>
/// Return the task responsible for fetching the provided user.
/// This may be part of a larger batch lookup to reduce web requests.
/// </summary>
/// <param name="userId">The user to lookup.</param>
/// <returns>The task responsible for the lookup.</returns>
private Task<List<User>> getQueryTaskForUser(int userId)
private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
private Task pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<User> queryUser(int userId)
{
lock (taskAssignmentLock)
{
nextTaskIDs.Add(userId);
var tcs = new TaskCompletionSource<User>();
// if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it.
if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50)
return pendingRequest;
// Add to the queue.
pendingUserTasks.Enqueue((userId, tcs));
return queueNextTask(nextLookup);
// Create a request task if there's not already one.
if (pendingRequestTask == null)
createNewTask();
return tcs.Task;
}
}
List<User> nextLookup()
private void performLookup()
{
// contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
var userTasks = new Dictionary<int, List<TaskCompletionSource<User>>>();
// Grab at most 50 unique user IDs from the queue.
lock (taskAssignmentLock)
{
int[] lookupItems;
lock (taskAssignmentLock)
while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
{
pendingRequestConsumedIDs = true;
lookupItems = nextTaskIDs.ToArray();
nextTaskIDs.Clear();
(int id, TaskCompletionSource<User> task) next = pendingUserTasks.Dequeue();
if (lookupItems.Length == 0)
// Perform a secondary check for existence, in case the user was queried in a previous batch.
if (CheckExists(next.id, out var existing))
next.task.SetResult(existing);
else
{
queueNextTask(null);
return new List<User>();
if (userTasks.TryGetValue(next.id, out var tasks))
tasks.Add(next.task);
else
userTasks[next.id] = new List<TaskCompletionSource<User>> { next.task };
}
}
var request = new GetUsersRequest(lookupItems);
// rather than queueing, we maintain our own single-threaded request stream.
api.Perform(request);
return request.Result?.Users;
}
}
/// <summary>
/// Queues new work at the end of the current work tasks.
/// Ensures the provided work is eventually run.
/// </summary>
/// <param name="work">The work to run. Can be null to signify the end of available work.</param>
/// <returns>The task tracking this work.</returns>
private Task<List<User>> queueNextTask(Func<List<User>> work)
{
// Query the users.
var request = new GetUsersRequest(userTasks.Keys.ToArray());
// rather than queueing, we maintain our own single-threaded request stream.
api.Perform(request);
// Create a new request task if there's still more users to query.
lock (taskAssignmentLock)
{
if (work == null)
{
pendingRequest = null;
pendingRequestConsumedIDs = false;
}
else if (pendingRequest == null)
{
// special case for the first request ever.
pendingRequest = Task.Run(work);
pendingRequestConsumedIDs = false;
}
else
{
// append the new request on to the last to be executed.
pendingRequest = pendingRequest.ContinueWith(_ => work());
pendingRequestConsumedIDs = false;
}
pendingRequestTask = null;
if (pendingUserTasks.Count > 0)
createNewTask();
}
return pendingRequest;
foreach (var user in request.Result.Users)
{
if (userTasks.TryGetValue(user.Id, out var tasks))
{
foreach (var task in tasks)
task.SetResult(user);
userTasks.Remove(user.Id);
}
}
// if any tasks remain which were not satisfied, return null.
foreach (var tasks in userTasks.Values)
{
foreach (var task in tasks)
task.SetResult(null);
}
}
private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
}
}

View File

@ -60,6 +60,7 @@ namespace osu.Game.Graphics.Backgrounds
/// <summary>
/// Whether we want to expire triangles as they exit our draw area completely.
/// </summary>
[Obsolete("Unused.")] // Can be removed 20210518
protected virtual bool ExpireOffScreenTriangles => true;
/// <summary>
@ -86,12 +87,9 @@ namespace osu.Game.Graphics.Backgrounds
/// </summary>
public float Velocity = 1;
private readonly Random stableRandom;
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
private Random stableRandom;
private IShader shader;
private readonly Texture texture;
@ -172,7 +170,20 @@ namespace osu.Game.Graphics.Backgrounds
}
}
protected int AimCount;
/// <summary>
/// Clears and re-initialises triangles according to a given seed.
/// </summary>
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
public void Reset(int? seed = null)
{
if (seed != null)
stableRandom = new Random(seed.Value);
parts.Clear();
addTriangles(true);
}
protected int AimCount { get; private set; }
private void addTriangles(bool randomY)
{
@ -226,6 +237,8 @@ namespace osu.Game.Graphics.Backgrounds
}
}
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this);
private class TrianglesDrawNode : DrawNode

View File

@ -0,0 +1,144 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Graphics
{
/// <summary>
/// An explosion of textured particles based on how osu-stable randomises the explosion pattern.
/// </summary>
public class ParticleExplosion : Sprite
{
private readonly int particleCount;
private readonly double duration;
private double startTime;
private readonly List<ParticlePart> parts = new List<ParticlePart>();
public ParticleExplosion(Texture texture, int particleCount, double duration)
{
Texture = texture;
this.particleCount = particleCount;
this.duration = duration;
Blending = BlendingParameters.Additive;
}
protected override void LoadComplete()
{
base.LoadComplete();
Restart();
}
/// <summary>
/// Restart the animation from the current point in time.
/// Supports transform time offset chaining.
/// </summary>
public void Restart()
{
startTime = TransformStartTime;
this.FadeOutFromOne(duration);
parts.Clear();
for (int i = 0; i < particleCount; i++)
parts.Add(new ParticlePart(duration));
}
protected override void Update()
{
base.Update();
Invalidate(Invalidation.DrawNode);
}
protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this);
private class ParticleExplosionDrawNode : SpriteDrawNode
{
private readonly List<ParticlePart> parts = new List<ParticlePart>();
private ParticleExplosion source => (ParticleExplosion)Source;
private double startTime;
private double currentTime;
private Vector2 sourceSize;
public ParticleExplosionDrawNode(Sprite source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
parts.Clear();
parts.AddRange(source.parts);
sourceSize = source.Size;
startTime = source.startTime;
currentTime = source.Time.Current;
}
protected override void Blit(Action<TexturedVertex2D> vertexAction)
{
var time = currentTime - startTime;
foreach (var p in parts)
{
Vector2 pos = p.PositionAtTime(time);
float alpha = p.AlphaAtTime(time);
var rect = new RectangleF(
pos.X * sourceSize.X - Texture.DisplayWidth / 2,
pos.Y * sourceSize.Y - Texture.DisplayHeight / 2,
Texture.DisplayWidth,
Texture.DisplayHeight);
// convert to screen space.
var quad = new Quad(
Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
);
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
null, TextureCoords);
}
}
}
private readonly struct ParticlePart
{
private readonly double duration;
private readonly float direction;
private readonly float distance;
public ParticlePart(double availableDuration)
{
distance = RNG.NextSingle(0.5f);
duration = RNG.NextDouble(availableDuration / 3, availableDuration);
direction = RNG.NextSingle(0, MathF.PI * 2);
}
public float AlphaAtTime(double time) => 1 - progressAtTime(time);
public Vector2 PositionAtTime(double time)
{
var travelledDistance = distance * progressAtTime(time);
return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction));
}
private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1);
}
}
}

View File

@ -420,7 +420,7 @@ namespace osu.Game
break;
case ScorePresentType.Results:
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo));
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
}, validScreens: new[] { typeof(PlaySongSelect) });

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{
public class PaginatedBeatmapContainer : PaginatedContainer<APIBeatmapSet>
public class PaginatedBeatmapContainer : PaginatedProfileSubsection<APIBeatmapSet>
{
private const float panel_padding = 10f;
private readonly BeatmapSetType type;

View File

@ -13,7 +13,7 @@ using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer<APIUserMostPlayedBeatmap>
public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
{
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)

View File

@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
public class PaginatedKudosuHistoryContainer : PaginatedContainer<APIKudosuHistory>
public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection<APIKudosuHistory>
{
public PaginatedKudosuHistoryContainer(Bindable<User> user)
: base(user, missingText: "This user hasn't received any kudosu!")

View File

@ -6,62 +6,51 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract class PaginatedContainer<TModel> : FillFlowContainer
public abstract class PaginatedProfileSubsection<TModel> : ProfileSubsection
{
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
protected RulesetStore Rulesets { get; private set; }
protected int VisiblePages;
protected int ItemsPerPage;
protected readonly Bindable<User> User = new Bindable<User>();
protected FillFlowContainer ItemsContainer;
protected RulesetStore Rulesets;
protected FillFlowContainer ItemsContainer { get; private set; }
private APIRequest<List<TModel>> retrievalRequest;
private CancellationTokenSource loadCancellation;
private readonly string missingText;
private ShowMoreButton moreButton;
private OsuSpriteText missing;
private PaginatedContainerHeader header;
private readonly string missingText;
private readonly string headerText;
private readonly CounterVisibilityState counterVisibilityState;
protected PaginatedContainer(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
: base(user, headerText, counterVisibilityState)
{
this.headerText = headerText;
this.missingText = missingText;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
}
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
protected override Drawable CreateContent() => new FillFlowContainer
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
header = new PaginatedContainerHeader(headerText, counterVisibilityState)
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
},
ItemsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections
Font = OsuFont.GetFont(size: 15),
Text = missingText,
Alpha = 0,
},
};
}
}
};
Rulesets = rulesets;
User.ValueChanged += onUserChanged;
User.TriggerChange();
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent<User> e)
@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide();
moreButton.IsLoading = false;
if (!string.IsNullOrEmpty(missing.Text))
if (!string.IsNullOrEmpty(missingText))
missing.Show();
return;
@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections
protected virtual int GetCount(User user) => 0;
protected void SetCount(int value) => header.Current.Value = value;
protected virtual void OnItemsReceived(List<TModel> items)
{
}
@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
retrievalRequest?.Cancel();
loadCancellation?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -0,0 +1,51 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
using JetBrains.Annotations;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract class ProfileSubsection : FillFlowContainer
{
protected readonly Bindable<User> User = new Bindable<User>();
private readonly string headerText;
private readonly CounterVisibilityState counterVisibilityState;
private ProfileSubsectionHeader header;
protected ProfileSubsection(Bindable<User> user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{
this.headerText = headerText;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Children = new[]
{
header = new ProfileSubsectionHeader(headerText, counterVisibilityState)
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
},
CreateContent()
};
}
[NotNull]
protected abstract Drawable CreateContent();
protected void SetCount(int value) => header.Current.Value = value;
}
}

View File

@ -14,7 +14,7 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue<int>
public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue<int>
{
private readonly BindableWithCurrent<int> current = new BindableWithCurrent<int>();
@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections
private CounterPill counterPill;
public PaginatedContainerHeader(string text, CounterVisibilityState counterState)
public ProfileSubsectionHeader(string text, CounterVisibilityState counterState)
{
this.text = text;
this.counterState = counterState;

View File

@ -14,7 +14,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
public class PaginatedScoreContainer : PaginatedContainer<APILegacyScoreInfo>
public class PaginatedScoreContainer : PaginatedProfileSubsection<APILegacyScoreInfo>
{
private readonly ScoreType type;

View File

@ -13,7 +13,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
public class PaginatedRecentActivityContainer : PaginatedContainer<APIRecentActivity>
public class PaginatedRecentActivityContainer : PaginatedProfileSubsection<APIRecentActivity>
{
public PaginatedRecentActivityContainer(Bindable<User> user)
: base(user, missingText: "This user hasn't done anything notable recently!")

View File

@ -0,0 +1,75 @@
// 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.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Judgements
{
public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; }
[Resolved]
private OsuColour colours { get; set; }
public DefaultJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Font = OsuFont.Numeric.With(size: 20),
Scale = new Vector2(0.85f, 1),
}
};
}
public virtual void PlayAnimation()
{
switch (Result)
{
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
default:
this.ScaleTo(0.9f);
this.ScaleTo(1, 500, Easing.OutElastic);
break;
}
this.FadeOutFromOne(800);
}
public Drawable GetAboveHitObjectsProxiedContent() => null;
}
}

View File

@ -1,20 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Judgements
{
@ -25,25 +22,29 @@ namespace osu.Game.Rulesets.Judgements
{
private const float judgement_size = 128;
[Resolved]
private OsuColour colours { get; set; }
public JudgementResult Result { get; private set; }
public DrawableHitObject JudgedObject { get; private set; }
protected Container JudgementBody { get; private set; }
protected SpriteText JudgementText { get; private set; }
public override bool RemoveCompletedTransforms => false;
private SkinnableDrawable bodyDrawable;
protected SkinnableDrawable JudgementBody { get; private set; }
private readonly Container aboveHitObjectsContent;
[Resolved]
private ISkinSource skinSource { get; set; }
/// <summary>
/// Duration of initial fade in.
/// </summary>
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeInDuration => 100;
/// <summary>
/// Duration to wait until fade out begins. Defaults to <see cref="FadeInDuration"/>.
/// </summary>
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
protected virtual double FadeOutDelay => FadeInDuration;
/// <summary>
@ -61,6 +62,12 @@ namespace osu.Game.Rulesets.Judgements
{
Size = new Vector2(judgement_size);
Origin = Anchor.Centre;
AddInternal(aboveHitObjectsContent = new Container
{
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both
});
}
[BackgroundDependencyLoader]
@ -69,15 +76,61 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
}
protected virtual void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.9f);
JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
this.Delay(FadeOutDelay).FadeOut(400);
protected override void LoadComplete()
{
base.LoadComplete();
skinSource.SourceChanged += onSkinChanged;
}
public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
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>
/// 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.
/// </summary>
/// <remarks>
/// For animating the actual "default skin" judgement itself, it is recommended to use <see cref="CreateDefaultJudgement"/>.
/// This allows applying animations which don't affect custom skins.
/// </remarks>
protected virtual void ApplyHitAnimations()
{
}
/// <summary>
/// Apply top-level animations to the current judgement when missed.
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
/// </summary>
/// <remarks>
/// For animating the actual "default skin" judgement itself, it is recommended to use <see cref="CreateDefaultJudgement"/>.
/// This allows applying animations which don't affect custom skins.
/// </remarks>
protected virtual void ApplyMissAnimations()
{
}
/// <summary>
/// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool.
/// </summary>
/// <param name="result">The applicable judgement.</param>
/// <param name="judgedObject">The drawable object.</param>
public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
{
Result = result;
JudgedObject = judgedObject;
@ -91,34 +144,46 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
bodyDrawable.ResetAnimation();
runAnimation();
}
this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
JudgementBody.ScaleTo(1);
JudgementBody.RotateTo(0);
JudgementBody.MoveTo(Vector2.Zero);
private void runAnimation()
{
ClearTransforms(true);
LifetimeStart = Result.TimeAbsolute;
switch (Result.Type)
using (BeginAbsoluteSequence(Result.TimeAbsolute, true))
{
case HitResult.None:
break;
// not sure if this should remain going forward.
JudgementBody.ResetAnimation();
case HitResult.Miss:
JudgementBody.ScaleTo(1.6f);
JudgementBody.ScaleTo(1, 100, Easing.In);
switch (Result.Type)
{
case HitResult.None:
break;
JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
JudgementBody.RotateTo(40, 800, Easing.InQuint);
case HitResult.Miss:
ApplyMissAnimations();
break;
this.Delay(600).FadeOut(200);
break;
default:
ApplyHitAnimations();
break;
}
default:
ApplyHitAnimations();
break;
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var drawableAnimation = (Drawable)animatable;
animatable.PlayAnimation();
// a derived version of DrawableJudgement may be proposing a lifetime.
// if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
double lastTransformTime = drawableAnimation.LatestTransformEndTime;
if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
LifetimeEnd = lastTransformTime;
}
}
Expire(true);
}
private HitResult? currentDrawableType;
@ -127,6 +192,7 @@ namespace osu.Game.Rulesets.Judgements
{
var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset
// todo: this should be removed once judgements are always pooled.
if (type == currentDrawableType)
return;
@ -134,21 +200,24 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null)
RemoveInternal(JudgementBody);
AddInternal(JudgementBody = new Container
aboveHitObjectsContent.Clear();
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ => JudgementText = new OsuSpriteText
{
Text = type.GetDescription().ToUpperInvariant(),
Font = OsuFont.Numeric.With(size: 20),
Colour = colours.ForHitResult(type),
Scale = new Vector2(0.85f, 1),
}, confineMode: ConfineMode.NoScaling)
});
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
if (proxiedContent != null)
aboveHitObjectsContent.Add(proxiedContent);
}
currentDrawableType = type;
}
protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);
}
}

View File

@ -0,0 +1,25 @@
// 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 JetBrains.Annotations;
using osu.Framework.Graphics;
namespace osu.Game.Rulesets.Judgements
{
/// <summary>
/// A skinnable judgement element which supports playing an animation from the current point in time.
/// </summary>
public interface IAnimatableJudgement : IDrawable
{
/// <summary>
/// Start the animation for this judgement from the current point in time.
/// </summary>
void PlayAnimation();
/// <summary>
/// Get proxied content which should be displayed above all hitobjects.
/// </summary>
[CanBeNull]
Drawable GetAboveHitObjectsProxiedContent();
}
}

View File

@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
[Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
// ReSharper disable once RedundantOverload.Global
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset)
{
return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
}
// ReSharper disable once MethodOverloadWithOptionalParameter
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default)

View File

@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
/// <summary>
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
@ -338,7 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether a selection was performed.</returns>
private bool beginClickSelection(MouseButtonEvent e)
{
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
{
if (!blueprint.IsHovered) continue;

View File

@ -0,0 +1,77 @@
// 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.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container for <see cref="SelectionBlueprint"/> ordered by their <see cref="HitObject"/> start times.
/// </summary>
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint>
{
public override void Add(SelectionBlueprint drawable)
{
base.Add(drawable);
bindStartTime(drawable);
}
public override bool Remove(SelectionBlueprint drawable)
{
if (!base.Remove(drawable))
return false;
unbindStartTime(drawable);
return true;
}
public override void Clear(bool disposeChildren)
{
base.Clear(disposeChildren);
unbindAllStartTimes();
}
private readonly Dictionary<SelectionBlueprint, IBindable> startTimeMap = new Dictionary<SelectionBlueprint, IBindable>();
private void bindStartTime(SelectionBlueprint blueprint)
{
var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
bindable.BindValueChanged(_ =>
{
if (LoadState >= LoadState.Ready)
SortInternal();
});
startTimeMap[blueprint] = bindable;
}
private void unbindStartTime(SelectionBlueprint blueprint)
{
startTimeMap[blueprint].UnbindAll();
startTimeMap.Remove(blueprint);
}
private void unbindAllStartTimes()
{
foreach (var kvp in startTimeMap)
kvp.Value.UnbindAll();
startTimeMap.Clear();
}
protected override int Compare(Drawable x, Drawable y)
{
var xObj = (SelectionBlueprint)x;
var yObj = (SelectionBlueprint)y;
// Put earlier blueprints towards the end of the list, so they handle input first
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
return i == 0 ? CompareReverseChildID(x, y) : i;
}
}
}

View File

@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}

View File

@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Compose
composer = ruleset?.CreateHitObjectComposer();
// make the composer available to the timeline and other components in this screen.
dependencies.CacheAs(composer);
if (composer != null)
dependencies.CacheAs(composer);
return dependencies;
}

View File

@ -92,7 +92,7 @@ namespace osu.Game.Screens.Multi.Play
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(roomId.Value != null);
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem);
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
}
protected override ScoreInfo CreateScore()

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking
[Resolved]
private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry)
: base(score, allowRetry)
{
this.roomId = roomId;

View File

@ -545,7 +545,7 @@ namespace osu.Game.Screens.Play
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score);
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
#region Fail Logic

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play
public class SpectatorResultsScreen : SoloResultsScreen
{
public SpectatorResultsScreen(ScoreInfo score)
: base(score)
: base(score, false)
{
}

View File

@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
@ -57,7 +58,7 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry;
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
protected ResultsScreen(ScoreInfo score, bool allowRetry)
{
Score = score;
this.allowRetry = allowRetry;
@ -149,7 +150,12 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
ScorePanelList.AddScore(Score, true);
{
// only show flair / animation when arriving after watching a play that isn't autoplay.
bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay);
ScorePanelList.AddScore(Score, shouldFlair);
}
if (player != null && allowRetry)
{

View File

@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private RulesetStore rulesets { get; set; }
public SoloResultsScreen(ScoreInfo score, bool allowRetry = true)
public SoloResultsScreen(ScoreInfo score, bool allowRetry)
: base(score, allowRetry)
{
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
}
protected void PresentScore(ScoreInfo score) =>
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score)));
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();

View File

@ -0,0 +1,127 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Skinning
{
public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement
{
private readonly HitResult result;
private readonly LegacyJudgementPieceOld temporaryOldStyle;
private readonly Drawable mainPiece;
private readonly ParticleExplosion particles;
public LegacyJudgementPieceNew(HitResult result, Func<Drawable> createMainDrawable, Texture particleTexture)
{
this.result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChildren = new[]
{
mainPiece = createMainDrawable().With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
})
};
if (particleTexture != null)
{
AddInternal(particles = new ParticleExplosion(particleTexture, 150, 1600)
{
Size = new Vector2(140),
Depth = float.MaxValue,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
if (result != HitResult.Miss)
{
//new judgement shows old as a temporary effect
AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f)
{
Blending = BlendingParameters.Additive,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
}
public void PlayAnimation()
{
var animation = mainPiece as IFramedAnimation;
animation?.GotoFrame(0);
if (particles != null)
{
// start the particles already some way into their animation to break cluster away from centre.
using (particles.BeginDelayedSequence(-100, true))
particles.Restart();
}
const double fade_in_length = 120;
const double fade_out_delay = 500;
const double fade_out_length = 600;
this.FadeInFromZero(fade_in_length);
this.Delay(fade_out_delay).FadeOut(fade_out_length);
// new style non-miss judgements show the original style temporarily, with additive colour.
if (temporaryOldStyle != null)
{
temporaryOldStyle.PlayAnimation();
temporaryOldStyle.Hide();
temporaryOldStyle.Delay(-16)
.FadeTo(0.5f, 56, Easing.Out).Then()
.FadeOut(300);
}
// legacy judgements don't play any transforms if they are an animation.
if (animation?.FrameCount > 1)
return;
switch (result)
{
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
//todo: this only applies to osu! ruleset apparently.
this.MoveTo(new Vector2(0, -2));
this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In);
float rotation = RNG.NextSingle(-8.6f, 8.6f);
this.RotateTo(0);
this.RotateTo(rotation, fade_in_length)
.Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
break;
default:
mainPiece.ScaleTo(0.9f);
mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length);
break;
}
}
public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects.
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Skinning
{
public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement
{
private readonly HitResult result;
private readonly float finalScale;
public LegacyJudgementPieceOld(HitResult result, Func<Drawable> createMainDrawable, float finalScale = 1f)
{
this.result = result;
this.finalScale = finalScale;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChild = createMainDrawable();
}
public virtual void PlayAnimation()
{
var animation = InternalChild as IFramedAnimation;
animation?.GotoFrame(0);
const double fade_in_length = 120;
const double fade_out_delay = 500;
const double fade_out_length = 600;
this.FadeInFromZero(fade_in_length);
this.Delay(fade_out_delay).FadeOut(fade_out_length);
// legacy judgements don't play any transforms if they are an animation.
if (animation?.FrameCount > 1)
return;
switch (result)
{
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
float rotation = RNG.NextSingle(-8.6f, 8.6f);
this.RotateTo(0);
this.RotateTo(rotation, fade_in_length)
.Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
break;
default:
this.ScaleTo(0.6f).Then()
.ScaleTo(1.1f, fade_in_length * 0.8f).Then()
// this is actually correct to match stable; there were overlapping transforms.
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
break;
}
}
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
}
}

View File

@ -371,19 +371,15 @@ namespace osu.Game.Skinning
}
case GameplaySkinComponent<HitResult> resultComponent:
switch (resultComponent.Component)
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now.
if (createDrawable() != null)
{
case HitResult.Miss:
return this.GetAnimation("hit0", true, false);
case HitResult.Meh:
return this.GetAnimation("hit50", true, false);
case HitResult.Ok:
return this.GetAnimation("hit100", true, false);
case HitResult.Great:
return this.GetAnimation("hit300", true, false);
if (Configuration.LegacyVersion > 1)
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component));
else
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
break;
@ -392,6 +388,43 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
private Texture getParticleTexture(HitResult result)
{
switch (result)
{
case HitResult.Meh:
return GetTexture("particle50");
case HitResult.Ok:
return GetTexture("particle100");
case HitResult.Great:
return GetTexture("particle300");
}
return null;
}
private Drawable getJudgementAnimation(HitResult result)
{
switch (result)
{
case HitResult.Miss:
return this.GetAnimation("hit0", true, false);
case HitResult.Meh:
return this.GetAnimation("hit50", true, false);
case HitResult.Ok:
return this.GetAnimation("hit100", true, false);
case HitResult.Great:
return this.GetAnimation("hit300", true, false);
}
return null;
}
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (var name in getFallbackNames(componentName))

View File

@ -24,7 +24,15 @@ namespace osu.Game.Skinning
{
}
protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) };
protected override Drawable CreateDefault(ISkinComponent component)
{
var texture = textures.Get(component.LookupName);
if (texture == null)
return null;
return new Sprite { Texture = texture };
}
private class SpriteComponent : ISkinComponent
{

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1120.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />