471 lines
18 KiB
C#
471 lines
18 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using GamePlay; // for PoolItem
|
||
using System.Collections;
|
||
|
||
public class NotePool : MonoBehaviour
|
||
{
|
||
public static NotePool Instance { get; private set; }
|
||
|
||
public GameObject[] notePrefabs; // Documentation text normalized.
|
||
public GameObject[] holdNotePrefabs; // Documentation text normalized.
|
||
public GameObject[] holdNoteEndPrefabs; // Documentation text normalized.
|
||
public GameObject[] startNotePrefabs; // Documentation text normalized.
|
||
|
||
[Header("Pool Size")]
|
||
public int poolSize = 24;
|
||
// Worst-case hold-segment count for one long note ≈ length(s) × sm(≤8) × bpm / 15.
|
||
// A 4s hold at 120bpm with max flow speed needs ~256 middle segments of one color at once.
|
||
// maxPoolSize is the retain cap on return (below it, returned segments are kept, not
|
||
// Destroyed), so it must exceed that worst case to avoid churn when a dense long note recycles.
|
||
public int maxPoolSize = 320;
|
||
// poolSize × this = per-color hold-segment prewarm target. Sized so prewarm covers the
|
||
// worst case (24 × 12 = 288 ≥ 256), preventing a mid-song runtime Instantiate spike the
|
||
// first time a dense long note needs more segments than were prewarmed.
|
||
[Tooltip("Multiplier for hold segment pool size (per color).")]
|
||
public int holdSegmentPoolMultiplier = 12;
|
||
[Tooltip("Multiplier for hold end pool size (per color).")]
|
||
public int holdEndPoolMultiplier = 8;
|
||
|
||
// Documentation text normalized.
|
||
private Dictionary<string, int> colorIndexMap;
|
||
|
||
private Dictionary<int, Stack<GameObject>> notePools; // Documentation text normalized.
|
||
private Dictionary<int, Stack<GameObject>> holdNotePools;
|
||
private Dictionary<int, Stack<GameObject>> holdNoteEndPools;
|
||
private Dictionary<int, Stack<GameObject>> startNotePools;
|
||
// Documentation text normalized.
|
||
private Transform notePoolContainer;
|
||
private Transform holdPoolContainer;
|
||
private Transform startPoolContainer;
|
||
|
||
[Header("Debug")]
|
||
public bool verboseLogging = false;
|
||
|
||
[Header("Prewarm Settings")]
|
||
[Tooltip("If true, the pool will be prewarmed across multiple frames to avoid a large GC/instantiation spike at scene load.")]
|
||
public bool prewarmOnStart = true;
|
||
[Tooltip("Number of instantiated pool items to create per frame during prewarming.")]
|
||
// Raised default so heavy pools finish prewarming faster and reduce hitch at first spawn.
|
||
public int prewarmPerFrame = 200;
|
||
|
||
// internal prewarm coroutine handle
|
||
private Coroutine prewarmCoroutine = null;
|
||
|
||
// Expose whether prewarm has completed
|
||
public bool IsPrewarmed => prewarmCoroutine == null;
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance == null)
|
||
{
|
||
Instance = this;
|
||
}
|
||
else
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
colorIndexMap = new Dictionary<string, int>
|
||
{
|
||
{ "red", 0 },
|
||
{ "green", 1 },
|
||
{ "yellow", 2 },
|
||
{ "purple", 3 },
|
||
{ "blue", 4 }
|
||
};
|
||
|
||
// Documentation text normalized.
|
||
notePoolContainer = new GameObject("_NotePool_Notes").transform;
|
||
notePoolContainer.SetParent(transform, false);
|
||
holdPoolContainer = new GameObject("_NotePool_HoldSegments").transform;
|
||
holdPoolContainer.SetParent(transform, false);
|
||
startPoolContainer = new GameObject("_NotePool_StartSegments").transform;
|
||
startPoolContainer.SetParent(transform, false);
|
||
|
||
notePools = new Dictionary<int, Stack<GameObject>>();
|
||
holdNotePools = new Dictionary<int, Stack<GameObject>>();
|
||
holdNoteEndPools = new Dictionary<int, Stack<GameObject>>();
|
||
startNotePools = new Dictionary<int, Stack<GameObject>>();
|
||
|
||
int colorCount = Mathf.Max(1, notePrefabs != null ? notePrefabs.Length : 0);
|
||
|
||
for (int i = 0; i < colorCount; i++)
|
||
{
|
||
notePools[i] = new Stack<GameObject>();
|
||
holdNotePools[i] = new Stack<GameObject>();
|
||
holdNoteEndPools[i] = new Stack<GameObject>();
|
||
startNotePools[i] = new Stack<GameObject>();
|
||
}
|
||
|
||
// minimal immediate entries to ensure safety (one each)
|
||
for (int i = 0; i < colorCount; i++)
|
||
{
|
||
if (notePrefabs != null && i < notePrefabs.Length && notePrefabs[i] != null)
|
||
AddToPool(notePools[i], notePrefabs[i], notePoolContainer);
|
||
if (startNotePrefabs != null && i < startNotePrefabs.Length && startNotePrefabs[i] != null)
|
||
AddToPool(startNotePools[i], startNotePrefabs[i], startPoolContainer);
|
||
if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||
AddToPool(holdNotePools[i], holdNotePrefabs[i], holdPoolContainer);
|
||
if (holdNoteEndPrefabs != null && i < holdNoteEndPrefabs.Length && holdNoteEndPrefabs[i] != null)
|
||
AddToPool(holdNoteEndPools[i], holdNoteEndPrefabs[i], holdPoolContainer);
|
||
}
|
||
|
||
if (prewarmOnStart)
|
||
{
|
||
prewarmCoroutine = StartCoroutine(PrewarmCoroutine(colorCount));
|
||
}
|
||
|
||
// Also ensure particle and judge prefab warm-up if AnimationController exists in scene.
|
||
var anim = AnimationController.Global ?? SceneObjectLookupCache.FindAny<AnimationController>();
|
||
if (anim != null)
|
||
{
|
||
anim.PrewarmParticles(6);
|
||
if (verboseLogging) Debug.Log("NotePool: requested AnimationController prewarm");
|
||
}
|
||
}
|
||
|
||
private IEnumerator PrewarmCoroutine(int colorCount)
|
||
{
|
||
// Calculate targets
|
||
int noteTarget = Mathf.Min(poolSize, maxPoolSize); // per color
|
||
int startTarget = Mathf.Min(poolSize, maxPoolSize);
|
||
int holdTarget = Mathf.Min(poolSize * Mathf.Max(1, holdSegmentPoolMultiplier), maxPoolSize);
|
||
int endTarget = Mathf.Min(poolSize * Mathf.Max(1, holdEndPoolMultiplier), maxPoolSize);
|
||
|
||
while (true)
|
||
{
|
||
int createdThisFrame = 0;
|
||
bool allReached = true;
|
||
|
||
for (int i = 0; i < colorCount; i++)
|
||
{
|
||
// note pool
|
||
while (notePools[i].Count < noteTarget && createdThisFrame < prewarmPerFrame)
|
||
{
|
||
if (notePrefabs != null && i < notePrefabs.Length && notePrefabs[i] != null)
|
||
AddToPool(notePools[i], notePrefabs[i], notePoolContainer);
|
||
createdThisFrame++;
|
||
}
|
||
if (notePools[i].Count < noteTarget) allReached = false;
|
||
|
||
// start pool
|
||
while (startNotePools[i].Count < startTarget && createdThisFrame < prewarmPerFrame)
|
||
{
|
||
if (startNotePrefabs != null && i < startNotePrefabs.Length && startNotePrefabs[i] != null)
|
||
AddToPool(startNotePools[i], startNotePrefabs[i], startPoolContainer);
|
||
createdThisFrame++;
|
||
}
|
||
if (startNotePools[i].Count < startTarget) allReached = false;
|
||
|
||
// hold middle pool
|
||
while (holdNotePools[i].Count < holdTarget && createdThisFrame < prewarmPerFrame)
|
||
{
|
||
if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||
AddToPool(holdNotePools[i], holdNotePrefabs[i], holdPoolContainer);
|
||
createdThisFrame++;
|
||
}
|
||
if (holdNotePools[i].Count < holdTarget) allReached = false;
|
||
|
||
// hold end pool
|
||
while (holdNoteEndPools[i].Count < endTarget && createdThisFrame < prewarmPerFrame)
|
||
{
|
||
if (holdNoteEndPrefabs != null && i < holdNoteEndPrefabs.Length && holdNoteEndPrefabs[i] != null)
|
||
AddToPool(holdNoteEndPools[i], holdNoteEndPrefabs[i], holdPoolContainer);
|
||
else if (holdNotePrefabs != null && i < holdNotePrefabs.Length && holdNotePrefabs[i] != null)
|
||
AddToPool(holdNoteEndPools[i], holdNotePrefabs[i], holdPoolContainer);
|
||
createdThisFrame++;
|
||
}
|
||
if (holdNoteEndPools[i].Count < endTarget) allReached = false;
|
||
|
||
if (createdThisFrame >= prewarmPerFrame)
|
||
break;
|
||
}
|
||
|
||
if (allReached) break;
|
||
yield return null;
|
||
}
|
||
|
||
prewarmCoroutine = null;
|
||
}
|
||
|
||
private GameObject GetObjectFromPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||
{
|
||
if (pool == null)
|
||
return InstantiateAndPrepare(prefab);
|
||
|
||
if (pool.Count > 0)
|
||
{
|
||
GameObject obj = pool.Pop();
|
||
GamePlay.PoolItem pi = null;
|
||
if (obj == null)
|
||
{
|
||
// Documentation text normalized.
|
||
}
|
||
else
|
||
{
|
||
// Documentation text normalized.
|
||
pi = obj.GetComponent<GamePlay.PoolItem>();
|
||
if (pi == null || prefab == null || pi.prefabName != prefab.name)
|
||
{
|
||
// Documentation text normalized.
|
||
if (pi == null && verboseLogging) Debug.LogWarning("PoolItem missing on pooled object, replacing.");
|
||
Destroy(obj);
|
||
obj = InstantiateAndPrepare(prefab);
|
||
pi = obj != null ? obj.GetComponent<GamePlay.PoolItem>() : null;
|
||
}
|
||
}
|
||
obj.transform.SetParent(null);
|
||
obj.SetActive(true);
|
||
|
||
// mark as taken from pool (reuse the PoolItem already resolved above)
|
||
if (pi != null)
|
||
{
|
||
pi.inPool = false;
|
||
if (pi.initialLocalScaleCaptured)
|
||
{
|
||
obj.transform.localScale = pi.initialLocalScale;
|
||
}
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
else
|
||
{
|
||
// Documentation text normalized.
|
||
GameObject obj = InstantiateAndPrepare(prefab);
|
||
obj.SetActive(true);
|
||
return obj;
|
||
}
|
||
}
|
||
|
||
private GameObject InstantiateAndPrepare(GameObject prefab)
|
||
{
|
||
if (prefab == null) return null;
|
||
GameObject obj = Instantiate(prefab);
|
||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||
if (pi == null) pi = obj.AddComponent<GamePlay.PoolItem>();
|
||
pi.prefabName = prefab.name;
|
||
pi.inPool = false;
|
||
if (!pi.initialLocalScaleCaptured)
|
||
{
|
||
pi.initialLocalScale = obj.transform.localScale;
|
||
pi.initialLocalScaleCaptured = true;
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
private void ReturnObjectToPool(Stack<GameObject> pool, GameObject obj, Transform container)
|
||
{
|
||
if (obj == null || pool == null) return; // Documentation text normalized.
|
||
|
||
// Defensive: unregister/unlock any TrackKeyManager state referencing this object to avoid stale queues/locks
|
||
try
|
||
{
|
||
// Try to determine track index from Note or HoldNote components. Reuse each
|
||
// note's already-cached instance-id string (Note.GetCachedInstanceId /
|
||
// HoldNote.GetQueueNoteId) instead of allocating a fresh GetInstanceID().ToString()
|
||
// on every return — a 256-segment hold recycle would otherwise burst 256 string
|
||
// allocations. The cached ids are byte-identical to what was used at RegisterKey.
|
||
int trackIdx = -1;
|
||
string id = null;
|
||
var noteComp = obj.GetComponent<Note>();
|
||
if (noteComp != null)
|
||
{
|
||
trackIdx = noteComp.GetTrackIndex();
|
||
id = noteComp.GetCachedInstanceId();
|
||
}
|
||
else
|
||
{
|
||
var holdComp = obj.GetComponent<HoldNote>();
|
||
if (holdComp != null)
|
||
{
|
||
trackIdx = holdComp.trackIndex;
|
||
id = holdComp.GetQueueNoteId();
|
||
}
|
||
}
|
||
|
||
// If a note component supplied a cached id, use it. When it's null (e.g. a hold
|
||
// middle/end segment that never registered a queue key), there is nothing to
|
||
// unregister — skip without allocating a throwaway ToString. Only the fully
|
||
// unknown case (no note component at all) falls back to a fresh id.
|
||
if (string.IsNullOrEmpty(id) && noteComp == null && trackIdx < 0)
|
||
{
|
||
id = obj.GetInstanceID().ToString();
|
||
}
|
||
|
||
if (TrackKeyManager.Instance != null && trackIdx >= 0 && !string.IsNullOrEmpty(id))
|
||
{
|
||
try { TrackKeyManager.Instance.UnregisterKey(trackIdx, id); } catch { }
|
||
try { TrackKeyManager.Instance.UnlockTrackForJudge(trackIdx, id); } catch { }
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
// Documentation text normalized.
|
||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
|
||
if (pi == null)
|
||
{
|
||
pi = obj.AddComponent<GamePlay.PoolItem>();
|
||
}
|
||
if (!pi.initialLocalScaleCaptured)
|
||
{
|
||
pi.initialLocalScale = obj.transform.localScale;
|
||
pi.initialLocalScaleCaptured = true;
|
||
}
|
||
if (pi != null && pi.inPool) return;
|
||
|
||
// Documentation text normalized.
|
||
obj.transform.SetParent(container, false);
|
||
obj.transform.localPosition = Vector3.zero;
|
||
obj.transform.localRotation = Quaternion.identity;
|
||
obj.transform.localScale = pi.initialLocalScaleCaptured ? pi.initialLocalScale : obj.transform.localScale;
|
||
|
||
// Documentation text normalized.
|
||
obj.SetActive(false);
|
||
|
||
// Documentation text normalized.
|
||
|
||
if (pool.Count < maxPoolSize)
|
||
{
|
||
pool.Push(obj);
|
||
}
|
||
else
|
||
{
|
||
// Documentation text normalized.
|
||
Destroy(obj);
|
||
}
|
||
}
|
||
|
||
private void AddToPool(Stack<GameObject> pool, GameObject prefab, Transform container)
|
||
{
|
||
if (prefab == null) return;
|
||
if (pool.Count < maxPoolSize)
|
||
{
|
||
GameObject obj = Instantiate(prefab, container);
|
||
// ensure PoolItem exists and is marked in pool
|
||
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>() ?? obj.AddComponent<GamePlay.PoolItem>();
|
||
pi.prefabName = prefab.name;
|
||
pi.inPool = true;
|
||
if (!pi.initialLocalScaleCaptured)
|
||
{
|
||
pi.initialLocalScale = obj.transform.localScale;
|
||
pi.initialLocalScaleCaptured = true;
|
||
}
|
||
|
||
obj.SetActive(false);
|
||
pool.Push(obj);
|
||
}
|
||
}
|
||
|
||
public GameObject GetNote(string color)
|
||
{
|
||
int colorIndex = GetColorIndexFromName(color);
|
||
return GetObjectFromPool(notePools[colorIndex], notePrefabs[colorIndex], notePoolContainer);
|
||
}
|
||
|
||
public GameObject GetStartNote(string color)
|
||
{
|
||
int colorIndex = GetColorIndexFromName(color);
|
||
return GetObjectFromPool(startNotePools[colorIndex], startNotePrefabs[colorIndex], startPoolContainer);
|
||
}
|
||
|
||
public GameObject GetHoldNoteSegment(string color)
|
||
{
|
||
int colorIndex = GetColorIndexFromName(color);
|
||
return GetObjectFromPool(holdNotePools[colorIndex], holdNotePrefabs[colorIndex], holdPoolContainer);
|
||
}
|
||
|
||
public GameObject GetHoldNoteEndSegment(string color)
|
||
{
|
||
int colorIndex = GetColorIndexFromName(color);
|
||
GameObject prefab = (holdNoteEndPrefabs != null && holdNoteEndPrefabs.Length > colorIndex && holdNoteEndPrefabs[colorIndex] != null)
|
||
? holdNoteEndPrefabs[colorIndex]
|
||
: holdNotePrefabs[colorIndex];
|
||
return GetObjectFromPool(holdNoteEndPools[colorIndex], prefab, holdPoolContainer);
|
||
}
|
||
|
||
public void ReturnNote(GameObject note, string color)
|
||
{
|
||
if (note == null) return;
|
||
NoteController noteScript = note.GetComponent<NoteController>();
|
||
if (noteScript != null)
|
||
{
|
||
noteScript.ResetState();
|
||
}
|
||
int idx = GetColorIndexFromName(color);
|
||
ReturnObjectToPool(notePools[idx], note, notePoolContainer);
|
||
}
|
||
|
||
public void ReturnStartNote(GameObject startNote, string color)
|
||
{
|
||
if (startNote == null) return;
|
||
if (string.IsNullOrEmpty(color))
|
||
{
|
||
if (verboseLogging) Debug.LogWarning($"归还 StartNote 时 color 为空或无效: {startNote.name}");
|
||
Destroy(startNote);
|
||
return;
|
||
}
|
||
|
||
HoldNote holdNote = startNote.GetComponent<HoldNote>();
|
||
if (holdNote != null)
|
||
{
|
||
holdNote.ResetState(); // Documentation text normalized.
|
||
}
|
||
|
||
int idx = GetColorIndexFromName(color);
|
||
ReturnObjectToPool(startNotePools[idx], startNote, startPoolContainer);
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
public void ReturnHoldNoteEndSegment(GameObject holdNoteEnd, string color)
|
||
{
|
||
if (holdNoteEnd == null) return;
|
||
if (string.IsNullOrEmpty(color))
|
||
{
|
||
if (verboseLogging) Debug.LogWarning($"归还 HoldNoteEndSegment 时 color 为空或无效: {holdNoteEnd.name}");
|
||
Destroy(holdNoteEnd);
|
||
return;
|
||
}
|
||
|
||
HoldNote holdNoteScript = holdNoteEnd.GetComponent<HoldNote>();
|
||
if (holdNoteScript != null)
|
||
{
|
||
holdNoteScript.ResetState(); // Documentation text normalized.
|
||
}
|
||
|
||
int idx = GetColorIndexFromName(color);
|
||
ReturnObjectToPool(holdNoteEndPools[idx], holdNoteEnd, holdPoolContainer);
|
||
}
|
||
|
||
public void ReturnHoldNoteSegment(GameObject holdNote, string color)
|
||
{
|
||
if (holdNote == null) return;
|
||
if (string.IsNullOrEmpty(color))
|
||
{
|
||
if (verboseLogging) Debug.LogWarning($"归还 HoldNoteSegment 时 color 为空或无效: {holdNote.name}");
|
||
Destroy(holdNote);
|
||
return;
|
||
}
|
||
|
||
HoldNote holdNoteScript = holdNote.GetComponent<HoldNote>();
|
||
if (holdNoteScript != null)
|
||
{
|
||
holdNoteScript.ResetState(); // Documentation text normalized.
|
||
}
|
||
|
||
int idx = GetColorIndexFromName(color);
|
||
ReturnObjectToPool(holdNotePools[idx], holdNote, holdPoolContainer);
|
||
}
|
||
|
||
private int GetColorIndexFromName(string colorName)
|
||
{
|
||
if (string.IsNullOrEmpty(colorName)) return 0;
|
||
if (colorIndexMap != null && colorIndexMap.TryGetValue(colorName, out int idx)) return idx;
|
||
if (verboseLogging) Debug.LogError($"未识别颜色名: {colorName},默认使用红色");
|
||
return 0;
|
||
}
|
||
}
|