This commit is contained in:
FloatGaming
2026-07-26 03:44:27 +08:00
parent 86351dbd2a
commit add675e45d
223 changed files with 1960 additions and 10735 deletions
@@ -67,6 +67,13 @@ public class AnimationController : MonoBehaviour
public bool[] ChildInitialActive;
}
private struct PendingFxReturn
{
public GameObject Instance;
public int RentToken;
public float ReturnTime;
}
private readonly Dictionary<GameObject, Queue<GameObject>> fxPools =
new Dictionary<GameObject, Queue<GameObject>>();
private readonly Dictionary<GameObject, FxCacheEntry> fxCache =
@@ -83,6 +90,7 @@ public class AnimationController : MonoBehaviour
// instance that was already returned early (StopHoldParticles) and re-rented.
private int fxRentCounter;
private readonly Dictionary<GameObject, int> fxRentId = new Dictionary<GameObject, int>();
private readonly List<PendingFxReturn> pendingFxReturns = new List<PendingFxReturn>(64);
private Transform fxPoolRoot;
private string hitRingPathUnderParticleSource;
@@ -349,6 +357,41 @@ public class AnimationController : MonoBehaviour
ReturnFxInstance(instance);
}
private void ScheduleFxReturn(GameObject instance, int rentToken, float delay)
{
if (instance == null) return;
pendingFxReturns.Add(new PendingFxReturn
{
Instance = instance,
RentToken = rentToken,
ReturnTime = Time.unscaledTime + Mathf.Max(0f, delay)
});
}
private void Update()
{
if (pendingFxReturns.Count == 0) return;
float now = Time.unscaledTime;
for (int i = pendingFxReturns.Count - 1; i >= 0; i--)
{
PendingFxReturn pending = pendingFxReturns[i];
if (pending.Instance == null)
{
pendingFxReturns.RemoveAt(i);
continue;
}
if (now < pending.ReturnTime) continue;
pendingFxReturns.RemoveAt(i);
if (CurrentRentToken(pending.Instance) != pending.RentToken) continue;
ReturnFxInstance(pending.Instance);
}
}
private void ReturnAllRentedFx()
{
var snapshot = new List<GameObject>(rentedFx);
@@ -364,6 +407,7 @@ public class AnimationController : MonoBehaviour
{
holdActive = false;
holdParticleCoroutine = null;
pendingFxReturns.Clear();
ReturnAllRentedFx();
}
@@ -409,6 +453,10 @@ public class AnimationController : MonoBehaviour
main.startColor = tint;
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
// Force pooled FX back to time 0 before replaying, matching a fresh Instantiate
// without paying allocation/destruction cost on every hit.
try { ps.Simulate(0f, false, true); } catch { }
try { ps.Clear(true); } catch { }
}
}
@@ -736,85 +784,48 @@ public class AnimationController : MonoBehaviour
// 判定字(PERFECT/GREAT 等)统一且仅由 Animation_GenerateJudgementSituationPrefab 负责生成,
// 本方法只负责打击粒子 + ring,不碰判定字。
// 【彻底去池化】前几轮所有"粒子变多/变少、ring 不激活、复用状态错乱"都源于对象池复用时
// 子节点激活态/粒子播放态无法可靠复位。改回最朴素可靠的 Instantiate + 定时 Destroy
// 每次判定 new 一个全新干净实例(与 prefab 设计态完全一致),播放完销毁。无复用=无脏状态。
// 这正是加池化之前本就正常工作的行为。
// 粒子本体:以场景源为模板实例化一份全新拷贝。
SpawnOneShotFx(source, spawnPos, parentTransform, trackColor, true);
// 粒子本体:走对象池,避免高频判定时 Instantiate/Destroy 造成移动端掉帧。
SpawnOneShotFx(source, spawnPos, parentTransform, trackColor, true, trackAsHoldParticle);
// Ring(叠加环)tap / hold 头尾 / hold 持续 tick 都按同一频率生成一次性实例。
// 实例会按自身生命周期 Destroy,不会修改或启动场景里的模板对象。
// 实例会按自身生命周期回池,不会修改或启动场景里的模板对象。
if (hit_ring_object != null)
{
SpawnOneShotFx(hit_ring_object, spawnPos, parentTransform, trackColor, true);
SpawnOneShotFx(hit_ring_object, spawnPos, parentTransform, trackColor, true, trackAsHoldParticle);
}
}
// 实例化一个一次性特效(粒子/ring),播放后按自身生命周期 Destroy。无对象池、无复用
// applyTint=true 时把粒子 startColor 染成轨道色(粒子本体)ring 保留自身配色(false)
private void SpawnOneShotFx(GameObject template, Vector3 spawnPos, Transform parentTransform, Color tint, bool applyTint)
// 播放一个一次性特效(粒子/ring),播放后按自身生命周期回池
// applyTint=true 时把粒子 startColor 染成轨道色;当前调用保持旧行为,粒子和 ring 都染色
private void SpawnOneShotFx(GameObject template, Vector3 spawnPos, Transform parentTransform, Color tint, bool applyTint, bool trackAsHoldParticle)
{
if (template == null) return;
GameObject instance = Instantiate(template);
GameObject instance = RentFxInstance(template, out FxCacheEntry entry, out int rentToken);
if (instance == null) return;
// 去除粒子本体克隆里"自带的嵌套 ring"hit_ring_object 本身是 hit_particular_object 的子物体,
// Instantiate(粒子本体) 会连它一起克隆(原色,未染色) → 与我另外单独实例化的染色 ring 叠成双层。
// 先 SetActive(false) 立即让它停播,并从下面的系统列表中排除,再销毁;只保留单独实例化的染色 ring。
Transform nestedRing = null;
if (template == hit_particular_object && !string.IsNullOrEmpty(hitRingPathUnderParticleSource))
{
nestedRing = instance.transform.Find(hitRingPathUnderParticleSource);
if (nestedRing != null)
{
nestedRing.gameObject.SetActive(false);
}
}
// 定位:跟随分轨效果对象(worldPositionStays=false 保留模板设计的本地朝向,如 ring 的 -90°X)。
instance.transform.SetParent(parentTransform, false);
instance.transform.position = spawnPos;
instance.transform.localRotation = template.transform.localRotation;
instance.transform.localRotation = entry != null ? entry.LocalRotation : template.transform.localRotation;
instance.SetActive(true);
// 排除嵌套 ring 的粒子系统,避免把那层原色 ring 播出来。
var systems = FilterParticleSystems(instance.GetComponentsInChildren<ParticleSystem>(true), nestedRing);
if (nestedRing != null) Destroy(nestedRing.gameObject);
float lifetime = 0.5f;
if (systems != null && systems.Length > 0)
if (applyTint)
{
lifetime = ComputeFxLifetime(systems, null);
// 关键修复(开局粒子多、之后变少的衰减):模板 hit_particular_object 是活动场景对象,
// Instantiate 会连同模板粒子系统"当前缓冲里的活粒子"一起克隆——歌曲早期模板残留/预热粒子多
// → 克隆多;随模板粒子消亡 → 克隆少。故必须把克隆继承的模拟状态彻底重置为"时间0、零粒子"
// 再从头播放,保证每次一致、与模板当时状态无关。
foreach (var ps in systems)
{
if (ps == null) continue;
var main = ps.main;
if (applyTint) main.startColor = tint;
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
// Simulate(0, withChildren:false, restart:true) 强制该系统回到时间 0 的干净态,
// 抹掉 Instantiate 继承的任何已模拟粒子(单靠 Clear 有时不彻底)。
try { ps.Simulate(0f, false, true); } catch { }
try { ps.Clear(true); } catch { }
}
// 全部重置后,只对根系统 Play(true) 级联播放,从零开始正常发射。
var rootPs = instance.GetComponent<ParticleSystem>();
if (rootPs == null) rootPs = systems[0];
try { rootPs.Play(true); } catch { }
RestartFxPlayback(entry, tint);
}
else
{
var animators = instance.GetComponentsInChildren<Animator>(true);
lifetime = ComputeFxLifetime(null, animators);
RestartFxPlayback(entry, Color.white);
}
Destroy(instance, Mathf.Max(0.5f, lifetime + 0.1f));
if (trackAsHoldParticle)
{
activeHoldParticles.Add(instance);
}
float lifetime = entry != null ? entry.Lifetime : 0.5f;
ScheduleFxReturn(instance, rentToken, Mathf.Max(0.5f, lifetime + 0.1f));
}
// Documentation text normalized.
@@ -10,6 +10,11 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
public GameObject prefabSource;
}
private sealed class PooledTrackSkillTag : MonoBehaviour
{
public GameObject prefabSource;
}
public static Animation_GenerateJudgementSituationPrefab Instance;
[Header("Inspector")]
@@ -103,6 +108,8 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
private Coroutine prewarmJudgeCoroutine;
private readonly Dictionary<GameObject, Stack<GameObject>> judgePools = new Dictionary<GameObject, Stack<GameObject>>();
private readonly HashSet<GameObject> rentedJudges = new HashSet<GameObject>();
private readonly Dictionary<GameObject, Stack<GameObject>> trackSkillPools = new Dictionary<GameObject, Stack<GameObject>>();
private readonly HashSet<GameObject> rentedTrackSkills = new HashSet<GameObject>();
private float lastTrackSkillSpawnTime = -999f;
private int trackSkillBurstCount = 0;
@@ -226,7 +233,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
if (spawnPoint == null)
return;
GameObject instance = Instantiate(trackSkill_prefab, spawnPoint, false);
GameObject instance = GetPooledTrackSkill(trackSkill_prefab, spawnPoint);
if (instance == null)
return;
@@ -289,7 +296,8 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
perfect_judge_prefab,
great_judge_prefab,
good_judge_prefab,
miss_judge_prefab
miss_judge_prefab,
trackSkill_prefab
};
int count = Mathf.Max(1, perPrefab);
@@ -303,12 +311,24 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
GameObject instance = null;
try
{
instance = CreatePooledJudge(prefab);
if (instance != null)
if (prefab == trackSkill_prefab)
{
rentedJudges.Add(instance);
instance = CreatePooledTrackSkill(prefab);
if (instance != null)
{
rentedTrackSkills.Add(instance);
}
ReturnTrackSkillToPool(instance);
}
else
{
instance = CreatePooledJudge(prefab);
if (instance != null)
{
rentedJudges.Add(instance);
}
ReturnJudgeToPool(instance);
}
ReturnJudgeToPool(instance);
}
catch { }
@@ -407,7 +427,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
if (obj != null)
{
if (destroyAtEnd)
Destroy(obj);
ReturnTrackSkillToPool(obj);
else
ReturnJudgeToPool(obj);
}
@@ -573,6 +593,81 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
return instance;
}
private GameObject GetPooledTrackSkill(GameObject prefab, Transform parent)
{
if (prefab == null || parent == null)
return null;
if (!trackSkillPools.TryGetValue(prefab, out var pool))
{
pool = new Stack<GameObject>();
trackSkillPools[prefab] = pool;
}
GameObject instance = null;
while (pool.Count > 0 && instance == null)
{
instance = pool.Pop();
}
if (instance == null)
{
instance = CreatePooledTrackSkill(prefab);
}
if (instance == null)
return null;
instance.transform.SetParent(parent, false);
instance.SetActive(true);
rentedTrackSkills.Add(instance);
return instance;
}
private GameObject CreatePooledTrackSkill(GameObject prefab)
{
if (prefab == null)
return null;
GameObject instance = Instantiate(prefab, transform);
var tag = instance.GetComponent<PooledTrackSkillTag>();
if (tag == null)
tag = instance.AddComponent<PooledTrackSkillTag>();
tag.prefabSource = prefab;
EnsureRenderableTrackSkillInstance(instance);
instance.SetActive(false);
return instance;
}
private void ReturnTrackSkillToPool(GameObject instance)
{
if (instance == null)
return;
if (!rentedTrackSkills.Remove(instance))
return;
var tag = instance.GetComponent<PooledTrackSkillTag>();
if (tag == null || tag.prefabSource == null)
{
Destroy(instance);
return;
}
if (!trackSkillPools.TryGetValue(tag.prefabSource, out var pool))
{
pool = new Stack<GameObject>();
trackSkillPools[tag.prefabSource] = pool;
}
instance.transform.SetParent(transform, false);
instance.transform.localPosition = new Vector3(99999f, 99999f, 0f);
instance.transform.localRotation = Quaternion.identity;
instance.SetActive(false);
pool.Push(instance);
}
private void ReturnJudgeToPool(GameObject instance)
{
if (instance == null)
@@ -29,12 +29,12 @@ public sealed class CameraResolutionTuner : MonoBehaviour
[Header("21:9 时相机本体(Main Camera)的目标 local 位姿(最大偏移)")]
[Tooltip("21:9 时相机本体的目标 local Z 高度。用户指定 2.5(相机自身 z=2.5)。")]
public float targetCameraY = 0f;
public float targetCameraY = -2.13f;
public float targetCameraZ = 2.5f;
public float targetCameraZ = 3.6f;
[Tooltip("21:9 时相机本体的目标 local X 旋转角度。用户指定 5(相机自身 X=5°)。")]
public float targetCameraRotX = 5f;
public float targetCameraRotX = -10f;
private float _lastAspect = -1f;
private bool _lastEnabled;
+24 -23
View File
@@ -67,6 +67,11 @@ public class GameManager : MonoBehaviour
[Tooltip("Delay in seconds before starting the black mask fade out. Configure in inspector.")]
public float blackMaskFadeStartDelay = 1.5f;
[Header("Gameplay Prewarm")]
[SerializeField] private int judgePopupPrewarmPerPrefab = 5;
[SerializeField] private int trackHitParticlePrewarmCount = 24;
[SerializeField] private int instantNumberPrewarmPerPrefab = 18;
private bool pauseSubscribed = false;
private bool musicWasPlayingBeforePause = false;
@@ -510,11 +515,13 @@ public class GameManager : MonoBehaviour
var judgeFx = Animation_GenerateJudgementSituationPrefab.Instance
?? SceneObjectLookupCache.FindAny<Animation_GenerateJudgementSituationPrefab>();
judgeFx?.PrewarmJudgePrefabs(5);
judgeFx?.PrewarmJudgePrefabs(judgePopupPrewarmPerPrefab);
var trackHitFx = TrackJudgeHitEffectController.Instance
?? SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
trackHitFx?.PrewarmTrackParticles(10);
trackHitFx?.PrewarmTrackParticles(trackHitParticlePrewarmCount);
iNumberPrefabController.PrewarmRuntime(instantNumberPrewarmPerPrefab);
var fxEvent = effectEventController.Instance ?? SceneObjectLookupCache.FindAny<effectEventController>();
if (fxEvent != null && fxEvent.isActiveAndEnabled)
@@ -536,18 +543,10 @@ public class GameManager : MonoBehaviour
// Detect Space or yellow note key to request start (only before gameplay starts)
if (!startRequested)
{
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
// Check Space key
if (Input.GetKeyDown(KeyCode.Space))
if (IsYellowStartKeyDown())
{
RequestStart();
}
// Check yellow track key from key binding manager
else if (yellowKey != KeyCode.None && Input.GetKeyDown(yellowKey))
{
if (yellowKey == KeyCode.Space) return;
else RequestStart();
}
// Check Escape key to trigger return to selecting page (only before gameplay starts and allowed)
if (allowEscapeReturn && Input.GetKeyDown(KeyCode.Escape))
@@ -558,6 +557,17 @@ public class GameManager : MonoBehaviour
// Once startRequested is true, Escape is disabled and gameplay begins countdown
}
private bool IsYellowStartKeyDown()
{
if (Input.GetKeyDown(KeyCode.Space)) return true;
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
if (yellowKey != KeyCode.None && yellowKey != KeyCode.Space && Input.GetKeyDown(yellowKey)) return true;
KeyCode secondaryYellowKey = KeyBindingManager.GetSecondaryKeyForTrack3();
return secondaryYellowKey != KeyCode.None && secondaryYellowKey != KeyCode.Space && Input.GetKeyDown(secondaryYellowKey);
}
private void OnDestroy()
{
RecordTotalPlayTime();
@@ -1281,11 +1291,8 @@ public class GameManager : MonoBehaviour
PauseManager.Instance?.Pause(true);
UpdateStatusOnConsole("Paused: press Space to start playback");
// Determine yellow track key from key binding manager
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
// Wait for start request: Space, yellow key, or UI button
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey)))
while (!startRequested && !IsYellowStartKeyDown())
{
yield return null;
}
@@ -1357,11 +1364,8 @@ public class GameManager : MonoBehaviour
PauseManager.Instance?.Pause(true);
UpdateStatusOnConsole("Paused: press Space to start playback");
// Determine yellow track key from key binding manager
KeyCode yellowKey = KeyBindingManager.GetKeyForColor("yellow");
// Wait for start request: Space, yellow key, or UI button
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey == KeyCode.None || !Input.GetKeyDown(yellowKey)))
while (!startRequested && !IsYellowStartKeyDown())
{
yield return null;
}
@@ -1468,11 +1472,8 @@ public class GameManager : MonoBehaviour
PauseManager.Instance?.Pause(true);
UpdateStatusOnConsole("Paused: press Space to start playback");
// Determine yellow track key from key binding manager
KeyCode yellowKey2 = KeyBindingManager.GetKeyForColor("yellow");
// Wait for start request: Space, yellow key, or UI button
while (!startRequested && !Input.GetKeyDown(KeyCode.Space) && (yellowKey2 == KeyCode.None || !Input.GetKeyDown(yellowKey2)))
while (!startRequested && !IsYellowStartKeyDown())
{
yield return null;
}
@@ -525,6 +525,57 @@ public class HoldNote : BaseNote
bool keyUp = !keyHeld && prevTrackHeld;
prevTrackHeld = keyHeld;
// 池化泄漏兜底:不依赖 hasEnteredLine / 几何进入离开事件。
// 正常情况下 middle/start/end 由 HandleZoneEnter/Exit(几何驱动)或第 556 行的
// in-zone auto-miss 负责回收;但当几何阈值未就绪、或帧尖峰导致音符 Y 未被采样到判定区内时,
// 上述事件可能永不触发,音符会一直下落不回池,严重拖累性能(尤其 useMultiSegmentFill 使
// 中段数量随速度倍增)。此兜底在"正常判定窗口 + backstopBuffer"之后才触发,严格晚于所有正常
// 路径,因此对正常音符零行为改变——仅回收那些几何事件从未触发、否则永远泄漏的段。
{
float missRangeBackstop = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
const float backstopBuffer = 0.5f;
bool startJudgedBs = jm != null && jm.IsStartJudged(noteID);
if (segment == NoteSegment.End)
{
if (scheduledEndTime > 0f && now > scheduledEndTime + missRangeBackstop + backstopBuffer)
{
// 兜底触发时音符早已越过判定线,几何仅是漏采样。置 hasEnteredLine 让 ReturnToPool
// 的 Start 守卫(第 1519 行,拒收未入线 Start)不再阻挡回收。
hasEnteredLine = true;
// 已按住到尾部的 End 早已在第 677 行按 Perfect 自动完成并 return;
// 走到这里说明几何事件从未触发。按原 HandleZoneExit(End) 的收尾语义处理。
bool releasedBs = jm != null && jm.HasNoteReleased(noteID);
if (!startJudgedBs)
{
if (!releasedBs) HandleEnd(true); // 头段未判定 → End 记 Miss
}
else
{
HandleEnd(); // 头段已判定:按松手时间正常评估收尾
}
isJudged = true;
ScheduleReturnToPool(0.2f);
return;
}
}
else if (hitTime > 0f && now > hitTime + missRangeBackstop + backstopBuffer)
{
// 兜底触发时音符早已越过判定线,几何仅是漏采样。置 hasEnteredLine 让 ReturnToPool
// 的 Start 守卫(第 1519 行,拒收未入线 Start)不再阻挡回收。
hasEnteredLine = true;
// Start 未判定 → 走正常 Miss(与第 569 行一致);已判定的 start / middle → 静默回收
// (与第 579-581 行、第 883 行一致,绝不 StopHoldParticles 杀掉长按持续特效)。
if (segment == NoteSegment.Start && !startJudgedBs)
{
EvaluateHoldEnd(now, true);
}
isJudged = true;
ScheduleReturnToPool(0.2f);
return;
}
}
// Optimization: Early exit if the note is far from judgment line and hasn't entered yet
if (!hasEnteredLine)
{
@@ -31,23 +31,33 @@ public class HoldNoteController : MonoBehaviour
if (rb != null) rb.simulated = false;
}
private void Update()
private void OnEnable()
{
NoteRuntimeTickManager.Register(this);
}
private void OnDisable()
{
NoteRuntimeTickManager.Unregister(this);
}
public void ManualTick(float songTime, float deltaTime)
{
if (useAbsolutePositioning)
{
if (isMoving) ApplyAbsolutePosition(GameplayClock.NowSongTime);
if (isMoving) ApplyAbsolutePosition(songTime);
}
else
{
// Start moving after activation time in legacy mode
if (!isMoving && GameplayClock.NowSongTime >= activationTime)
if (!isMoving && songTime >= activationTime)
{
isMoving = true;
}
if (isMoving)
{
transform.Translate(Vector3.down * speed * Time.deltaTime);
transform.Translate(Vector3.down * speed * deltaTime);
}
}
@@ -44,6 +44,7 @@ public class InputManager : MonoBehaviour
public Color missColor = Color.red;
private KeyCode[] cachedKeys = new KeyCode[5];
private KeyCode cachedSecondaryTrack3Key = KeyCode.V;
private bool pauseBlockedLastFrame = false;
// Per-track held count. Written by both keyboard (Update) and touch (PressTrack/
@@ -89,6 +90,7 @@ public class InputManager : MonoBehaviour
{
cachedKeys[i] = KeyBindingManager.GetKeyForColor(TrackColors[i]);
}
cachedSecondaryTrack3Key = KeyBindingManager.GetSecondaryKeyForTrack3();
}
private void Start()
@@ -137,6 +139,14 @@ public class InputManager : MonoBehaviour
if (Input.GetKeyUp(key))
ReleaseTrack(i);
}
if (cachedSecondaryTrack3Key != KeyCode.None)
{
if (Input.GetKeyDown(cachedSecondaryTrack3Key))
PressTrack(2, cachedSecondaryTrack3Key);
if (Input.GetKeyUp(cachedSecondaryTrack3Key))
ReleaseTrack(2);
}
}
/// <summary>
@@ -146,6 +156,12 @@ public class InputManager : MonoBehaviour
/// Always fires OnKeyPressed to allow rapid same-track taps even when held.
/// </summary>
public void PressTrack(int index)
{
KeyCode sourceKey = index >= 0 && index < cachedKeys.Length ? cachedKeys[index] : KeyCode.None;
PressTrack(index, sourceKey);
}
public void PressTrack(int index, KeyCode sourceKey)
{
if (index < 0 || index >= TrackColors.Length) return;
bool wasHeld = trackHeldCount[index] > 0;
@@ -156,6 +172,17 @@ public class InputManager : MonoBehaviour
if (key != KeyCode.None)
OnKeyPressed?.Invoke(key);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
{
if (sourceKey != KeyCode.None)
txt.text = KeyBindingManager.GetDisplayName(sourceKey);
txt.color = keyActiveColor;
}
}
// Only update visuals on the first press (0→1 transition)
if (wasHeld) return;
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 088ed1ed6b6731f43ad3166781e3e931, type: 3}
m_Name: NoteJudgeConfig
m_EditorClassIdentifier:
perfectRange: 0.075
greatRange: 0.1
goodRange: 0.125
missRange: 0.15
perfectRange: 0.1
greatRange: 0.12
goodRange: 0.15
missRange: 0.18
@@ -3,10 +3,12 @@ using System.Collections.Generic;
public class KeyBindingManager : MonoBehaviour
{
private static Dictionary<string, KeyCode> keyBindings = new Dictionary<string, KeyCode>();
private static readonly string[] colors = { "red", "green", "yellow", "purple", "blue" };
private static readonly KeyCode[] defaultKeys = { KeyCode.D, KeyCode.F, KeyCode.Space, KeyCode.J, KeyCode.K };
private static bool initialized = false;
private static Dictionary<string, KeyCode> keyBindings = new Dictionary<string, KeyCode>();
private static readonly string[] colors = { "red", "green", "yellow", "purple", "blue" };
private static readonly KeyCode[] defaultKeys = { KeyCode.D, KeyCode.F, KeyCode.Space, KeyCode.J, KeyCode.K };
private const string SecondaryTrack3PrefsKey = "KeyBinding_yellow_secondary";
private static KeyCode secondaryTrack3Key = KeyCode.V;
private static bool initialized = false;
private void Awake()
{
@@ -14,7 +16,7 @@ public class KeyBindingManager : MonoBehaviour
{
LoadKeyBindings();
}
Debug.Log("KeyBindingManager Awake() 被调用,开始加载按键映射...");
Debug.Log("KeyBindingManager Awake() 被调用,开始加载按键映射...");
}
public static KeyCode GetKeyForColor(string color)
@@ -38,28 +40,58 @@ public class KeyBindingManager : MonoBehaviour
return defaultKeys[i];
}
}
return KeyCode.None;
}
public static void ChangeKeyBinding(string color, KeyCode newKey)
{
if (string.IsNullOrEmpty(color)) return;
var keyLower = color.ToLower();
keyBindings[keyLower] = newKey;
SaveKeyBindings();
}
private static void SaveKeyBindings()
{
for (int i = 0; i < colors.Length; i++)
{
var col = colors[i];
return KeyCode.None;
}
public static KeyCode GetSecondaryKeyForTrack3()
{
if (!initialized)
{
LoadKeyBindings();
}
return secondaryTrack3Key;
}
public static bool IsKeyForColor(string color, KeyCode key)
{
if (!initialized)
{
LoadKeyBindings();
}
if (key == KeyCode.None || string.IsNullOrEmpty(color)) return false;
string keyLower = color.ToLower();
if (GetKeyForColor(keyLower) == key) return true;
return keyLower == "yellow" && secondaryTrack3Key == key;
}
public static void ChangeKeyBinding(string color, KeyCode newKey)
{
if (string.IsNullOrEmpty(color)) return;
var keyLower = color.ToLower();
keyBindings[keyLower] = newKey;
SaveKeyBindings();
}
public static void ChangeSecondaryTrack3KeyBinding(KeyCode newKey)
{
secondaryTrack3Key = newKey;
SaveKeyBindings();
}
private static void SaveKeyBindings()
{
for (int i = 0; i < colors.Length; i++)
{
var col = colors[i];
KeyCode k = defaultKeys[i];
if (keyBindings.TryGetValue(col, out KeyCode stored)) k = stored;
PlayerPrefs.SetInt($"KeyBinding_{col}", (int)k);
}
PlayerPrefs.Save();
}
if (keyBindings.TryGetValue(col, out KeyCode stored)) k = stored;
PlayerPrefs.SetInt($"KeyBinding_{col}", (int)k);
}
PlayerPrefs.SetInt(SecondaryTrack3PrefsKey, (int)secondaryTrack3Key);
PlayerPrefs.Save();
}
private static void LoadKeyBindings()
{
@@ -73,12 +105,22 @@ public class KeyBindingManager : MonoBehaviour
}
else
{
keyBindings[col] = defaultKeys[i];
}
}
initialized = true;
Debug.Log("KeyBindings 初始化成功:" + string.Join(", ", keyBindings));
}
keyBindings[col] = defaultKeys[i];
}
}
if (PlayerPrefs.HasKey(SecondaryTrack3PrefsKey))
{
secondaryTrack3Key = (KeyCode)PlayerPrefs.GetInt(SecondaryTrack3PrefsKey);
}
else
{
secondaryTrack3Key = KeyCode.V;
PlayerPrefs.SetInt(SecondaryTrack3PrefsKey, (int)secondaryTrack3Key);
PlayerPrefs.Save();
}
initialized = true;
Debug.Log("KeyBindings 初始化成功:" + string.Join(", ", keyBindings));
}
// Return a human-friendly display string for a KeyCode (symbols shown as their character)
public static string GetDisplayName(KeyCode key)
+17
View File
@@ -435,6 +435,16 @@ public class Note : BaseNote
}
else
{
// "提前按下"不再判 Miss:音符尚未到达判定线(pressTime < hitTime)且超出 good 窗口,
// 视为过早误触 → 无任何反应,释放锁并保持未判定,音符继续下落等待玩家在正确时机再次击打。
// 仅"过晚"(pressTime >= hitTime,音符已越过判定点)才照常判 Miss;
// 完全没接住的情况由 Update 中 missDeadlineTime 的兜底 auto-Miss 处理(属"过晚没接住")。
if (pressTime < hitTime)
{
ReleaseTrackLock(myId);
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} early press ignored (no miss): pressTime={pressTime:F3}, hitTime={hitTime:F3}");
return;
}
JudgeMiss();
return;
}
@@ -459,6 +469,13 @@ public class Note : BaseNote
}
else
{
// 同上:提前误触不判 Miss,仅过晚才判 Miss。
if (pressTime < hitTime)
{
ReleaseTrackLock(myId);
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} early press ignored (no miss, fallback): pressTime={pressTime:F3}, hitTime={hitTime:F3}");
return;
}
JudgeMiss();
return;
}
@@ -33,12 +33,22 @@ public class NoteController : MonoBehaviour
if (rb != null) rb.simulated = false;
}
private void Update()
private void OnEnable()
{
NoteRuntimeTickManager.Register(this);
}
private void OnDisable()
{
NoteRuntimeTickManager.Unregister(this);
}
public void ManualTick(float songTime)
{
if (isMoving && useAbsolutePositioning)
{
// Use absolute positioning based on elapsed time since activation
float elapsedSinceActivation = GameplayClock.NowSongTime - activationTime;
float elapsedSinceActivation = songTime - activationTime;
float travelDistance = speed * Mathf.Max(0f, elapsedSinceActivation);
// Position = spawnPoint + initial offset + downward travel
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Centralizes runtime ticking for note controllers so dense charts do not pay one
/// MonoBehaviour.Update dispatch per active note/hold segment. Per-note movement
/// and judge-zone behavior remains in the original controllers.
/// </summary>
public sealed class NoteRuntimeTickManager : MonoBehaviour
{
private static NoteRuntimeTickManager instance;
private readonly List<NoteController> noteControllers = new List<NoteController>(256);
private readonly List<HoldNoteController> holdControllers = new List<HoldNoteController>(512);
public static void Register(NoteController controller)
{
if (controller == null) return;
EnsureInstance().RegisterInternal(controller);
}
public static void Unregister(NoteController controller)
{
if (controller == null || instance == null) return;
instance.noteControllers.Remove(controller);
}
public static void Register(HoldNoteController controller)
{
if (controller == null) return;
EnsureInstance().RegisterInternal(controller);
}
public static void Unregister(HoldNoteController controller)
{
if (controller == null || instance == null) return;
instance.holdControllers.Remove(controller);
}
private static NoteRuntimeTickManager EnsureInstance()
{
if (instance != null) return instance;
var go = new GameObject("NoteRuntimeTickManager");
instance = go.AddComponent<NoteRuntimeTickManager>();
return instance;
}
private void Awake()
{
if (instance == null)
{
instance = this;
}
else if (instance != this)
{
Destroy(gameObject);
}
}
private void OnDestroy()
{
if (instance == this) instance = null;
}
private void RegisterInternal(NoteController controller)
{
if (!noteControllers.Contains(controller))
noteControllers.Add(controller);
}
private void RegisterInternal(HoldNoteController controller)
{
if (!holdControllers.Contains(controller))
holdControllers.Add(controller);
}
private void Update()
{
float songTime = GameplayClock.NowSongTime;
float deltaTime = Time.deltaTime;
for (int i = noteControllers.Count - 1; i >= 0; i--)
{
NoteController controller = noteControllers[i];
if (controller == null || !controller.isActiveAndEnabled)
{
noteControllers.RemoveAt(i);
continue;
}
controller.ManualTick(songTime);
}
for (int i = holdControllers.Count - 1; i >= 0; i--)
{
HoldNoteController controller = holdControllers[i];
if (controller == null || !controller.isActiveAndEnabled)
{
holdControllers.RemoveAt(i);
continue;
}
controller.ManualTick(songTime, deltaTime);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6199ecee8133e8647bb30dd21fb7a764
@@ -26,6 +26,9 @@ public class iNumberPrefabController : MonoBehaviour
public GameObject ally04_number_toput;
public GameObject ally05_number_toput;
[Header("Prewarm")]
[SerializeField] private int defaultPrewarmPerPrefab = 12;
private bool _warnedMissingPrefab;
private bool _warnedMissingParents;
private Transform pooledRoot;
@@ -47,6 +50,52 @@ public class iNumberPrefabController : MonoBehaviour
{
TryAutoBindParentsFromTeamUI();
TryAutoBindPrefabFromScene();
PrewarmNumberPopups(defaultPrewarmPerPrefab);
}
public static void PrewarmRuntime(int perPrefab)
{
if (Instance == null) return;
Instance.PrewarmNumberPopups(perPrefab);
}
public void PrewarmNumberPopups(int perPrefab)
{
int count = Mathf.Max(0, perPrefab);
if (count <= 0) return;
PrewarmPrefab(ally_number_prefab, count);
PrewarmPrefab(enemy_number_prefab, count);
}
private void PrewarmPrefab(GameObject prefab, int count)
{
if (prefab == null || pooledRoot == null) return;
if (!pools.TryGetValue(prefab, out var pool))
{
pool = new System.Collections.Generic.Stack<playerInstantNumbersPrefab>();
pools[prefab] = pool;
}
int missing = Mathf.Max(0, count - pool.Count);
for (int i = 0; i < missing; i++)
{
var go = Instantiate(prefab, pooledRoot, false);
if (go == null) continue;
var view = go.GetComponent<playerInstantNumbersPrefab>();
if (view == null)
{
Destroy(go);
continue;
}
sourcePrefabs[view] = prefab;
view.ResetForReuse();
go.SetActive(false);
pool.Push(view);
}
}
private void TryAutoBindParentsFromTeamUI()
@@ -129,6 +129,10 @@ public class settlementController : MonoBehaviour
[Header("Settlement Intro Animation")]
[SerializeField] private bool enableDetailedSettlementIntro = true;
// 整体结算入场动画速度倍率。>1 更快,所有分段时长与间隔按此等比缩短,编排/节奏比例与业务不变。
// 新增字段:旧场景实例反序列化时缺此字段会取此默认值,因此无需改动 Inspector 即可生效。
[Tooltip("结算入场动画整体速度倍率(>1 更快)。等比缩短所有分段,不改变编排与业务逻辑。")]
[SerializeField] private float introTimeScale = 2f;
[SerializeField] private int introFlashCount = 2;
[SerializeField] private float introFlashDuration = 0.24f;
[SerializeField] private float introMoveDuration = 0.42f;
@@ -1723,7 +1727,7 @@ public class settlementController : MonoBehaviour
if (group != null) group.alpha = 0f;
mvpTransform.localPosition = new Vector3(baseLocalPosition.x, baseLocalPosition.y + introMvpEnterYOffset, baseLocalPosition.z);
float duration = Mathf.Max(0.01f, introMvpEnterDuration);
float duration = Mathf.Max(0.01f, ScaledDuration(introMvpEnterDuration));
Sequence seq = DOTween.Sequence().SetUpdate(introTweenUseUnscaledTime);
seq.Join(mvpTransform.DOLocalMoveY(baseLocalPosition.y, duration).SetEase(introMvpEnterEase).SetUpdate(introTweenUseUnscaledTime));
if (group != null)
@@ -1740,7 +1744,7 @@ public class settlementController : MonoBehaviour
SetCanvasAlpha(target.transform, 1f);
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
while (elapsed < dur)
{
@@ -1761,7 +1765,7 @@ public class settlementController : MonoBehaviour
SetCanvasAlpha(target.transform, 1f);
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
while (elapsed < dur)
{
@@ -1782,7 +1786,7 @@ public class settlementController : MonoBehaviour
SetCanvasAlpha(target.transform, 1f);
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
while (elapsed < dur)
{
@@ -1803,7 +1807,7 @@ public class settlementController : MonoBehaviour
target.fillAmount = 0f;
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
float targetValue = Mathf.Clamp01(toValue);
while (elapsed < dur)
@@ -1826,7 +1830,7 @@ public class settlementController : MonoBehaviour
if (fadeIn) SetCanvasAlpha(rt, 0f);
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
while (elapsed < dur)
{
if (skipIntroRequested) break;
@@ -1849,7 +1853,7 @@ public class settlementController : MonoBehaviour
SetCanvasAlpha(rt, 0f);
float elapsed = 0f;
float dur = Mathf.Max(0.01f, duration);
float dur = Mathf.Max(0.01f, ScaledDuration(duration));
while (elapsed < dur)
{
if (skipIntroRequested) break;
@@ -1868,7 +1872,7 @@ public class settlementController : MonoBehaviour
{
if (cg == null) yield break;
int count = Mathf.Max(1, flashes);
float total = Mathf.Max(0.05f, duration);
float total = Mathf.Max(0.05f, ScaledDuration(duration));
float flashWindow = total / Mathf.Max(1, count * 2);
cg.alpha = 0f;
@@ -1909,6 +1913,8 @@ public class settlementController : MonoBehaviour
{
if (target == null) yield break;
int count = Mathf.Max(1, flashes);
// 注意:这里的 step 传给 WaitRealtime,由其内部统一按 introTimeScale 缩放,
// 故此处用未缩放时长,避免二次缩放(否则会被缩放两次 → duration/scale²)。
float total = Mathf.Max(0.05f, duration);
float step = total / Mathf.Max(1, count * 2);
@@ -1960,10 +1966,17 @@ public class settlementController : MonoBehaviour
return 1f - inv * inv * inv;
}
// 结算动画整体提速:把任意时长按 introTimeScale 等比缩短。集中在此,避免逐个调用点改动。
private float ScaledDuration(float duration)
{
float scale = introTimeScale > 0.01f ? introTimeScale : 1f;
return duration / scale;
}
private IEnumerator WaitRealtime(float duration)
{
float elapsed = 0f;
float dur = Mathf.Max(0f, duration);
float dur = Mathf.Max(0f, ScaledDuration(duration));
while (elapsed < dur)
{
if (skipIntroRequested) yield break;
@@ -73,6 +73,14 @@ public class trackFractureController : MonoBehaviour
{
CacheTrackReferences();
ApplyRuntimeDissolveMaterials();
// 游戏准备开始时强制把所有轨道的 _Fade 复位为 1(完整显示,未溶解)。
// 碎裂溶解是 _Fade 1→0 的过程(见 fractureFadeStartValue/EndValue),1 表示轨道完整。
// 不克隆(cloneDissolveMaterialAtRuntime=0)时直接驱动 .mat 资产,若上次运行因异常
// (编辑器崩溃/强杀)未走 OnDestroy 的恢复,_Fade 可能残留为 0 导致本局轨道不显示。
// 这里在开局主动复位,作为兜底,避免此类未恢复的显示 bug。
SetAllTrackFade(1f);
SetAllTrackLocalZ(startLocalZ);
if (playStartZMove)