2修
This commit is contained in:
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
+49
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user