Merge branch 'master' into fix-spectator-frame-conversion

This commit is contained in:
Dan Balasescu 2021-06-04 21:17:40 +09:00 committed by GitHub
commit 57a38374db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 435 additions and 225 deletions

View File

@ -0,0 +1,30 @@
// 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.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneHitCircleKiai : TestSceneHitCircle
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
@ -12,34 +13,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s. /// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointConnection : PoolableDrawable public class FollowPointConnection : PoolableDrawableWithLifetime<FollowPointLifetimeEntry>
{ {
// Todo: These shouldn't be constants // Todo: These shouldn't be constants
public const int SPACING = 32; public const int SPACING = 32;
public const double PREEMPT = 800; public const double PREEMPT = 800;
public FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool; public DrawablePool<FollowPoint> Pool;
protected override void PrepareForUse() protected override void OnApply(FollowPointLifetimeEntry entry)
{ {
base.PrepareForUse(); base.OnApply(entry);
Entry.Invalidated += onEntryInvalidated;
entry.Invalidated += onEntryInvalidated;
refreshPoints(); refreshPoints();
} }
protected override void FreeAfterUse() protected override void OnFree(FollowPointLifetimeEntry entry)
{ {
base.FreeAfterUse(); base.OnFree(entry);
Entry.Invalidated -= onEntryInvalidated;
entry.Invalidated -= onEntryInvalidated;
// Return points to the pool. // Return points to the pool.
ClearInternal(false); ClearInternal(false);
Entry = null;
} }
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints); private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
@ -48,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
ClearInternal(false); ClearInternal(false);
OsuHitObject start = Entry.Start; var entry = Entry;
OsuHitObject end = Entry.End; if (entry?.End == null) return;
OsuHitObject start = entry.Start;
OsuHitObject end = entry.End;
double startTime = start.GetEndTime(); double startTime = start.GetEndTime();
@ -87,14 +86,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
fp.FadeIn(end.TimeFadeIn); fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn).Expire();
finalTransformEndTime = fadeOutTime + end.TimeFadeIn; finalTransformEndTime = fp.LifetimeEnd;
} }
} }
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. entry.LifetimeEnd = finalTransformEndTime;
Entry.LifetimeEnd = finalTransformEndTime;
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
public class FollowPointLifetimeEntry : LifetimeEntry public class FollowPointLifetimeEntry : LifetimeEntry
{ {
public event Action Invalidated; public event Action? Invalidated;
public readonly OsuHitObject Start; public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start) public FollowPointLifetimeEntry(OsuHitObject start)
@ -22,9 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
bindEvents(); bindEvents();
} }
private OsuHitObject end; private OsuHitObject? end;
public OsuHitObject End public OsuHitObject? End
{ {
get => end; get => end;
set set
@ -56,11 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public void UnbindEvents() public void UnbindEvents()
{ {
if (Start != null) Start.DefaultsApplied -= onDefaultsApplied;
{ Start.PositionBindable.ValueChanged -= onPositionChanged;
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null) if (End != null)
{ {

View File

@ -6,43 +6,32 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s. /// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointRenderer : CompositeDrawable public class FollowPointRenderer : PooledDrawableWithLifetimeContainer<FollowPointLifetimeEntry, FollowPointConnection>
{ {
public override bool RemoveCompletedTransforms => false; public new IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
private DrawablePool<FollowPointConnection> connectionPool; private DrawablePool<FollowPointConnection> connectionPool;
private DrawablePool<FollowPoint> pointPool; private DrawablePool<FollowPoint> pointPool;
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>(); 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 Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
public FollowPointRenderer()
{
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePoolNoLifetime<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }
@ -107,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
previousEntry.End = newEntry.Start; previousEntry.End = newEntry.Start;
} }
lifetimeManager.AddEntry(newEntry); Add(newEntry);
} }
private void removeEntry(OsuHitObject hitObject) private void removeEntry(OsuHitObject hitObject)
@ -118,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.RemoveAt(index); lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry); Remove(entry);
if (index > 0) if (index > 0)
{ {
@ -131,30 +120,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
} }
} }
protected override bool CheckChildrenLife() protected override FollowPointConnection GetDrawable(FollowPointLifetimeEntry entry)
{ {
bool anyAliveChanged = base.CheckChildrenLife(); var connection = connectionPool.Get();
anyAliveChanged |= lifetimeManager.Update(Time.Current); connection.Pool = pointPool;
return anyAliveChanged; connection.Apply(entry);
} return connection;
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) private void onStartTimeChanged(OsuHitObject hitObject)
@ -171,16 +142,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
entry.UnbindEvents(); entry.UnbindEvents();
lifetimeEntries.Clear(); 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

@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"), Texture = textures.Get(@"Gameplay/osu/disc"),
}, },
new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
},
triangles = new TrianglesPiece triangles = new TrianglesPiece
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -0,0 +1,43 @@
// 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.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class KiaiFlash : BeatSyncedContainer
{
private const double fade_length = 80;
private const float flash_opacity = 0.25f;
public KiaiFlash()
{
EarlyActivationMilliseconds = 80;
Blending = BlendingParameters.Additive;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
Alpha = 0f,
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then()
.FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine);
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingSprite : BeatSyncedContainer
{
private readonly Sprite mainSprite;
private readonly Sprite flashingSprite;
public Texture Texture
{
set
{
mainSprite.Texture = value;
flashingSprite.Texture = value;
}
}
private const float flash_opacity = 0.3f;
public KiaiFlashingSprite()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
mainSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
flashingSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingSprite
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -32,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
private Container<Sprite> circleSprites; private Container circleSprites;
private Sprite hitCircleSprite; private Drawable hitCircleSprite;
private Sprite hitCircleOverlay; private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText; private SkinnableSpriteText hitCircleText;
@ -72,20 +71,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
circleSprites = new Container<Sprite> circleSprites = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new[]
{ {
hitCircleSprite = new Sprite hitCircleSprite = new KiaiFlashingSprite
{ {
Texture = baseTexture, Texture = baseTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
hitCircleOverlay = new Sprite hitCircleOverlay = new KiaiFlashingSprite
{ {
Texture = overlayTexture, Texture = overlayTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -11,6 +8,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
internal class DrumRollHitContainer : ScrollingHitObjectContainer internal class DrumRollHitContainer : ScrollingHitObjectContainer
{ {
// TODO: this usage is buggy.
// Because `LifetimeStart` is set based on scrolling, lifetime is not same as the time when the object is created.
// If the `Update` override is removed, it breaks in an obscure way.
protected override bool RemoveRewoundEntry => true;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -23,14 +25,5 @@ namespace osu.Game.Rulesets.Taiko.UI
Remove(flyingHit); Remove(flyingHit);
} }
} }
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{
base.OnChildLifetimeBoundaryCrossed(e);
// ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above).
if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)
Remove((DrawableHitObject)e.Child);
}
} }
} }

View File

@ -1,11 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum BackgroundSource public enum BackgroundSource
{ {
Skin, Skin,
Beatmap Beatmap,
[Description("Beatmap (with storyboard / video)")]
BeatmapWithStoryboard,
} }
} }

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Graphics.Backgrounds
{
public class BeatmapBackgroundWithStoryboard : BeatmapBackground
{
public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1")
: base(beatmap, fallbackTextureName)
{
}
[BackgroundDependencyLoader]
private void load()
{
if (!Beatmap.Storyboard.HasDrawable)
return;
if (Beatmap.Storyboard.ReplacesBackground)
Sprite.Alpha = 0;
LoadComponentAsync(new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = new InterpolatingFramedClock(Beatmap.Track) }
}, AddInternal);
}
}
}

