diff --git a/osu.Game.Benchmarks/BenchmarkHitObject.cs b/osu.Game.Benchmarks/BenchmarkHitObject.cs index 65c78e39b3..4e411ca7c6 100644 --- a/osu.Game.Benchmarks/BenchmarkHitObject.cs +++ b/osu.Game.Benchmarks/BenchmarkHitObject.cs @@ -11,7 +11,7 @@ namespace osu.Game.Benchmarks { public class BenchmarkHitObject : BenchmarkTest { - [Params(1, 100, 1000)] + [Params(1, 1000, 5000)] public int Count { get; set; } [Params(false, true)] diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs index 4cd86f81df..f328a9e8b5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - accentColour = column.AccentColour.GetBoundCopy(); + // Use shared column bindable to prevent per-instance allocation via GetBoundCopy(). + accentColour = column.AccentColour; accentColour.BindValueChanged(colour => { largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyArea.cs index fdb4f98751..03c36b87d5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyArea.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 private Circle hitTargetLine = null!; private CircularContainer? topIcon; + private Box? topIconBox; private Bindable accentColour = null!; [Resolved] @@ -123,7 +124,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - accentColour = column.AccentColour.GetBoundCopy(); + // Use the column's shared bindable to avoid per-instance allocations from GetBoundCopy() + accentColour = column.AccentColour; accentColour.BindValueChanged(colour => { background.Colour = colour.NewValue.Darken(0.2f); @@ -142,13 +144,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 double bpm = beatmap.BeatmapInfo.BPM * gameplayClock.GetTrueGameplayRate(); beatInterval = 60000 / bpm; + // cache reference to inner box to avoid LINQ allocations during Update + topIconBox = topIcon?.Children.OfType().FirstOrDefault(); } protected override void Update() { base.Update(); - if (topIcon == null || !topIcon.Children.Any()) + if (topIconBox == null) return; double progress = (gameplayClock.CurrentTime % beatInterval) / beatInterval; @@ -156,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 if (progress < gameplayClock.ElapsedFrameTime / beatInterval) { double fadeTime = Math.Max(1, beatInterval / 2); - var box = topIcon.Children.OfType().FirstOrDefault(); + var box = topIconBox; box?.FadeTo(1, fadeTime) .Then() diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyAreaPlus.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyAreaPlus.cs index a92c71f9e1..fe4736dccd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyAreaPlus.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2KeyAreaPlus.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 private Container bottomIcon = null!; private CircularContainer topIcon = null!; + private Box? topIconBox; private Bindable accentColour = null!; [Resolved] @@ -162,12 +163,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 // double bpm = beatmap.BeatmapInfo.BPM; double bpm = beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.GetTrueGameplayRate(); - applyBlinkingEffect(topIcon, bpm); + // cache inner box once to avoid repeated LINQ allocation inside scheduled callback + topIconBox = topIcon.Children.OfType().FirstOrDefault(); + applyBlinkingEffect(topIconBox, bpm); direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - accentColour = column.AccentColour.GetBoundCopy(); + // Use the column's shared bindable to avoid per-instance allocations from GetBoundCopy() + accentColour = column.AccentColour; accentColour.BindValueChanged(colour => { background.Colour = colour.NewValue.Darken(0.2f); @@ -178,13 +182,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 column.TopLevelContainer.Add(CreateProxy()); } - private void applyBlinkingEffect(CircularContainer container, double bpm) + private void applyBlinkingEffect(Box? box, double bpm) { + if (box == null) return; + double interval = 60000 / bpm; Scheduler.AddDelayed(() => { - container.Children.OfType().First().FadeTo(1, interval / 2).Then().FadeTo(0, interval / 2); + box.FadeTo(1, interval / 2).Then().FadeTo(0, interval / 2); }, interval, true); } diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2NotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2NotePiece.cs index 2c17a4dda9..b59cf1a416 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2NotePiece.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 private readonly IBindable accentColour = new Bindable(); private readonly Circle colouredBox; + private Drawable iconContainer = null!; public Ez2NotePiece() { @@ -35,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 CornerRadius = CORNER_RADIUS; // Masking = true; + iconContainer = CreateIcon(); + InternalChildren = new[] { new Container @@ -83,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 RelativeSizeAxes = Axes.X, Height = 0, }, - CreateIcon(), + iconContainer, }; } @@ -99,7 +102,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 // NoteHeight = (float)config.Get(ManiaRulesetSetting.ColumnWidth); // specialFactor = (float)config.Get(ManiaRulesetSetting.SpecialFactor); - CreateIcon().Size = new Vector2(DrawWidth / NoteHeight * 0.7f); + if (iconContainer != null) + iconContainer.Size = new Vector2(DrawWidth / NoteHeight * 0.7f); } protected virtual Drawable CreateIcon() => new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzColumnBackground.cs index 016371c5db..42db924bed 100644 --- a/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzColumnBackground.cs @@ -87,7 +87,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro // 计算 drawSeparator 结果(基于不变的列数和列索引) shouldDrawSeparator = drawSeparatorImpl(Column.Index, stageDefinition); - accentColour = new Bindable(ezSkinConfig.GetColumnColor(stageDefinition.Columns, Column.Index)); + // 使用 Column 提供的共享 bindable,避免为每个列背景构建新的 Bindable 实例 + accentColour = Column.AccentColour; accentColour.BindValueChanged(colour => { var baseCol = colour.NewValue; @@ -104,7 +105,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro { base.LoadComplete(); - if (Column.BackgroundContainer.Children.OfType().All(b => b.Name != "Separator")) + bool hasSeparator = false; + + foreach (var child in Column.BackgroundContainer.Children) + { + if (child is Box b && b.Name == "Separator") + { + hasSeparator = true; + break; + } + } + + if (!hasSeparator) Column.BackgroundContainer.Add(separator); if (!Column.BackgroundContainer.Children.Contains(hitOverlay)) diff --git a/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzNoteBase.cs b/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzNoteBase.cs index 7b16897c6e..cc1d8bac6d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzNoteBase.cs +++ b/osu.Game.Rulesets.Mania/Skinning/EzStylePro/EzNoteBase.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -101,9 +104,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro NoteSetName = Column.NoteSetBindable; NoteSize = Column.NoteSizeBindable; - Column.NoteSetChanged += OnNoteSetChanged; - Column.NoteColourChanged += OnColourChanged; - Column.NoteSizeChanged += OnNoteSizeChanged; + // Use a per-column watcher to avoid creating an event handler delegate per-note. + ColumnWatcher.GetOrCreate(Column).Add(this); UpdateSize(); } @@ -153,7 +155,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro sideLine.UpdateGlowEffect(NoteColor); } } - } private void OnColourChanged() @@ -197,13 +198,122 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro { if (isDisposing) { - Column.NoteSetChanged -= OnNoteSetChanged; - Column.NoteColourChanged -= OnColourChanged; - Column.NoteSizeChanged -= OnNoteSizeChanged; + ColumnWatcher.Remove(Column, this); } base.Dispose(isDisposing); } + + // Forwarders used by ColumnWatcher when broadcasting per-column changes to instances. + internal void ForwardOnNoteSetChanged(ValueChangedEvent e) => OnNoteSetChanged(e); + internal void ForwardOnColourChanged() => OnColourChanged(); + internal void ForwardOnNoteSizeChanged() => OnNoteSizeChanged(); + + private class ColumnWatcher + { + private readonly Column column; + private readonly List> notes = new List>(); + + public ColumnWatcher(Column column) + { + this.column = column; + column.NoteSetChanged += onNoteSetChanged; + column.NoteColourChanged += onNoteColourChanged; + column.NoteSizeChanged += onNoteSizeChanged; + } + + public void Add(EzNoteBase note) + { + // store weak reference to avoid preventing note GC if removed elsewhere + notes.Add(new WeakReference(note)); + } + + public void Remove(EzNoteBase note) + { + notes.RemoveAll(wr => !wr.TryGetTarget(out var target) || ReferenceEquals(target, note)); + } + + private void onNoteSetChanged(ValueChangedEvent e) + { + // iterate backwards and remove dead weak references in-place to avoid + // allocating a large temporary array when the column has many notes. + for (int i = notes.Count - 1; i >= 0; i--) + { + var wr = notes[i]; + if (wr.TryGetTarget(out var target)) + target.ForwardOnNoteSetChanged(e); + else + notes.RemoveAt(i); + } + } + + private void onNoteColourChanged() + { + for (int i = notes.Count - 1; i >= 0; i--) + { + var wr = notes[i]; + if (wr.TryGetTarget(out var target)) + target.ForwardOnColourChanged(); + else + notes.RemoveAt(i); + } + } + + private void onNoteSizeChanged() + { + for (int i = notes.Count - 1; i >= 0; i--) + { + var wr = notes[i]; + if (wr.TryGetTarget(out var target)) + target.ForwardOnNoteSizeChanged(); + else + notes.RemoveAt(i); + } + } + + public void Unsubscribe() + { + column.NoteSetChanged -= onNoteSetChanged; + column.NoteColourChanged -= onNoteColourChanged; + column.NoteSizeChanged -= onNoteSizeChanged; + } + + public bool IsEmpty => notes.All(wr => !wr.TryGetTarget(out _)); + + private static readonly Dictionary watchers = new Dictionary(); + private static readonly object watcher_lock = new object(); + + public static ColumnWatcher GetOrCreate(Column c) + { + lock (watcher_lock) + { + if (!watchers.TryGetValue(c, out var w)) + { + w = new ColumnWatcher(c); + watchers[c] = w; + } + + return w; + } + } + + public static void Remove(Column c, EzNoteBase note) + { + lock (watcher_lock) + { + if (!watchers.TryGetValue(c, out var w)) + return; + + w.Remove(note); + + if (w.IsEmpty) + { + w.Unsubscribe(); + watchers.Remove(c); + } + } + } + } } } diff --git a/osu.sln b/osu.sln index 8854a3af9d..7b1f751025 100644 --- a/osu.sln +++ b/osu.sln @@ -128,7 +128,6 @@ Global {F5037F74-E137-4940-8CE7-4D3E5DF0FB39}.Release|Any CPU.ActiveCfg = Release|Any CPU {F5037F74-E137-4940-8CE7-4D3E5DF0FB39}.Release|Any CPU.Build.0 = Release|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.Build.0 = Debug|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.ActiveCfg = Release|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.Build.0 = Release|Any CPU {5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.Deploy.0 = Release|Any CPU @@ -136,9 +135,7 @@ Global {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Build.0 = Release|Any CPU {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Deploy.0 = Release|Any CPU {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection