1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 10:52:53 +08:00

Merge remote-tracking branch 'origin/master' into on-drawable-hitobject-added

This commit is contained in:
ekrctb 2020-11-21 15:25:16 +09:00
commit 295ca38cda
23 changed files with 952 additions and 352 deletions

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

@ -5,6 +5,7 @@ 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,42 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
protected override double FadeInDuration => 50;
protected override void ApplyMissAnimations()
{
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);
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 ManiaJudgementPiece(result);
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private class ManiaJudgementPiece : DefaultJudgementPiece
private class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
public ManiaJudgementPiece(HitResult result)
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}

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

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

@ -44,26 +44,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
private double fadeOutDelay;
protected override double FadeOutDelay => fadeOutDelay;
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;
}
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
// extend the lifetime to cover lighting fade
LifetimeEnd = Lighting.LatestTransformEndTime;
}
base.ApplyHitAnimations();
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Diagnostics;
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;
@ -21,7 +20,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
@ -41,44 +39,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);
@ -87,7 +62,7 @@ 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);
@ -117,6 +92,11 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
private void onJudgmentLoaded(DrawableOsuJudgement judgement)
{
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
@ -172,11 +152,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()
@ -186,6 +168,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

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

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

@ -47,18 +47,18 @@ namespace osu.Game.Rulesets.Judgements
public virtual void PlayAnimation()
{
this.RotateTo(0);
this.MoveTo(Vector2.Zero);
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:
@ -66,6 +66,10 @@ namespace osu.Game.Rulesets.Judgements
this.ScaleTo(1, 500, Easing.OutElastic);
break;
}
this.FadeOutFromOne(800);
}
public Drawable GetAboveHitObjectsProxiedContent() => null;
}
}

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;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -25,18 +26,25 @@ namespace osu.Game.Rulesets.Judgements
public DrawableHitObject JudgedObject { get; private set; }
protected Container JudgementBody { get; private set; }
public override bool RemoveCompletedTransforms => false;
private SkinnableDrawable skinnableJudgement;
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>
@ -54,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]
@ -62,10 +76,34 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
}
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
protected override void LoadComplete()
{
base.LoadComplete();
skinSource.SourceChanged += onSkinChanged;
}
private void onSkinChanged()
{
// on a skin change, the child component will update but not get correctly triggered to play its animation.
// we need to trigger a reinitialisation to make things right.
currentDrawableType = null;
PrepareForUse();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skinSource != null)
skinSource.SourceChanged -= onSkinChanged;
}
/// <summary>
/// Apply top-level animations to the current judgement when successfully hit.
/// Generally used for fading, defaulting to a simple fade out based on <see cref="FadeOutDelay"/>.
/// This will be used to calculate the lifetime of the judgement.
/// 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"/>.
@ -73,13 +111,11 @@ namespace osu.Game.Rulesets.Judgements
/// </remarks>
protected virtual void ApplyHitAnimations()
{
this.Delay(FadeOutDelay).FadeOut(400);
}
/// <summary>
/// Apply top-level animations to the current judgement when missed.
/// Generally used for fading, defaulting to a simple fade out based on <see cref="FadeOutDelay"/>.
/// This will be used to calculate the lifetime of the judgement.
/// 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"/>.
@ -87,7 +123,6 @@ namespace osu.Game.Rulesets.Judgements
/// </remarks>
protected virtual void ApplyMissAnimations()
{
this.Delay(600).FadeOut(200);
}
/// <summary>
@ -109,32 +144,46 @@ namespace osu.Game.Rulesets.Judgements
prepareDrawables();
// not sure if this should remain going forward.
skinnableJudgement.ResetAnimation();
runAnimation();
}
this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
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:
ApplyMissAnimations();
break;
switch (Result.Type)
{
case HitResult.None:
break;
default:
ApplyHitAnimations();
break;
}
case HitResult.Miss:
ApplyMissAnimations();
break;
default:
ApplyHitAnimations();
break;
}
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var drawableAnimation = (Drawable)animatable;
if (skinnableJudgement.Drawable is IAnimatableJudgement animatable)
{
using (BeginAbsoluteSequence(Result.TimeAbsolute))
animatable.PlayAnimation();
}
Expire(true);
// 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;
}
}
}
private HitResult? currentDrawableType;
@ -151,15 +200,21 @@ 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 = skinnableJudgement = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
});
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
if (proxiedContent != null)
aboveHitObjectsContent.Add(proxiedContent);
}
currentDrawableType = type;
}

View File

@ -1,13 +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
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

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

@ -1,58 +0,0 @@
// 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.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Skinning
{
public class LegacyJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
private readonly HitResult result;
public LegacyJudgementPiece(HitResult result, Drawable drawable)
{
this.result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
InternalChild = drawable;
}
public virtual void PlayAnimation()
{
var animation = InternalChild as IFramedAnimation;
animation?.GotoFrame(0);
this.RotateTo(0);
this.MoveTo(Vector2.Zero);
// 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);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(40, 800, Easing.InQuint);
break;
default:
this.ScaleTo(0.9f);
this.ScaleTo(1, 500, Easing.OutElastic);
break;
}
}
}
}

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,9 +371,16 @@ namespace osu.Game.Skinning
}
case GameplaySkinComponent<HitResult> resultComponent:
var drawable = getJudgementAnimation(resultComponent.Component);
if (drawable != null)
return new LegacyJudgementPiece(resultComponent.Component, drawable);
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component);
// kind of wasteful that we throw this away, but should do for now.
if (createDrawable() != null)
{
if (Configuration.LegacyVersion > 1)
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component));
else
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
break;
}
@ -381,6 +388,23 @@ 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)