View File

@ -172,6 +172,8 @@ namespace osu.Game.Graphics.Containers
private class ScalingBackgroundScreen : BackgroundScreenDefault private class ScalingBackgroundScreen : BackgroundScreenDefault
{ {
protected override bool AllowStoryboardBackground => false;
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
this.FadeInFromZero(4000, Easing.OutQuint); this.FadeInFromZero(4000, Easing.OutQuint);

View File

@ -58,7 +58,7 @@ namespace osu.Game
/// <summary> /// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
/// </summary> /// </summary>
internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5; internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.8;
public bool UseDevelopmentServer { get; } public bool UseDevelopmentServer { get; }

View File

@ -0,0 +1,163 @@
// 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.
#nullable enable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
namespace osu.Game.Rulesets.Objects.Pooling
{
/// <summary>
/// A container of <typeparamref name="TDrawable"/>s dynamically added/removed by model <typeparamref name="TEntry"/>s.
/// When an entry became alive, a drawable corresponding to the entry is obtained (potentially pooled), and added to this container.
/// The drawable is removed when the entry became dead.
/// </summary>
/// <typeparam name="TEntry">The type of entries managed by this container.</typeparam>
/// <typeparam name="TDrawable">The type of drawables corresponding to the entries.</typeparam>
public abstract class PooledDrawableWithLifetimeContainer<TEntry, TDrawable> : CompositeDrawable
where TEntry : LifetimeEntry
where TDrawable : Drawable
{
/// <summary>
/// All entries added to this container, including dead entries.
/// </summary>
/// <remarks>
/// The enumeration order is undefined.
/// </remarks>
public IEnumerable<TEntry> Entries => allEntries;
/// <summary>
/// All alive entries and drawables corresponding to the entries.
/// </summary>
/// <remarks>
/// The enumeration order is undefined.
/// </remarks>
public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
/// <summary>
/// Whether to remove an entry when clock goes backward and crossed its <see cref="LifetimeEntry.LifetimeStart"/>.
/// Used when entries are dynamically added at its <see cref="LifetimeEntry.LifetimeStart"/> to prevent duplicated entries.
/// </summary>
protected virtual bool RemoveRewoundEntry => false;
/// <summary>
/// The amount of time prior to the current time within which entries should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which entries should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<TEntry, TDrawable> aliveDrawableMap = new Dictionary<TEntry, TDrawable>();
private readonly HashSet<TEntry> allEntries = new HashSet<TEntry>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
protected PooledDrawableWithLifetimeContainer()
{
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
}
/// <summary>
/// Add a <typeparamref name="TEntry"/> to be managed by this container.
/// </summary>
/// <remarks>
/// The aliveness of the entry is not updated until <see cref="CheckChildrenLife"/>.
/// </remarks>
public virtual void Add(TEntry entry)
{
allEntries.Add(entry);
lifetimeManager.AddEntry(entry);
}
/// <summary>
/// Remove a <typeparamref name="TEntry"/> from this container.
/// </summary>
/// <remarks>
/// If the entry was alive, the corresponding drawable is removed.
/// </remarks>
/// <returns>Whether the entry was in this container.</returns>
public virtual bool Remove(TEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
allEntries.Remove(entry);
return true;
}
/// <summary>
/// Initialize new <typeparamref name="TDrawable"/> corresponding <paramref name="entry"/>.
/// </summary>
/// <returns>The <typeparamref name="TDrawable"/> corresponding to the entry.</returns>
protected abstract TDrawable GetDrawable(TEntry entry);
private void entryBecameAlive(LifetimeEntry lifetimeEntry)
{
var entry = (TEntry)lifetimeEntry;
Debug.Assert(!aliveDrawableMap.ContainsKey(entry));
TDrawable drawable = GetDrawable(entry);
aliveDrawableMap[entry] = drawable;
AddDrawable(entry, drawable);
}
/// <summary>
/// Add a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> to this container.
/// </summary>
/// <remarks>
/// Invoked when the entry became alive and a <typeparamref name="TDrawable"/> is obtained by <see cref="GetDrawable"/>.
/// </remarks>
protected virtual void AddDrawable(TEntry entry, TDrawable drawable) => AddInternal(drawable);
private void entryBecameDead(LifetimeEntry lifetimeEntry)
{
var entry = (TEntry)lifetimeEntry;
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
TDrawable drawable = aliveDrawableMap[entry];
aliveDrawableMap.Remove(entry);
RemoveDrawable(entry, drawable);
}
/// <summary>
/// Remove a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> from this container.
/// </summary>
/// <remarks>
/// Invoked when the entry became dead.
/// </remarks>
protected virtual void RemoveDrawable(TEntry entry, TDrawable drawable) => RemoveInternal(drawable);
private void entryCrossedBoundary(LifetimeEntry lifetimeEntry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
{
if (RemoveRewoundEntry && kind == LifetimeBoundaryKind.Start && direction == LifetimeBoundaryCrossingDirection.Backward)
Remove((TEntry)lifetimeEntry);
}
/// <summary>
/// Remove all <typeparamref name="TEntry"/>s.
/// </summary>
public void Clear()
{
foreach (var entry in Entries.ToArray())
Remove(entry);
Debug.Assert(aliveDrawableMap.Count == 0);
}
protected override bool CheckChildrenLife()
{
bool aliveChanged = base.CheckChildrenLife();
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
return aliveChanged;
}
}
}

View File

@ -3,35 +3,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Pooling;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public class HitObjectContainer : CompositeDrawable, IHitObjectContainer public class HitObjectContainer : PooledDrawableWithLifetimeContainer<HitObjectLifetimeEntry, DrawableHitObject>, IHitObjectContainer
{ {
/// <summary>
/// All entries in this <see cref="HitObjectContainer"/> including dead entries.
/// </summary>
public IEnumerable<HitObjectLifetimeEntry> Entries => allEntries;
/// <summary>
/// All alive entries and <see cref="DrawableHitObject"/>s used by the entries.
/// </summary>
public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime);
/// <summary> /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged. /// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -59,34 +47,16 @@ namespace osu.Game.Rulesets.UI
/// </remarks> /// </remarks>
internal event Action<HitObject> HitObjectUsageFinished; internal event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double PastLifetimeExtension { get; set; }
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>(); private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> aliveDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>(); private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly HashSet<HitObjectLifetimeEntry> allEntries = new HashSet<HitObjectLifetimeEntry>();
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; } private IPooledHitObjectProvider pooledObjectProvider { get; set; }
public HitObjectContainer() public HitObjectContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
} }
protected override void LoadAsyncComplete() protected override void LoadAsyncComplete()
@ -99,63 +69,41 @@ namespace osu.Game.Rulesets.UI
#region Pooling support #region Pooling support
public void Add(HitObjectLifetimeEntry entry) public override bool Remove(HitObjectLifetimeEntry entry)
{ {
allEntries.Add(entry); if (!base.Remove(entry)) return false;
lifetimeManager.AddEntry(entry);
}
public bool Remove(HitObjectLifetimeEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry. // This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry.
if (nonPooledDrawableMap.Remove(entry, out var drawable)) if (nonPooledDrawableMap.Remove(entry, out var drawable))
removeDrawable(drawable); removeDrawable(drawable);
allEntries.Remove(entry);
return true; return true;
} }
private void entryBecameAlive(LifetimeEntry lifetimeEntry) protected sealed override DrawableHitObject GetDrawable(HitObjectLifetimeEntry entry)
{ {
var entry = (HitObjectLifetimeEntry)lifetimeEntry; if (nonPooledDrawableMap.TryGetValue(entry, out var drawable))
Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); return drawable;
bool isPooled = !nonPooledDrawableMap.TryGetValue(entry, out var drawable); return pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null) ??
drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
if (drawable == null)
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
aliveDrawableMap[entry] = drawable;
if (isPooled)
{
addDrawable(drawable);
HitObjectUsageBegan?.Invoke(entry.HitObject);
}
OnAdd(drawable);
} }
private void entryBecameDead(LifetimeEntry lifetimeEntry) protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{ {
var entry = (HitObjectLifetimeEntry)lifetimeEntry; if (nonPooledDrawableMap.ContainsKey(entry)) return;
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
var drawable = aliveDrawableMap[entry]; addDrawable(drawable);
bool isPooled = !nonPooledDrawableMap.ContainsKey(entry); HitObjectUsageBegan?.Invoke(entry.HitObject);
}
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
drawable.OnKilled(); drawable.OnKilled();
aliveDrawableMap.Remove(entry); if (nonPooledDrawableMap.ContainsKey(entry)) return;
if (isPooled) removeDrawable(drawable);
{ HitObjectUsageFinished?.Invoke(entry.HitObject);
removeDrawable(drawable);
HitObjectUsageFinished?.Invoke(entry.HitObject);
}
OnRemove(drawable);
} }
private void addDrawable(DrawableHitObject drawable) private void addDrawable(DrawableHitObject drawable)
@ -201,49 +149,8 @@ namespace osu.Game.Rulesets.UI
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
{
if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable))
OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction));
}
protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{
}
#endregion #endregion
/// <summary>
/// Invoked after a <see cref="DrawableHitObject"/> is added to this container.
/// </summary>
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
{
Debug.Assert(drawableHitObject.LoadState >= LoadState.Ready);
}
/// <summary>
/// Invoked after a <see cref="DrawableHitObject"/> is removed from this container.
/// </summary>
protected virtual void OnRemove(DrawableHitObject drawableHitObject)
{
}
public virtual void Clear()
{
lifetimeManager.ClearEntries();
foreach (var drawable in nonPooledDrawableMap.Values)
removeDrawable(drawable);
nonPooledDrawableMap.Clear();
Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed");
}
protected override bool CheckChildrenLife()
{
bool aliveChanged = base.CheckChildrenLife();
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
return aliveChanged;
}
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -45,13 +47,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
timeRange.ValueChanged += _ => layoutCache.Invalidate(); timeRange.ValueChanged += _ => layoutCache.Invalidate();
} }
public override void Clear()
{
base.Clear();
layoutComputed.Clear();
}
/// <summary> /// <summary>
/// Given a position in screen space, return the time within this column. /// Given a position in screen space, return the time within this column.
/// </summary> /// </summary>
@ -147,17 +142,20 @@ namespace osu.Game.Rulesets.UI.Scrolling
} }
} }
protected override void OnAdd(DrawableHitObject drawableHitObject) protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{ {
invalidateHitObject(drawableHitObject); base.AddDrawable(entry, drawable);
drawableHitObject.DefaultsApplied += invalidateHitObject;
invalidateHitObject(drawable);
drawable.DefaultsApplied += invalidateHitObject;
} }
protected override void OnRemove(DrawableHitObject drawableHitObject) protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{ {
layoutComputed.Remove(drawableHitObject); base.RemoveDrawable(entry, drawable);
drawableHitObject.DefaultsApplied -= invalidateHitObject; drawable.DefaultsApplied -= invalidateHitObject;
layoutComputed.Remove(drawable);
} }
private void invalidateHitObject(DrawableHitObject hitObject) private void invalidateHitObject(DrawableHitObject hitObject)
@ -206,6 +204,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
{ {
// Origin position may be relative to the parent size
Debug.Assert(hitObject.Parent != null);
float originAdjustment = 0.0f; float originAdjustment = 0.0f;
// calculate the dimension of the part of the hitobject that should already be visible // calculate the dimension of the part of the hitobject that should already be visible

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.Backgrounds
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } private IBindable<WorkingBeatmap> beatmap { get; set; }
protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true) public BackgroundScreenDefault(bool animateOnEnter = true)
: base(animateOnEnter) : base(animateOnEnter)
{ {
@ -110,6 +112,12 @@ namespace osu.Game.Screens.Backgrounds
newBackground = new BeatmapBackground(beatmap.Value, backgroundName); newBackground = new BeatmapBackground(beatmap.Value, backgroundName);
break; break;
case BackgroundSource.BeatmapWithStoryboard:
newBackground = AllowStoryboardBackground
? new BeatmapBackgroundWithStoryboard(beatmap.Value, backgroundName)
: new BeatmapBackground(beatmap.Value, backgroundName);
break;
default: default:
newBackground = new SkinnedBackground(skin.Value, backgroundName); newBackground = new SkinnedBackground(skin.Value, backgroundName);
break; break;