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
+22 -6
View File
@@ -53,6 +53,8 @@ public class ScoreManager : MonoBehaviour
private readonly TextMeshProUGUI[] slotTmpFallback = new TextMeshProUGUI[5];
private readonly Text[] slotLegacyFallback = new Text[5];
private readonly bool[] slotFallbackResolved = new bool[5];
private float nextIdolscoreKeyLookupTime = 0f;
private const float IdolscoreKeyLookupInterval = 0.5f;
// 性能:缓存 currentTotalScore 的 TMP/Text 组件,避免每次判定 GetComponent。
private TextMeshProUGUI _cachedTotalTmp;
@@ -324,12 +326,26 @@ public class ScoreManager : MonoBehaviour
private void EnsureIdolscoreKeys()
{
// Try to resolve missing refs (GameObject.Find doesn't return inactive objects).
if (_idolscoreKeys[0] == null) _idolscoreKeys[0] = SceneObjectLookupCache.Find("idolscoreKey_red")?.transform;
if (_idolscoreKeys[1] == null) _idolscoreKeys[1] = SceneObjectLookupCache.Find("idolscoreKey_green")?.transform;
if (_idolscoreKeys[2] == null) _idolscoreKeys[2] = SceneObjectLookupCache.Find("idolscoreKey_yellow")?.transform;
if (_idolscoreKeys[3] == null) _idolscoreKeys[3] = SceneObjectLookupCache.Find("idolscoreKey_purple")?.transform;
if (_idolscoreKeys[4] == null) _idolscoreKeys[4] = SceneObjectLookupCache.Find("idolscoreKey_blue")?.transform;
bool hasMissingKey = false;
for (int i = 0; i < _idolscoreKeys.Length; i++)
{
if (_idolscoreKeys[i] == null)
{
hasMissingKey = true;
break;
}
}
// Avoid scene lookup every frame when one of these optional key objects is absent.
if (hasMissingKey && Time.unscaledTime >= nextIdolscoreKeyLookupTime)
{
nextIdolscoreKeyLookupTime = Time.unscaledTime + IdolscoreKeyLookupInterval;
if (_idolscoreKeys[0] == null) _idolscoreKeys[0] = SceneObjectLookupCache.Find("idolscoreKey_red")?.transform;
if (_idolscoreKeys[1] == null) _idolscoreKeys[1] = SceneObjectLookupCache.Find("idolscoreKey_green")?.transform;
if (_idolscoreKeys[2] == null) _idolscoreKeys[2] = SceneObjectLookupCache.Find("idolscoreKey_yellow")?.transform;
if (_idolscoreKeys[3] == null) _idolscoreKeys[3] = SceneObjectLookupCache.Find("idolscoreKey_purple")?.transform;
if (_idolscoreKeys[4] == null) _idolscoreKeys[4] = SceneObjectLookupCache.Find("idolscoreKey_blue")?.transform;
}
for (int i = 0; i < 5; i++)
{
+131 -6
View File
@@ -12,6 +12,23 @@ public class SpriteNumberDisplay : MonoBehaviour
public Sprite plusSprite;
public Sprite minusSprite;
[Header("string")]
public string en_combo = "COMBO";
[Tooltip("COMBO 标签字形高度(像素)。宽度按各字母 sprite 宽高比自动计算。")]
public int en_combo_fontSize = 20;
public Color en_combo_color = Color.white;
public float en_combo_yOffset= 0f;
[Header("COMBO letter sprites")]
[Tooltip("字母 C 的 sprite")]
public Sprite letterC_sprite;
[Tooltip("字母 O 的 sprite")]
public Sprite letterO_sprite;
[Tooltip("字母 M 的 sprite")]
public Sprite letterM_sprite;
[Tooltip("字母 B 的 sprite")]
public Sprite letterB_sprite;
[Header("Layout")]
public Vector2 digitSize = new Vector2(28f, 42f);
public Vector2 decimalPointSize = new Vector2(12f, 42f);
@@ -19,6 +36,8 @@ public class SpriteNumberDisplay : MonoBehaviour
public float spacing = 2f;
public bool centerAlign = true;
public bool preserveAspect = true;
[Tooltip("Keep all visible glyphs at the configured height. Width is recalculated from each sprite aspect ratio.")]
public bool forceUniformHeight = true;
public Color color = Color.white;
[Tooltip("Canvas sorting order used by generated character sprites.")]
public int characterSortingOrder = 0;
@@ -26,6 +45,9 @@ public class SpriteNumberDisplay : MonoBehaviour
private readonly List<Image> glyphImages = new List<Image>();
private string currentText = string.Empty;
// 数字上方用 sprite 拼接的 "COMBO" 标签所用的 glyph 池。
private readonly List<Image> comboLabelImages = new List<Image>();
private void OnEnable()
{
Refresh();
@@ -39,6 +61,12 @@ public class SpriteNumberDisplay : MonoBehaviour
decimalPointSize.y = Mathf.Max(0f, decimalPointSize.y);
percentSize.x = Mathf.Max(0f, percentSize.x);
percentSize.y = Mathf.Max(0f, percentSize.y);
// 编辑器中改动 combo 标签相关字段时,已生成的字母 glyph 实时更新(不新建,避免编辑期实例化)。
if (comboLabelImages.Count > 0 && isActiveAndEnabled)
{
RefreshComboLabel();
}
}
public void SetNumber(int value)
@@ -83,7 +111,7 @@ public class SpriteNumberDisplay : MonoBehaviour
char c = currentText[i];
Sprite sprite = ResolveSprite(c);
Vector2 size = ResolveSize(c);
Vector2 size = ResolveSize(c, sprite);
if (sprite == null)
{
@@ -105,6 +133,94 @@ public class SpriteNumberDisplay : MonoBehaviour
x += size.x + spacing;
}
RefreshComboLabel();
}
// 用字母 sprite(C/O/M/B)在数字上方拼接 "COMBO" 标签。字形高度、颜色、Y 偏移由 Inspector 字段控制。
private void RefreshComboLabel()
{
string label = en_combo ?? string.Empty;
// 计算总宽度并按需扩容 glyph 池。
float height = Mathf.Max(1f, en_combo_fontSize);
float totalWidth = 0f;
int visibleCount = 0;
for (int i = 0; i < label.Length; i++)
{
Sprite s = ResolveComboLetterSprite(label[i]);
if (s == null) continue;
totalWidth += ComboLetterWidth(s, height);
visibleCount++;
}
if (visibleCount > 1) totalWidth += spacing * (visibleCount - 1);
EnsureComboImageCount(label.Length);
float x = centerAlign ? -totalWidth * 0.5f : 0f;
for (int i = 0; i < comboLabelImages.Count; i++)
{
Image image = comboLabelImages[i];
if (image == null) continue;
Sprite sprite = i < label.Length ? ResolveComboLetterSprite(label[i]) : null;
if (sprite == null)
{
image.gameObject.SetActive(false);
continue;
}
float w = ComboLetterWidth(sprite, height);
RectTransform rect = image.rectTransform;
image.gameObject.SetActive(true);
image.sprite = sprite;
image.color = en_combo_color;
image.preserveAspect = preserveAspect;
ApplyCharacterSortingOrder(image);
rect.sizeDelta = new Vector2(w, height);
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
// 上方定位:数字 glyph 以 y=0 居中、高约 digitSize.y,标签放其上半高 + 半个字形高 + 偏移处。
float baseY = digitSize.y * 0.5f + height * 0.5f;
rect.anchoredPosition = new Vector2(x + w * 0.5f, baseY + en_combo_yOffset);
x += w + spacing;
}
}
private void EnsureComboImageCount(int count)
{
while (comboLabelImages.Count < count)
{
GameObject glyph = new GameObject("combo_letter_" + comboLabelImages.Count, typeof(RectTransform), typeof(Canvas), typeof(Image));
RectTransform rect = glyph.transform as RectTransform;
rect.SetParent(transform, false);
Image image = glyph.GetComponent<Image>();
image.raycastTarget = false;
comboLabelImages.Add(image);
}
}
private Sprite ResolveComboLetterSprite(char c)
{
switch (c)
{
case 'C': case 'c': return letterC_sprite;
case 'O': case 'o': return letterO_sprite;
case 'M': case 'm': return letterM_sprite;
case 'B': case 'b': return letterB_sprite;
default: return null;
}
}
private float ComboLetterWidth(Sprite sprite, float height)
{
if (sprite == null || sprite.rect.height <= 0f) return height;
float aspect = sprite.rect.width / sprite.rect.height;
return height * aspect;
}
private void ApplyCharacterSortingOrder(Image image)
@@ -154,7 +270,7 @@ public class SpriteNumberDisplay : MonoBehaviour
continue;
}
total += ResolveSize(text[i]).x;
total += ResolveSize(text[i], ResolveSprite(text[i])).x;
visibleCount++;
}
@@ -181,10 +297,19 @@ public class SpriteNumberDisplay : MonoBehaviour
return null;
}
private Vector2 ResolveSize(char c)
private Vector2 ResolveSize(char c, Sprite sprite)
{
if (c == '.') return decimalPointSize;
if (c == '%') return percentSize;
return digitSize;
Vector2 baseSize = digitSize;
if (c == '.') baseSize = decimalPointSize;
else if (c == '%') baseSize = percentSize;
if (!forceUniformHeight || sprite == null || sprite.rect.height <= 0f)
{
return baseSize;
}
float height = Mathf.Max(0f, baseSize.y);
float aspect = sprite.rect.width / sprite.rect.height;
return new Vector2(height * aspect, height);
}
}
@@ -464,6 +464,7 @@ public static class FirstRunFactoryResetService
{
PlayerPrefs.DeleteKey($"KeyBinding_{keyBindingPrefixes[i]}");
}
PlayerPrefs.DeleteKey("KeyBinding_yellow_secondary");
}
private static void ResetPlayerMirrorSo()
@@ -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)
+271 -259
View File
@@ -2,18 +2,19 @@ using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class controllerSettings : MonoBehaviour
{
[Header("Inspector")]
public Button[] keyButtons; // assign 5 buttons in Inspector
public Text errorText; // UI text to show error/success messages
public Button[] keyButtons;
public Button secondary_track3Button;
public Text errorText;
[Header("Message Colors")]
public Color messageErrorColor = Color.red;
public Color messageSuccessColor = Color.green;
private Coroutine messageCoroutine = null;
[Header("Inspector")]
public Slider noteSpeedMultipler_slider;
@@ -22,236 +23,228 @@ public class controllerSettings : MonoBehaviour
public Button noteSpeed_IncreaseButton;
public Button noteSpeed_ResetButton;
// internal colors/order must match KeyBindingManager usage
private readonly string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
private bool isRebinding = false;
private int rebindingIndex = -1;
private Coroutine blinkCoroutine = null;
// store previous key so we can restore if user cancels
private KeyCode previousKey = KeyCode.None;
private bool previousKeyValid = false;
// slider range
private const float NoteSpeedMin = 1f;
// Player-facing range is 1-3. NoteSpawner multiplies it by 2, so the real range is 2-6.
private const float NoteSpeedMax = 3f;
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private const string NoteSpeedDefaultVersionKey = "noteSpeedMultiplierDefaultVersion";
private const int NoteSpeedDefaultVersion = 2;
private const float NoteSpeedDefault = 2.0f;
// continuous change
private Coroutine continuousChangeCoroutine = null;
private const float ContinuousInitialDelay = 0.5f; // increased to avoid accidental long-press
private const float ContinuousRepeatRate = 0.05f;
private const float StepAmount = 0.01f;
[Header("call dt prefab")]
public GameObject delayTapper_prefab;
public GameObject dt_to_put;
public Button launch_delayTapper;
public Text currentDelayText;
void Start()
{
// initialize button labels from KeyBindingManager
RefreshButtonLabels();
private readonly string[] colors = { "red", "green", "yellow", "purple", "blue" };
private readonly string[] bindingDisplayNames = { "轨道1键位", "轨道2键位", "轨道3键位", "轨道4键位", "轨道5键位", "轨道3键位2" };
// initialize errorText
if (errorText != null)
private const int SecondaryTrack3Slot = 5;
private const float NoteSpeedMin = 1f;
private const float NoteSpeedMax = 3f;
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private const string NoteSpeedDefaultVersionKey = "noteSpeedMultiplierDefaultVersion";
private const int NoteSpeedDefaultVersion = 2;
private const float NoteSpeedDefault = 2.0f;
private const float ContinuousInitialDelay = 0.5f;
private const float ContinuousRepeatRate = 0.05f;
private const float StepAmount = 0.01f;
private bool isRebinding;
private int rebindingIndex = -1;
private KeyCode previousKey = KeyCode.None;
private bool previousKeyValid;
private Coroutine blinkCoroutine;
private Coroutine messageCoroutine;
private Coroutine continuousChangeCoroutine;
private void Start()
{
RefreshButtonLabels();
ClearMessage();
SetupKeyButtonListeners();
SetupNoteSpeedControls();
SetupDelayTapper();
RefreshDelayText();
}
private void OnDestroy()
{
if (keyButtons != null)
{
errorText.text = string.Empty;
foreach (Button button in keyButtons)
{
if (button != null) button.onClick.RemoveAllListeners();
}
}
// hook up listeners for key buttons
if (secondary_track3Button != null) secondary_track3Button.onClick.RemoveAllListeners();
if (noteSpeedMultipler_slider != null) noteSpeedMultipler_slider.onValueChanged.RemoveListener(OnNoteSpeedSliderChanged);
if (noteSpeed_DecreaseButton != null) noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_IncreaseButton != null) noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_ResetButton != null) noteSpeed_ResetButton.onClick.RemoveAllListeners();
if (launch_delayTapper != null) launch_delayTapper.onClick.RemoveAllListeners();
}
private void Update()
{
if (isRebinding && Input.GetMouseButtonDown(0) && !IsPointerOverAnyKeyButton())
{
CancelRebind();
return;
}
if (!isRebinding) return;
foreach (KeyCode kc in Enum.GetValues(typeof(KeyCode)))
{
if (kc >= KeyCode.Mouse0 && kc <= KeyCode.Mouse6) continue;
if (kc >= KeyCode.JoystickButton0 && kc <= KeyCode.Joystick8Button19) continue;
if (!Input.GetKeyDown(kc)) continue;
HandleRebindKey(kc);
break;
}
}
public void RefreshDelayText()
{
if (currentDelayText == null) return;
const string delayPrefsKey = "UserGlobalDelaySeconds";
if (PlayerPrefs.HasKey(delayPrefsKey))
{
float savedDelaySeconds = PlayerPrefs.GetFloat(delayPrefsKey, 0f);
int ms = Mathf.RoundToInt(savedDelaySeconds * 1000f);
currentDelayText.text = $"当前偏移:{(ms >= 0 ? "+" : "")}{ms}ms";
}
else
{
currentDelayText.text = "未设定偏移数值";
}
}
public void OnLaunchDelayTapperClicked()
{
if (delayTapper_prefab == null || dt_to_put == null)
{
Debug.LogWarning("[controllerSettings] Prefab or target container is missing!");
return;
}
GameObject instantiated = Instantiate(delayTapper_prefab, dt_to_put.transform);
Debug.Log($"[controllerSettings] Instantiated delay tapper prefab under {dt_to_put.name}");
delayTapperPrefab dtp = instantiated.GetComponent<delayTapperPrefab>();
if (dtp != null)
{
dtp.mainSettings = this;
}
}
private void SetupKeyButtonListeners()
{
if (keyButtons != null)
{
for (int i = 0; i < keyButtons.Length && i < colors.Length; i++)
{
int idx = i;
if (keyButtons[i] == null) continue;
keyButtons[i].onClick.RemoveAllListeners();
keyButtons[i].onClick.AddListener(() => OnKeyButtonClicked(idx));
}
}
// initialize slider
if (secondary_track3Button != null)
{
secondary_track3Button.onClick.RemoveAllListeners();
secondary_track3Button.onClick.AddListener(() => OnKeyButtonClicked(SecondaryTrack3Slot));
}
}
private void SetupNoteSpeedControls()
{
if (noteSpeedMultipler_slider != null)
{
noteSpeedMultipler_slider.minValue = NoteSpeedMin;
noteSpeedMultipler_slider.maxValue = NoteSpeedMax;
EnsureDefaultNoteSpeedPreference();
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
saved = Mathf.Clamp(saved, NoteSpeedMin, NoteSpeedMax);
float saved = Mathf.Clamp(PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault), NoteSpeedMin, NoteSpeedMax);
noteSpeedMultipler_slider.value = saved;
UpdateNoteSpeedText(saved);
noteSpeedMultipler_slider.onValueChanged.RemoveAllListeners();
noteSpeedMultipler_slider.onValueChanged.AddListener(OnNoteSpeedSliderChanged);
// Add PointerUp event to save value when sliding ends (kept for compatibility)
EventTrigger trigger = noteSpeedMultipler_slider.gameObject.GetComponent<EventTrigger>();
if (trigger == null) trigger = noteSpeedMultipler_slider.gameObject.AddComponent<EventTrigger>();
trigger.triggers.RemoveAll(e => e.eventID == EventTriggerType.PointerUp);
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
entry.callback.AddListener((data) => { OnNoteSpeedSliderPointerUp(); });
EventTrigger.Entry entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
entry.callback.AddListener(_ => OnNoteSpeedSliderPointerUp());
trigger.triggers.Add(entry);
}
// setup buttons
if (noteSpeed_DecreaseButton != null)
{
noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
noteSpeed_DecreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(-StepAmount));
AddButtonContinuousEvents(noteSpeed_DecreaseButton, -StepAmount);
}
if (noteSpeed_IncreaseButton != null)
{
noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
noteSpeed_IncreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(StepAmount));
AddButtonContinuousEvents(noteSpeed_IncreaseButton, StepAmount);
}
if (noteSpeed_ResetButton != null)
{
noteSpeed_ResetButton.onClick.RemoveAllListeners();
noteSpeed_ResetButton.onClick.AddListener(() => ResetNoteSpeed());
}
// Initialize delay tapper button and display
if (launch_delayTapper != null)
{
launch_delayTapper.onClick.RemoveAllListeners();
launch_delayTapper.onClick.AddListener(OnLaunchDelayTapperClicked);
}
RefreshDelayText();
}
public void RefreshDelayText()
{
if (currentDelayText != null)
{
const string DELAY_PREFS_KEY = "UserGlobalDelaySeconds";
if (PlayerPrefs.HasKey(DELAY_PREFS_KEY))
{
float savedDelaySeconds = PlayerPrefs.GetFloat(DELAY_PREFS_KEY, 0f);
int ms = Mathf.RoundToInt(savedDelaySeconds * 1000f);
currentDelayText.text = $"当前偏移:{(ms >= 0 ? "+" : "")}{ms}ms";
}
else
{
currentDelayText.text = "未设定偏移数值";
}
noteSpeed_ResetButton.onClick.AddListener(ResetNoteSpeed);
}
}
public void OnLaunchDelayTapperClicked()
private void SetupDelayTapper()
{
if (delayTapper_prefab != null && dt_to_put != null)
{
GameObject instantiated = Instantiate(delayTapper_prefab, dt_to_put.transform);
Debug.Log($"[controllerSettings] Instantiated delay tapper prefab under {dt_to_put.name}");
// 获取 dtp 脚本并传递当前 settings 引用,以便保存时刷新 UI
delayTapperPrefab dtp = instantiated.GetComponent<delayTapperPrefab>();
if (dtp != null)
{
dtp.mainSettings = this;
}
}
else
{
Debug.LogWarning("[controllerSettings] Prefab or target container is missing!");
}
}
void OnDestroy()
{
if (keyButtons != null)
{
foreach (var b in keyButtons)
if (b != null) b.onClick.RemoveAllListeners();
}
if (noteSpeedMultipler_slider != null)
{
noteSpeedMultipler_slider.onValueChanged.RemoveListener(OnNoteSpeedSliderChanged);
// Do not remove EventTrigger to avoid affecting other listeners, but it's okay on destroy
}
if (noteSpeed_DecreaseButton != null) noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_IncreaseButton != null) noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_ResetButton != null) noteSpeed_ResetButton.onClick.RemoveAllListeners();
}
void Update()
{
// If waiting for a new key, also cancel if user clicks outside UI/buttons
if (isRebinding)
{
if (Input.GetMouseButtonDown(0))
{
if (!IsPointerOverAnyKeyButton())
{
// clicked outside the key buttons while rebinding -> cancel and restore
CancelRebind();
return;
}
}
}
if (!isRebinding) return;
// detect any keydown by iterating KeyCode values
foreach (KeyCode kc in Enum.GetValues(typeof(KeyCode)))
{
// ignore mouse buttons and joystick buttons
if (kc >= KeyCode.Mouse0 && kc <= KeyCode.Mouse6) continue;
if (kc >= KeyCode.JoystickButton0 && kc <= KeyCode.Joystick8Button19) continue;
if (Input.GetKeyDown(kc))
{
HandleRebindKey(kc);
break;
}
}
if (launch_delayTapper == null) return;
launch_delayTapper.onClick.RemoveAllListeners();
launch_delayTapper.onClick.AddListener(OnLaunchDelayTapperClicked);
}
private bool IsPointerOverAnyKeyButton()
{
if (EventSystem.current == null) return false;
var pointerData = new PointerEventData(EventSystem.current) { position = Input.mousePosition };
var results = new List<RaycastResult>();
PointerEventData pointerData = new PointerEventData(EventSystem.current) { position = Input.mousePosition };
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerData, results);
if (results.Count == 0) return false;
foreach (var res in results)
foreach (RaycastResult result in results)
{
var go = res.gameObject;
// check if this gameobject is one of the keyButtons or a child of one
for (int i = 0; i < keyButtons.Length; i++)
GameObject go = result.gameObject;
if (IsPointerOverButton(go, secondary_track3Button)) return true;
if (keyButtons == null) continue;
foreach (Button button in keyButtons)
{
var btn = keyButtons[i];
if (btn == null) continue;
if (go == btn.gameObject || go.transform.IsChildOf(btn.transform))
return true;
if (IsPointerOverButton(go, button)) return true;
}
}
return false;
}
private static bool IsPointerOverButton(GameObject go, Button button)
{
return go != null && button != null && (go == button.gameObject || go.transform.IsChildOf(button.transform));
}
private void OnKeyButtonClicked(int index)
{
if (isRebinding) return; // ignore while another rebind in progress
if (index < 0 || index >= colors.Length) return;
if (isRebinding || !IsValidBindingSlot(index)) return;
isRebinding = true;
rebindingIndex = index;
// remember previous key so we can restore if user cancels
previousKey = KeyBindingManager.GetKeyForColor(colors[index]);
previousKey = GetKeyForSlot(index);
previousKeyValid = true;
// start blinking underscore on the clicked button
blinkCoroutine = StartCoroutine(BlinkUnderscore(index));
// show persistent prompt
ShowPersistentMessage("按下新按键", messageErrorColor);
}
@@ -259,6 +252,7 @@ public class controllerSettings : MonoBehaviour
{
Text txt = GetButtonText(index);
if (txt == null) yield break;
while (isRebinding && rebindingIndex == index)
{
txt.text = "_";
@@ -266,50 +260,89 @@ public class controllerSettings : MonoBehaviour
txt.text = "";
yield return new WaitForSeconds(0.25f);
}
// restore label when finished
RefreshButtonLabel(index);
}
private void HandleRebindKey(KeyCode key)
{
// ignore ESC
if (key == KeyCode.Escape)
{
CancelRebind();
return;
}
// check if key already assigned to another color
for (int i = 0; i < colors.Length; i++)
if (!IsValidBindingSlot(rebindingIndex)) return;
int conflictSlot = FindSlotByKey(key, rebindingIndex);
KeyCode oldKey = GetKeyForSlot(rebindingIndex);
if (conflictSlot >= 0)
{
if (i == rebindingIndex) continue;
KeyCode existing = KeyBindingManager.GetKeyForColor(colors[i]);
if (existing == key)
{
// conflict: reject and keep waiting
string msg = $"按键冲突:{KeyBindingManager.GetDisplayName(key)}已被分配给按键{i+1}。";
ShowMessage(msg, messageErrorColor, 2f);
StartCoroutine(FlashButtonInvalid(rebindingIndex));
return;
}
SetKeyForSlot(rebindingIndex, key);
SetKeyForSlot(conflictSlot, oldKey);
string msg = $"按键冲突:{KeyBindingManager.GetDisplayName(key)}已与{GetSlotDisplayName(conflictSlot)}互换。";
ShowMessage(msg, messageErrorColor, 2f);
StartCoroutine(FlashButtonInvalid(rebindingIndex));
}
else
{
SetKeyForSlot(rebindingIndex, key);
ClearMessage();
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
}
// accept binding
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], key);
// update UI labels
RefreshButtonLabel(rebindingIndex);
InputManager.Instance?.RefreshKeyLabels();
// clear persistent prompt and show success message
ClearMessage();
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
// finish
previousKeyValid = false;
previousKey = KeyCode.None;
RefreshButtonLabels();
InputManager.Instance?.RefreshKeyLabels();
StopRebind();
}
private int FindSlotByKey(KeyCode key, int ignoredSlot)
{
for (int i = 0; i <= SecondaryTrack3Slot; i++)
{
if (i == ignoredSlot || !IsValidBindingSlot(i)) continue;
if (GetKeyForSlot(i) == key) return i;
}
return -1;
}
private bool IsValidBindingSlot(int index)
{
return (index >= 0 && index < colors.Length) || index == SecondaryTrack3Slot;
}
private KeyCode GetKeyForSlot(int index)
{
if (index == SecondaryTrack3Slot) return KeyBindingManager.GetSecondaryKeyForTrack3();
if (index >= 0 && index < colors.Length) return KeyBindingManager.GetKeyForColor(colors[index]);
return KeyCode.None;
}
private void SetKeyForSlot(int index, KeyCode key)
{
if (index == SecondaryTrack3Slot)
{
KeyBindingManager.ChangeSecondaryTrack3KeyBinding(key);
return;
}
if (index >= 0 && index < colors.Length)
{
KeyBindingManager.ChangeKeyBinding(colors[index], key);
}
}
private string GetSlotDisplayName(int index)
{
if (index >= 0 && index < bindingDisplayNames.Length) return bindingDisplayNames[index];
return $"键位{index + 1}";
}
private void ShowMessage(string message, Color color, float duration = 2f)
{
if (errorText == null) return;
@@ -327,25 +360,24 @@ public class controllerSettings : MonoBehaviour
t += Time.deltaTime;
yield return null;
}
errorText.text = string.Empty;
messageCoroutine = null;
}
// Show a persistent message (until explicitly cleared)
private void ShowPersistentMessage(string message, Color color)
{
if (errorText == null) return;
// stop any timed message
if (messageCoroutine != null)
{
StopCoroutine(messageCoroutine);
messageCoroutine = null;
}
errorText.text = message;
errorText.color = color;
}
// Clear any displayed message immediately
private void ClearMessage()
{
if (messageCoroutine != null)
@@ -353,29 +385,31 @@ public class controllerSettings : MonoBehaviour
StopCoroutine(messageCoroutine);
messageCoroutine = null;
}
if (errorText != null) errorText.text = string.Empty;
}
private IEnumerator FlashButtonInvalid(int index)
{
Button b = GetButton(index);
if (b == null) yield break;
Color original = b.image.color;
b.image.color = Color.red;
Button button = GetButton(index);
if (button == null || button.image == null) yield break;
Color original = button.image.color;
button.image.color = Color.red;
yield return new WaitForSeconds(0.35f);
b.image.color = original;
button.image.color = original;
}
private void CancelRebind()
{
// restore previous key if we have one
if (previousKeyValid && rebindingIndex >= 0 && rebindingIndex < colors.Length)
if (previousKeyValid && IsValidBindingSlot(rebindingIndex))
{
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], previousKey);
SetKeyForSlot(rebindingIndex, previousKey);
InputManager.Instance?.RefreshKeyLabels();
ClearMessage();
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
}
previousKeyValid = false;
previousKey = KeyCode.None;
StopRebind();
@@ -390,6 +424,7 @@ public class controllerSettings : MonoBehaviour
StopCoroutine(blinkCoroutine);
blinkCoroutine = null;
}
RefreshButtonLabels();
}
@@ -399,57 +434,49 @@ public class controllerSettings : MonoBehaviour
{
RefreshButtonLabel(i);
}
RefreshButtonLabel(SecondaryTrack3Slot);
}
// update RefreshButtonLabel to use display name
private void RefreshButtonLabel(int index)
{
Text txt = GetButtonText(index);
if (txt == null) return;
KeyCode k = KeyBindingManager.GetKeyForColor(colors[index]);
txt.text = KeyBindingManager.GetDisplayName(k);
txt.text = KeyBindingManager.GetDisplayName(GetKeyForSlot(index));
}
private Button GetButton(int index)
{
if (keyButtons == null) return null;
if (index < 0 || index >= keyButtons.Length) return null;
if (index == SecondaryTrack3Slot) return secondary_track3Button;
if (keyButtons == null || index < 0 || index >= keyButtons.Length) return null;
return keyButtons[index];
}
private Text GetButtonText(int index)
{
Button b = GetButton(index);
if (b == null) return null;
Text t = b.GetComponentInChildren<Text>();
return t;
Button button = GetButton(index);
return button != null ? button.GetComponentInChildren<Text>() : null;
}
private void OnNoteSpeedSliderChanged(float value)
{
UpdateNoteSpeedText(value);
// save on every change
SaveNoteSpeedValue(value);
}
private void OnNoteSpeedSliderPointerUp()
{
if (noteSpeedMultipler_slider != null)
{
float value = noteSpeedMultipler_slider.value;
PlayerPrefs.SetFloat(NoteSpeedPrefKey, value);
PlayerPrefs.Save();
}
if (noteSpeedMultipler_slider == null) return;
PlayerPrefs.SetFloat(NoteSpeedPrefKey, noteSpeedMultipler_slider.value);
PlayerPrefs.Save();
}
private void UpdateNoteSpeedText(float value)
{
if (noteSpeedMultipler_valueText != null)
{
// 将 0.5-2.0 映射为 50%-200% 的百分比显示
int percentage = Mathf.RoundToInt(value * 100f);
noteSpeedMultipler_valueText.text = $"{percentage}%";
}
if (noteSpeedMultipler_valueText == null) return;
int percentage = Mathf.RoundToInt(value * 100f);
noteSpeedMultipler_valueText.text = $"{percentage}%";
}
private void SaveNoteSpeedValue(float value)
@@ -462,44 +489,35 @@ public class controllerSettings : MonoBehaviour
private void ChangeNoteSpeedBy(float delta)
{
if (noteSpeedMultipler_slider == null) return;
float cur = noteSpeedMultipler_slider.value;
float step = StepAmount;
float newVal = cur;
float current = noteSpeedMultipler_slider.value;
float newValue = current;
const float eps = 1e-5f;
if (delta > 0f)
{
float ceilStep = Mathf.Ceil(cur / step) * step;
if (Mathf.Abs(cur - ceilStep) < eps)
{
newVal = Mathf.Min(cur + step, NoteSpeedMax);
}
else
{
newVal = Mathf.Min(ceilStep, NoteSpeedMax);
}
float ceilStep = Mathf.Ceil(current / StepAmount) * StepAmount;
newValue = Mathf.Abs(current - ceilStep) < eps
? Mathf.Min(current + StepAmount, NoteSpeedMax)
: Mathf.Min(ceilStep, NoteSpeedMax);
}
else if (delta < 0f)
{
float floorStep = Mathf.Floor(cur / step) * step;
if (Mathf.Abs(cur - floorStep) < eps)
{
newVal = Mathf.Max(cur - step, NoteSpeedMin);
}
else
{
newVal = Mathf.Max(floorStep, NoteSpeedMin);
}
float floorStep = Mathf.Floor(current / StepAmount) * StepAmount;
newValue = Mathf.Abs(current - floorStep) < eps
? Mathf.Max(current - StepAmount, NoteSpeedMin)
: Mathf.Max(floorStep, NoteSpeedMin);
}
newVal = Mathf.Clamp(newVal, NoteSpeedMin, NoteSpeedMax);
// round to 3 decimals for display and consistency
newVal = (float)System.Math.Round(newVal, 3);
noteSpeedMultipler_slider.value = newVal; // will trigger OnNoteSpeedSliderChanged and save
newValue = Mathf.Clamp(newValue, NoteSpeedMin, NoteSpeedMax);
newValue = (float)Math.Round(newValue, 3);
noteSpeedMultipler_slider.value = newValue;
}
private void ResetNoteSpeed()
{
if (noteSpeedMultipler_slider == null) return;
noteSpeedMultipler_slider.value = NoteSpeedDefault; // triggers change and save
noteSpeedMultipler_slider.value = NoteSpeedDefault;
SaveNoteSpeedValue(NoteSpeedDefault);
UpdateNoteSpeedText(NoteSpeedDefault);
}
@@ -527,30 +545,26 @@ public class controllerSettings : MonoBehaviour
PlayerPrefs.Save();
}
private void AddButtonContinuousEvents(Button btn, float delta)
private void AddButtonContinuousEvents(Button button, float delta)
{
EventTrigger trigger = btn.gameObject.GetComponent<EventTrigger>();
if (trigger == null) trigger = btn.gameObject.AddComponent<EventTrigger>();
EventTrigger trigger = button.gameObject.GetComponent<EventTrigger>();
if (trigger == null) trigger = button.gameObject.AddComponent<EventTrigger>();
// PointerDown -> start continuous change
var downEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
downEntry.callback.AddListener((data) => { StartContinuousChange(delta); });
EventTrigger.Entry downEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
downEntry.callback.AddListener(_ => StartContinuousChange(delta));
trigger.triggers.Add(downEntry);
// PointerUp -> stop continuous change
var upEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
upEntry.callback.AddListener((data) => { StopContinuousChange(); });
EventTrigger.Entry upEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
upEntry.callback.AddListener(_ => StopContinuousChange());
trigger.triggers.Add(upEntry);
// Also stop on PointerExit in case cursor leaves button while pressed
var exitEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
exitEntry.callback.AddListener((data) => { StopContinuousChange(); });
EventTrigger.Entry exitEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
exitEntry.callback.AddListener(_ => StopContinuousChange());
trigger.triggers.Add(exitEntry);
}
private void StartContinuousChange(float delta)
{
// perform immediate single step
ChangeNoteSpeedBy(delta);
if (continuousChangeCoroutine != null) StopCoroutine(continuousChangeCoroutine);
continuousChangeCoroutine = StartCoroutine(ContinuousChangeRoutine(delta));
@@ -558,11 +572,9 @@ public class controllerSettings : MonoBehaviour
private void StopContinuousChange()
{
if (continuousChangeCoroutine != null)
{
StopCoroutine(continuousChangeCoroutine);
continuousChangeCoroutine = null;
}
if (continuousChangeCoroutine == null) return;
StopCoroutine(continuousChangeCoroutine);
continuousChangeCoroutine = null;
}
private IEnumerator ContinuousChangeRoutine(float delta)