Merge branch 'master' into hit-sample-pooling

This commit is contained in:
smoogipoo
2020-11-30 18:31:58 +09:00
154 changed files with 3046 additions and 1294 deletions

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

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

@ -1,63 +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 System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditPool<T> : DrawableOsuPool<T>
where T : DrawableHitObject, new()
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(checkHittable, onLoaded, initialSize, maximumSize)
{
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState);
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
}
}

View File

@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics.Pooling;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
@ -26,8 +29,51 @@ namespace osu.Game.Rulesets.Osu.Edit
{
protected override GameplayCursorContainer CreateCursor() => null;
protected override DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
=> new DrawableOsuEditPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
protected override void OnNewDrawableHitObject(DrawableHitObject d)
{
d.ApplyCustomUpdateState += updateState;
}
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
}
}
}

View File

@ -105,11 +105,20 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
{
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult;
return new SnapResult(screenSpacePosition, null);
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition);
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap;
// will be null if distance snap is disabled or not feasible for the current time value.
if (distanceSnapGrid == null)
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModEasy : ModEasy
public class OsuModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
}

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osuTK;
using osuTK.Graphics;
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 +17,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 +27,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,
@ -46,6 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
});
}
public double AnimationStartTime { get; set; }
public Bindable<double> AnimationStartTime { get; } = new BindableDouble();
}
}

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);
fp.Scale = new Vector2(1.5f * end.Scale);
firstTransformStartTime ??= fadeInTime;
fp.AnimationStartTime = fadeInTime;
fp.AnimationStartTime.Value = 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

@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
public class DrawableHitCircle : DrawableOsuHitObject
{
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;

View File

@ -53,9 +53,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
protected override void OnApply(HitObject hitObject)
protected override void OnApply()
{
base.OnApply(hitObject);
base.OnApply();
IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable);
PositionBindable.BindTo(HitObject.PositionBindable);
@ -70,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
}
protected override void OnFree(HitObject hitObject)
protected override void OnFree()
{
base.OnFree(hitObject);
base.OnFree();
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable);

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

@ -1,32 +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 System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuPool<T> : DrawablePool<T>
where T : DrawableHitObject, new()
{
private readonly Func<DrawableHitObject, double, bool> checkHittable;
private readonly Action<Drawable> onLoaded;
public DrawableOsuPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
this.checkHittable = checkHittable;
this.onLoaded = onLoaded;
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o =>
{
var osuObject = (DrawableOsuHitObject)(object)o;
osuObject.CheckHittable = checkHittable;
osuObject.OnLoadComplete += onLoaded;
});
}
}

View File

@ -20,7 +20,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
public class DrawableSlider : DrawableOsuHitObject
{
public new Slider HitObject => (Slider)base.HitObject;
@ -87,18 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
protected override void OnApply(HitObject hitObject)
protected override void OnApply()
{
base.OnApply(hitObject);
base.OnApply();
// Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version);
}
protected override void OnFree(HitObject hitObject)
protected override void OnFree()
{
base.OnFree(hitObject);
base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version);

View File

@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
@ -36,9 +35,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindValueChanged(_ => updatePosition());
}
protected override void OnFree(HitObject hitObject)
protected override void OnFree()
{
base.OnFree(hitObject);
base.OnFree();
pathVersion.UnbindFrom(drawableSlider.PathVersion);
}

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour =>
{
explode.Colour = colour.NewValue;
@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
}
private void updateState(ValueChangedEvent<ArmedState> state)
private void updateState(DrawableHitObject drawableObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{
glow.FadeOut(400);
switch (state.NewValue)
switch (state)
{
case ArmedState.Hit:
const double flash_in = 40;

View File

@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint);
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
}
}
}

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
private SkinnableSpriteText hitCircleText;
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (overlayAboveNumber)
AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
}
private void updateState(ValueChangedEvent<ArmedState> state)
private void updateState(DrawableHitObject drawableObject, ArmedState state)
{
const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{
switch (state.NewValue)
switch (state)
{
case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);

View File

@ -3,9 +3,9 @@
using System;
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;
@ -20,15 +20,12 @@ 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
{
public class OsuPlayfield : Playfield
{
public readonly Func<DrawableHitObject, double, bool> CheckHittable;
private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
@ -40,86 +37,81 @@ 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);
CheckHittable = hitPolicy.IsHittable;
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;
}
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
}
private void onDrawableHitObjectLoaded(Drawable drawable)
{
// note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded.
switch (drawable)
{
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case DrawableHitCircle hitCircle:
approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy());
break;
}
}
private void onJudgmentLoaded(DrawableOsuJudgement judgement)
{
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
registerPool<HitCircle, DrawableHitCircle>(10, 100);
RegisterPool<HitCircle, DrawableHitCircle>(10, 100);
registerPool<Slider, DrawableSlider>(10, 100);
registerPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
registerPool<SliderTailCircle, DrawableSliderTail>(10, 100);
registerPool<SliderTick, DrawableSliderTick>(10, 100);
registerPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
RegisterPool<Slider, DrawableSlider>(10, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100);
RegisterPool<SliderTick, DrawableSliderTick>(10, 100);
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
registerPool<Spinner, DrawableSpinner>(2, 20);
registerPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
registerPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
RegisterPool<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
}
private void registerPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(CreatePool<TDrawable>(initialSize, maximumSize));
protected virtual DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
where TDrawable : DrawableHitObject, new()
=> new DrawableOsuPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
protected override void OnHitObjectAdded(HitObject hitObject)
@ -134,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI
followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
}
public void OnHitObjectLoaded(Drawable drawable)
{
switch (drawable)
{
case DrawableSliderHead _:
case DrawableSliderTail _:
case DrawableSliderTick _:
case DrawableSliderRepeat _:
case DrawableSpinnerTick _:
break;
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case IDrawableHitObjectWithProxiedApproach approach:
approachCircles.Add(approach.ProxiedLayer.CreateProxy());
break;
}
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
@ -178,11 +149,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 +165,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;
}
}