ui基本完毕,修了一大把的bug

This commit is contained in:
FloatGaming
2026-07-13 02:28:39 +08:00
parent 1e20d73e90
commit fd22501f71
958 changed files with 378289 additions and 41038 deletions
@@ -45,6 +45,195 @@ public class AnimationController : MonoBehaviour
// Track active particle instances for immediate cleanup
private List<GameObject> activeParticles = new List<GameObject>();
// Per-hit FX pooling. PlayDestroyAnimation used to Instantiate + Destroy three
// objects on every judge (hit particle, hit ring, judge text prefab). Under dense
// note bursts that is the dominant GC + instantiation spike. We now reuse instances
// from a pool keyed by template, and cache each instance's ParticleSystem[] +
// computed lifetime so the hot path never calls GetComponentsInChildren per hit.
// Business behavior is unchanged: same templates, same tint, same lifetime, same
// parenting; instances are recycled instead of created and destroyed.
private sealed class FxCacheEntry
{
public ParticleSystem[] Systems;
public float Lifetime;
}
private readonly Dictionary<GameObject, Queue<GameObject>> fxPools =
new Dictionary<GameObject, Queue<GameObject>>();
private readonly Dictionary<GameObject, FxCacheEntry> fxCache =
new Dictionary<GameObject, FxCacheEntry>();
private readonly Dictionary<GameObject, GameObject> fxInstanceTemplate =
new Dictionary<GameObject, GameObject>();
// Instances currently rented out. Return is guarded by this set so the timed
// RecycleFxRoutine and an explicit StopHoldParticles cleanup can never return the
// same instance twice (which would double-enqueue it and hand it out concurrently).
private readonly HashSet<GameObject> rentedFx = new HashSet<GameObject>();
// Per-rent token. Each rent bumps a counter and records it for the instance. The
// timed RecycleFxRoutine captures its token and only returns the instance if the
// token still matches, so a stale timer from a previous rent cannot recycle an
// instance that was already returned early (StopHoldParticles) and re-rented.
private int fxRentCounter;
private readonly Dictionary<GameObject, int> fxRentId = new Dictionary<GameObject, int>();
private Transform fxPoolRoot;
private void EnsureFxPoolRoot()
{
if (fxPoolRoot == null)
{
var root = new GameObject("AnimationControllerFxPool");
root.transform.SetParent(transform, false);
root.SetActive(false);
fxPoolRoot = root.transform;
}
}
// Creates one instance of the template, caches its particle systems + lifetime,
// warms the play path once, and leaves it stopped/cleared ready for reuse.
private GameObject CreateFxInstance(GameObject template)
{
if (template == null)
return null;
GameObject instance = Instantiate(template);
var systems = instance.GetComponentsInChildren<ParticleSystem>(true);
if (systems == null)
systems = new ParticleSystem[0];
var entry = new FxCacheEntry
{
Systems = systems,
Lifetime = ComputeFxLifetime(systems)
};
foreach (var ps in systems)
{
if (ps == null) continue;
try { ps.Play(true); } catch { }
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
}
fxCache[instance] = entry;
fxInstanceTemplate[instance] = template;
return instance;
}
private GameObject RentFxInstance(GameObject template, out FxCacheEntry entry, out int rentToken)
{
entry = null;
rentToken = -1;
if (template == null)
return null;
if (fxPools.TryGetValue(template, out var pool))
{
while (pool.Count > 0)
{
GameObject pooled = pool.Dequeue();
if (pooled != null && fxCache.TryGetValue(pooled, out entry))
{
rentToken = MarkRented(pooled);
return pooled;
}
}
}
GameObject created = CreateFxInstance(template);
if (created != null)
{
fxCache.TryGetValue(created, out entry);
rentToken = MarkRented(created);
}
return created;
}
private int CurrentRentToken(GameObject instance)
{
return fxRentId.TryGetValue(instance, out int token) ? token : -1;
}
private int MarkRented(GameObject instance)
{
rentedFx.Add(instance);
int token = ++fxRentCounter;
fxRentId[instance] = token;
return token;
}
private void ReturnFxInstance(GameObject instance)
{
if (instance == null)
return;
// Idempotent: if this instance was already returned, ignore the second call.
if (!rentedFx.Remove(instance))
return;
fxRentId.Remove(instance);
activeParticles.Remove(instance);
if (fxCache.TryGetValue(instance, out var entry) && entry.Systems != null)
{
foreach (var ps in entry.Systems)
{
if (ps == null) continue;
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
try { ps.Clear(true); } catch { }
}
}
instance.SetActive(false);
EnsureFxPoolRoot();
instance.transform.SetParent(fxPoolRoot, false);
if (!fxInstanceTemplate.TryGetValue(instance, out var template) || template == null)
{
// Unknown origin (should not happen for pooled instances): drop it.
Destroy(instance);
return;
}
if (!fxPools.TryGetValue(template, out var pool))
{
pool = new Queue<GameObject>();
fxPools[template] = pool;
}
pool.Enqueue(instance);
}
private IEnumerator RecycleFxRoutine(GameObject instance, int rentToken, float delay)
{
yield return new WaitForSeconds(delay);
if (instance == null)
yield break;
// Only recycle if this instance is still on the same rent. If it was
// returned early (StopHoldParticles) and re-rented in the meantime, its
// token changed and this stale timer must not touch it.
if (CurrentRentToken(instance) != rentToken)
yield break;
ReturnFxInstance(instance);
}
private static float ComputeFxLifetime(ParticleSystem[] systems)
{
float maxLifetime = 0f;
if (systems == null)
return maxLifetime;
foreach (var ps in systems)
{
if (ps == null) continue;
var main = ps.main;
var lifetime = main.startLifetime;
float life = lifetime.constantMax;
if (life <= 0f) life = lifetime.constant;
if (life > maxLifetime) maxLifetime = life;
}
return maxLifetime;
}
[Header("Inspector")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
@@ -107,12 +296,16 @@ public class AnimationController : MonoBehaviour
holdParticleCoroutine = null;
}
// Immediately destroy all active particle instances
foreach (var p in activeParticles)
// Immediately return all active pooled instances (was Destroy). Returning is
// idempotent and token-guarded, so a later timed RecycleFxRoutine for the same
// instance becomes a no-op. Iterate a snapshot because ReturnFxInstance removes
// the instance from activeParticles (mutating the list mid-enumeration would throw).
var snapshot = activeParticles.ToArray();
foreach (var p in snapshot)
{
if (p != null)
{
Destroy(p);
ReturnFxInstance(p);
}
}
activeParticles.Clear();
@@ -235,8 +428,8 @@ public class AnimationController : MonoBehaviour
}
SpawnJudgePrefabByTrackText(color);
// Instantiate a copy of the scene object
GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity);
// Rent a pooled copy of the scene particle source (was Instantiate + Destroy per hit).
GameObject particleInstance = RentFxInstance(source, out var particleEntry, out int particleToken);
if (particleInstance == null)
{
Debug.LogError("Failed to instantiate hit_particular_object");
@@ -244,104 +437,67 @@ public class AnimationController : MonoBehaviour
return;
}
// Place at spawn position, then optionally parent under the effect object so it
// moves with UI/slot (worldPositionStays=true keeps world pos at spawnPos).
particleInstance.transform.SetParent(parentTransform, false);
particleInstance.transform.position = spawnPos;
particleInstance.transform.rotation = Quaternion.identity;
particleInstance.SetActive(true);
// Find all ParticleSystems on the instance (root + children) and set their startColor
var systems = particleInstance.GetComponentsInChildren<ParticleSystem>(true);
var systems = particleEntry != null ? particleEntry.Systems : null;
if (systems != null && systems.Length > 0)
{
float maxLifetime = 0f;
foreach (var ps in systems)
{
if (ps == null) continue;
var main = ps.main;
// Set start color (works for most setups)
main.startColor = trackColor;
// Play the system
ps.Play();
// determine lifetime (use constantMax for safety)
var lifetime = main.startLifetime;
float life = lifetime.constantMax;
if (life <= 0f) life = lifetime.constant; // fallback
if (life > maxLifetime) maxLifetime = life;
}
// Optionally parent the instance under the effect object so it moves with UI/slot
if (parentTransform != null)
{
particleInstance.transform.SetParent(parentTransform, true);
}
// Recycle after cached lifetime + small buffer (token-guarded).
StartCoroutine(RecycleFxRoutine(particleInstance, particleToken, Mathf.Max(0.5f, particleEntry.Lifetime + 0.1f)));
// Destroy after max lifetime + small buffer
Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f));
// Track for immediate cleanup
// Track for immediate cleanup on hold stop.
activeParticles.Add(particleInstance);
}
else
{
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
Destroy(particleInstance);
ReturnFxInstance(particleInstance);
TriggerAnimatorEffect(color);
}
// NEW: instantiate and play ring effect (overlay) if assigned
// Ring effect (overlay) if assigned.
if (hit_ring_object != null)
{
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
GameObject ringInstance = RentFxInstance(hit_ring_object, out var ringEntry, out int ringToken);
if (ringInstance != null)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
// Parent before activation to avoid transform surprises when Simulation Space = Local
if (parentTransform != null)
{
ringInstance.transform.SetParent(parentTransform, false);
// keep world position at spawnPos
ringInstance.transform.position = spawnPos;
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
}
// Activate after parenting
// Parent before activation to avoid transform surprises when Simulation Space = Local.
ringInstance.transform.SetParent(parentTransform, false);
ringInstance.transform.position = spawnPos;
ringInstance.transform.rotation = Quaternion.identity;
ringInstance.SetActive(true);
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
if (JudgeManager.IsDebugEnabled) Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
var ringSystems = ringEntry != null ? ringEntry.Systems : null;
if (ringSystems != null && ringSystems.Length > 0)
{
float ringMaxLife = 0f;
foreach (var rps in ringSystems)
{
if (rps == null) continue;
var rmain = rps.main;
// log important runtime properties for debugging
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
// reuse same track color
rmain.startColor = trackColor;
rps.Play();
var rlifetime = rmain.startLifetime;
float rlife = rlifetime.constantMax;
if (rlife <= 0f) rlife = rlifetime.constant;
if (rlife > ringMaxLife) ringMaxLife = rlife;
// Also log emission rate if available
try
{
var emission = rps.emission;
var rate = emission.rateOverTime;
if (JudgeManager.IsDebugEnabled) Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
}
catch { }
}
Destroy(ringInstance, Mathf.Max(0.5f, ringMaxLife + 0.1f));
// Track for immediate cleanup
StartCoroutine(RecycleFxRoutine(ringInstance, ringToken, Mathf.Max(0.5f, ringEntry.Lifetime + 0.1f)));
activeParticles.Add(ringInstance);
}
else
{
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("hit_ring_object has no ParticleSystem components");
Destroy(ringInstance);
ReturnFxInstance(ringInstance);
}
}
else
@@ -452,11 +608,18 @@ public class AnimationController : MonoBehaviour
if (prefabToUse != null)
{
// Documentation text normalized.
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
// Rent a pooled copy of the judge-text prefab (was Instantiate + Destroy per hit).
GameObject instance = RentFxInstance(prefabToUse, out _, out int judgeToken);
if (instance == null)
return;
// Documentation text normalized.
Destroy(instance, 1.0f);
instance.transform.SetParent(null, false);
instance.transform.position = spawnTransform.position;
instance.transform.rotation = Quaternion.identity;
instance.SetActive(true);
// Same 1.0s visible lifetime as before, token-guarded recycle instead of Destroy.
StartCoroutine(RecycleFxRoutine(instance, judgeToken, 1.0f));
}
}
@@ -473,50 +636,45 @@ public class AnimationController : MonoBehaviour
private IEnumerator PrewarmCoroutine(int perTemplate)
{
// List of templates to warm
// Templates that flow through PlayDestroyAnimation on every judge. Seeding the
// FX pool here (instead of instantiate-then-destroy) means the first real hits
// rent a ready instance rather than paying Instantiate + first-Play on the hot
// path. CreateFxInstance already warms each instance's play path once.
var templates = new List<GameObject>();
if (hit_particular_object != null) templates.Add(hit_particular_object);
if (hit_ring_object != null) templates.Add(hit_ring_object);
if (perfect_judge_prefab != null) templates.Add(perfect_judge_prefab);
if (great_judge_prefab != null) templates.Add(great_judge_prefab);
if (good_judge_prefab != null) templates.Add(good_judge_prefab);
if (miss_judge_prefab != null) templates.Add(miss_judge_prefab);
EnsureFxPoolRoot();
for (int i = 0; i < templates.Count; i++)
{
var prefab = templates[i];
var template = templates[i];
for (int j = 0; j < perTemplate; j++)
{
GameObject inst = null;
try
{
inst = Instantiate(prefab, this.transform);
inst.SetActive(true);
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
if (systems != null)
inst = CreateFxInstance(template);
if (inst != null)
{
foreach (var ps in systems)
inst.SetActive(false);
inst.transform.SetParent(fxPoolRoot, false);
if (!fxPools.TryGetValue(template, out var pool))
{
try { ps.Play(); } catch { }
pool = new Queue<GameObject>();
fxPools[template] = pool;
}
pool.Enqueue(inst);
}
}
catch { }
// wait a frame to allow any internal initialization to run
yield return null;
// stop and destroy the instance to free memory - the warmup work is done
if (inst != null)
{
var systems = inst.GetComponentsInChildren<ParticleSystem>(true);
if (systems != null)
{
foreach (var ps in systems)
{
try { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } catch { }
}
}
Destroy(inst);
}
// small yield to spread work across frames
// spread work across frames to avoid a long load frame
yield return null;
}
}
@@ -504,7 +504,11 @@ public class GameManager : MonoBehaviour
var judgeFx = Animation_GenerateJudgementSituationPrefab.Instance
?? SceneObjectLookupCache.FindAny<Animation_GenerateJudgementSituationPrefab>();
judgeFx?.PrewarmJudgePrefabs(1);
judgeFx?.PrewarmJudgePrefabs(5);
var trackHitFx = TrackJudgeHitEffectController.Instance
?? SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
trackHitFx?.PrewarmTrackParticles(10);
var fxEvent = effectEventController.Instance ?? SceneObjectLookupCache.FindAny<effectEventController>();
if (fxEvent != null && fxEvent.isActiveAndEnabled)
+41 -11
View File
@@ -43,6 +43,20 @@ public class HoldNote : BaseNote
// whether this middle segment was held from the start (used for logic checks)
private bool hasBeenHeldFromStart = false;
// Previous-frame held state for this track, used to derive key-down / key-up edges
// from the platform-agnostic InputManager.IsTrackHeld table instead of polling
// Input.GetKeyDown/Up directly (which touch cannot drive on Android).
private bool prevTrackHeld = false;
// Query the shared held-state table (keyboard OR touch). Falls back to legacy
// Input.GetKey if the InputManager is somehow absent so editor/standalone still works.
private bool IsHeld()
{
var im = InputManager.Instance;
if (im != null) return im.IsTrackHeld(trackIndex);
return Input.GetKey(keyToPress);
}
[Header("Judge configuration")]
public NoteJudgeConfig judgeConfig; // judge windows configuration
private NoteData noteData;
@@ -56,9 +70,9 @@ public class HoldNote : BaseNote
private List<UnityEngine.UI.Graphic> cachedUIComponents = new List<UnityEngine.UI.Graphic>();
[Header("Hold judgement adjustments")]
[Tooltip("Multiplier applied to judgement windows for hold notes. >1 makes hold judgement more lenient (wider windows).")]
[Range(1f, 2f)]
public float holdWindowMultiplier = 1.3f;
[Tooltip("Multiplier applied to judgement windows for hold notes. Lower values make hold judgement stricter.")]
[Range(0.1f, 2f)]
public float holdWindowMultiplier = 0.8f;
public float visualSpeedMultiplier = 1f; // injected from NoteSpawner to adapt windows when visual speed changes
@@ -434,6 +448,16 @@ public class HoldNote : BaseNote
bool debugEnabled = JudgeManager.IsDebugEnabled;
float now = Time.time;
// Sample held state and derive key-down/up edges at the very top, BEFORE any
// early return. Unity's Input.GetKeyDown/Up are global per-frame edges that
// don't depend on whether we polled last frame; updating prevTrackHeld every
// frame reproduces that. If we only sampled after the early exits below, a key
// already held when the note enters range would produce a false keyDown.
bool keyHeld = IsHeld();
bool keyDown = keyHeld && !prevTrackHeld;
bool keyUp = !keyHeld && prevTrackHeld;
prevTrackHeld = keyHeld;
// Optimization: Early exit if the note is far from judgment line and hasn't entered yet
if (!hasEnteredLine)
{
@@ -448,10 +472,6 @@ public class HoldNote : BaseNote
return;
}
bool keyHeld = Input.GetKey(keyToPress);
bool keyDown = Input.GetKeyDown(keyToPress);
bool keyUp = Input.GetKeyUp(keyToPress);
// If start was judged and player is holding key, start hold effects
if (!isHoldActive && jm != null && jm.IsStartJudged(noteID) && !jm.HasNoteReleased(noteID) && keyHeld)
{
@@ -694,8 +714,8 @@ public class HoldNote : BaseNote
}
else if (segment == NoteSegment.Middle)
{
hasBeenHeldFromStart = Input.GetKey(keyToPress);
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
hasBeenHeldFromStart = IsHeld();
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={IsHeld()}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
@@ -716,7 +736,7 @@ public class HoldNote : BaseNote
// If the key is still being held down when end segment enters judge zone, immediately judge.
// Autoplay must ignore physical input to guarantee Perfect at scheduledEndTime.
if (!GameConfig.autoPlayEnabled && Input.GetKey(keyToPress) && JudgeManager.Instance.IsStartJudged(noteID) && !isJudged)
if (!GameConfig.autoPlayEnabled && IsHeld() && JudgeManager.Instance.IsStartJudged(noteID) && !isJudged)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] End entered while key still held: {noteColor}, forcing Perfect");
// Record release time as current time (player is still holding)
@@ -980,6 +1000,11 @@ public class HoldNote : BaseNote
JudgeSoundManager.Instance?.PlayJudgeSound(result);
}
if (result != "Miss")
{
TrackJudgeHitEffectController.PlayTrackHitFx(trackIndex);
}
// Do not spawn the judgement animation prefab for Start (head) presses to avoid duplicated prefabs
if (segment != NoteSegment.Start)
{
@@ -1152,6 +1177,11 @@ public class HoldNote : BaseNote
teamUIController.Instance?.OnJudgeResult(result);
}
if (result != "Miss")
{
TrackJudgeHitEffectController.PlayTrackHitFx(trackIndex);
}
// Ensure END always spawns judgement prefab (including Miss)
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
@@ -1295,7 +1325,7 @@ public class HoldNote : BaseNote
// record release time and register release in JudgeManager
if (!hasReleased)
{
if (Input.GetKey(keyToPress))
if (IsHeld())
{
releaseTime = scheduledEndTime;
}
@@ -3,6 +3,10 @@ using TMPro;
using System;
using UnityEngine.UI;
// Runs before default-order scripts so the per-track held-state table (trackHeld) is
// updated from keyboard/touch this frame *before* hold notes read it. This preserves the
// frame-accurate timing the old direct Input.GetKey polling had in HoldNote.
[DefaultExecutionOrder(-100)]
public class InputManager : MonoBehaviour
{
public static InputManager Instance { get; private set; }
@@ -42,6 +46,21 @@ public class InputManager : MonoBehaviour
private KeyCode[] cachedKeys = new KeyCode[5];
private bool pauseBlockedLastFrame = false;
// Per-track held state. Written by both keyboard (Update) and touch (PressTrack/
// ReleaseTrack) so hold-note logic can query one platform-agnostic source instead
// of polling Input.GetKey directly (which cannot be driven by touch on Android).
private bool[] trackHeld = new bool[5];
/// <summary>
/// True while the given track (0-4) is currently held, regardless of whether the
/// source is keyboard or touch. Replaces direct Input.GetKey polling in hold notes.
/// </summary>
public bool IsTrackHeld(int trackIndex)
{
if (trackIndex < 0 || trackIndex >= trackHeld.Length) return false;
return trackHeld[trackIndex];
}
private void Awake()
{
if (Instance == null)
@@ -111,45 +130,61 @@ public class InputManager : MonoBehaviour
KeyCode key = cachedKeys[i];
if (key == KeyCode.None) continue;
int index = i; // TrackColors array index matches track index logic here
if (Input.GetKeyDown(key))
{
OnKeyPressed?.Invoke(key);
// Documentation text normalized.
// Update lane sprite alpha
if (laneSprites != null && index >= 0 && index < laneSprites.Length)
{
SetSpriteAlpha(laneSprites[index], pressedAlpha);
}
// Documentation text normalized.
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyActiveColor;
}
}
PressTrack(i);
if (Input.GetKeyUp(key))
{
OnKeyReleased?.Invoke(key);
ReleaseTrack(i);
}
}
// Update lane sprite alpha
if (laneSprites != null && index >= 0 && index < laneSprites.Length)
{
SetSpriteAlpha(laneSprites[index], 0f);
}
/// <summary>
/// Begin holding a track (0-4). Called by keyboard Update on key-down and by touch
/// regions on pointer-down. Sets the held-state table, raises OnKeyPressed with the
/// track's bound key so event-driven tap notes keep working, and updates lane visuals.
/// </summary>
public void PressTrack(int index)
{
if (index < 0 || index >= TrackColors.Length) return;
if (trackHeld[index]) return; // already held; avoid duplicate press events
// Documentation text normalized.
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
txt.color = keyInactiveColor;
}
}
trackHeld[index] = true;
KeyCode key = cachedKeys[index];
if (key != KeyCode.None)
OnKeyPressed?.Invoke(key);
if (laneSprites != null && index < laneSprites.Length)
SetSpriteAlpha(laneSprites[index], pressedAlpha);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyActiveColor;
}
}
/// <summary>
/// Release a track (0-4). Called by keyboard Update on key-up and by touch regions
/// on pointer-up. Clears the held-state table, raises OnKeyReleased, and resets visuals.
/// </summary>
public void ReleaseTrack(int index)
{
if (index < 0 || index >= TrackColors.Length) return;
if (!trackHeld[index]) return; // not held; nothing to release
trackHeld[index] = false;
KeyCode key = cachedKeys[index];
if (key != KeyCode.None)
OnKeyReleased?.Invoke(key);
if (laneSprites != null && index < laneSprites.Length)
SetSpriteAlpha(laneSprites[index], 0f);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyInactiveColor;
}
}
@@ -169,6 +204,7 @@ public class InputManager : MonoBehaviour
for (int i = 0; i < TrackColors.Length; i++)
{
string color = TrackColors[i];
trackHeld[i] = false; // clear held state so hold notes don't see a stale press after pause/clear
KeyCode key = KeyBindingManager.GetKeyForColor(color);
if (key == KeyCode.None) continue;
try { OnKeyReleased?.Invoke(key); } catch { }
@@ -15,4 +15,4 @@ MonoBehaviour:
perfectRange: 0.1
greatRange: 0.15
goodRange: 0.2
missRange: 0.25
missRange: 0.2
+20 -4
View File
@@ -159,7 +159,11 @@ public class Note : BaseNote
private void Awake()
{
anim = GetComponent<AnimationController>();
anim = AnimationController.Global;
if (anim == null)
{
anim = GetComponent<AnimationController>();
}
controller = GetComponent<NoteController>();
}
@@ -298,6 +302,11 @@ public class Note : BaseNote
effectEventController.TryTriggerMultiNoteShake();
}
if (judgeResult != "Miss")
{
TrackJudgeHitEffectController.PlayTrackHitFx(TrackIndex);
}
globalNoteEffect.TryTrigger(noteData != null ? noteData.noteFunction : null);
Judge();
@@ -464,6 +473,11 @@ public class Note : BaseNote
effectEventController.TryTriggerMultiNoteShake();
}
if (judgeResult != "Miss")
{
TrackJudgeHitEffectController.PlayTrackHitFx(TrackIndex);
}
globalNoteEffect.TryTrigger(noteData != null ? noteData.noteFunction : null);
}
@@ -498,12 +512,14 @@ public class Note : BaseNote
hasLock = false;
}
ReturnToPool();
if (anim != null)
var animationController = AnimationController.Global ?? anim;
if (animationController != null)
{
anim.PlayDestroyAnimation(noteColor);
animationController.PlayDestroyAnimation(noteColor);
}
ReturnToPool();
// notify global judge manager that this short note has been finally judged (hit)
JudgeManager.Instance?.NotifyNoteJudged();
}
@@ -14,5 +14,5 @@ public class NoteJudgeConfig : ScriptableObject
[Tooltip("Good")]
public float goodRange = 0.3f;
[Tooltip("Miss")]
public float missRange = 0.5f;
public float missRange = 0.2f;
}
+9 -8
View File
@@ -115,7 +115,7 @@ public class NotePool : MonoBehaviour
var anim = AnimationController.Global ?? SceneObjectLookupCache.FindAny<AnimationController>();
if (anim != null)
{
anim.PrewarmParticles(2);
anim.PrewarmParticles(6);
if (verboseLogging) Debug.Log("NotePool: requested AnimationController prewarm");
}
}
@@ -192,6 +192,7 @@ public class NotePool : MonoBehaviour
if (pool.Count > 0)
{
GameObject obj = pool.Pop();
GamePlay.PoolItem pi = null;
if (obj == null)
{
// Documentation text normalized.
@@ -199,26 +200,26 @@ public class NotePool : MonoBehaviour
else
{
// Documentation text normalized.
GamePlay.PoolItem pi = obj.GetComponent<GamePlay.PoolItem>();
pi = obj.GetComponent<GamePlay.PoolItem>();
if (pi == null || prefab == null || pi.prefabName != prefab.name)
{
// Documentation text normalized.
if (pi == null && verboseLogging) Debug.LogWarning("PoolItem missing on pooled object, replacing.");
Destroy(obj);
obj = InstantiateAndPrepare(prefab);
pi = obj != null ? obj.GetComponent<GamePlay.PoolItem>() : null;
}
}
obj.transform.SetParent(null);
obj.SetActive(true);
// mark as taken from pool
GamePlay.PoolItem takenPi = obj.GetComponent<GamePlay.PoolItem>();
if (takenPi != null)
// mark as taken from pool (reuse the PoolItem already resolved above)
if (pi != null)
{
takenPi.inPool = false;
if (takenPi.initialLocalScaleCaptured)
pi.inPool = false;
if (pi.initialLocalScaleCaptured)
{
obj.transform.localScale = takenPi.initialLocalScale;
obj.transform.localScale = pi.initialLocalScale;
}
}
@@ -100,6 +100,7 @@ public class NoteSpawner : MonoBehaviour
private Dictionary<int, int> noteIndexToHoldId = new Dictionary<int, int>();
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private const float NoteSpeedDefault = 1f;
private const float BaseTravelDistance = 10.75f;
private Coroutine spawnCoroutine;
@@ -118,13 +119,25 @@ public class NoteSpawner : MonoBehaviour
private void Awake()
{
EnsureDefaultNoteSpeedPreference();
// Load saved visual speed multiplier before any spawning logic uses it
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, speedMultiplier);
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
saved = Mathf.Clamp(saved, SpeedMultiplierMin, SpeedMultiplierMax);
speedMultiplier = saved;
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs");
}
private static void EnsureDefaultNoteSpeedPreference()
{
if (PlayerPrefs.HasKey(NoteSpeedPrefKey))
{
return;
}
PlayerPrefs.SetFloat(NoteSpeedPrefKey, NoteSpeedDefault);
PlayerPrefs.Save();
}
private void OnEnable()
{
// Documentation text normalized.
@@ -429,7 +442,7 @@ public class NoteSpawner : MonoBehaviour
}
int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval);
if (segmentCount < 1) segmentCount = 1; // Documentation text normalized.
if (segmentCount < 2) segmentCount = 2; // Force at least one middle segment between start and end.
float actualSegmentInterval = segmentCount > 0 ? noteData.length / segmentCount : segmentInterval;
Transform spawnPoint = spawnPoints[noteData.trackIndex];
@@ -0,0 +1,996 @@
using System;
using System.Collections;
using System.Collections.Generic;
using KD.Destro2D;
using UnityEngine;
public class TrackCrashController : MonoBehaviour
{
public static TrackCrashController Instance { get; private set; }
public enum TrackCrashShape
{
Auto,
Radial,
RectCut
}
[System.Serializable]
public class TrackEntry
{
public int trackIndex;
public GameObject trackObject;
public bool hideOriginalWhenCrashed = true;
public float restoreDelay = 0f;
}
[Header("Track Crash")]
[SerializeField] private List<TrackEntry> tracks = new List<TrackEntry>(5);
[SerializeField] private bool useDestro2D = true;
[SerializeField] private bool autoRestoreOnTrackCrash = false;
[SerializeField] private float defaultRestoreDelay = 0f;
[SerializeField] private bool enableDebugKeyTrigger = true;
[SerializeField] private bool debugLogs = true;
[SerializeField] private bool useAutoFractureSize = true;
[SerializeField] private float fractureCoreRadius = 0.05f;
[SerializeField] private float fractureOuterRadius = 0.45f;
[SerializeField] private float fractureOuterRadiusScale = 1.1f;
[SerializeField] private float fractureNoiseScale = 10f;
[SerializeField] private float fractureThickness = 0.12f;
[SerializeField] private int fractureLines = 8;
[SerializeField] private TrackCrashShape trackCrashShape = TrackCrashShape.Auto;
[SerializeField] private float rectCutOversize = 1.15f;
[SerializeField] private float chunkDetectDelay = 0.08f;
[SerializeField] private int rectCutCount = 3;
[SerializeField] private float rectCutBandScale = 0.05f;
[SerializeField] private float rectCutPositionSpan = 0.65f;
[SerializeField] private int rectCrossCutCount = 1;
[SerializeField] private float rectCrossCutBandScale = 0.22f;
[SerializeField] private float chunkExplodeForce = 2.4f;
[SerializeField] private float chunkExplodeTorque = 30f;
[SerializeField] private Vector2 chunkWindDirection = Vector2.zero;
[SerializeField] private float chunkWindForce = 0f;
[SerializeField] private Vector2 chunkGravityDirection = Vector2.down;
[SerializeField] private bool keepDestro2DBaseTrackVisible = true;
[SerializeField] private bool keepChunksInPlace = true;
[SerializeField] private bool disableChunkGravity = true;
[SerializeField] private bool autoTuneSplitHandler = true;
[SerializeField] private int splitHandlerMaxChunkCount = 12;
[SerializeField] private int splitHandlerMinCount = 3;
[SerializeField] private bool manualChunkMotion = true;
[SerializeField] private float chunkGravityStrength = 6f;
[SerializeField] private float chunkLinearDamping = 4f;
[SerializeField] private float chunkAngularDamping = 5f;
[SerializeField] private float chunkMotionDuration = 0.45f;
[SerializeField] private bool useRuntimeSpriteFallback = true;
[SerializeField] private int runtimeFallbackColumns = 3;
[SerializeField] private int runtimeFallbackRows = 12;
[SerializeField] private int runtimeFallbackMinVisiblePieces = 6;
[SerializeField] private float runtimeFallbackRandomOffset = 0.015f;
private readonly Dictionary<int, TrackEntry> trackMap = new Dictionary<int, TrackEntry>();
private readonly HashSet<int> crashedTracks = new HashSet<int>();
private readonly HashSet<int> processingTracks = new HashSet<int>();
private readonly Dictionary<int, Coroutine> restoreRoutines = new Dictionary<int, Coroutine>();
private readonly Dictionary<int, GameObject> fractureProxies = new Dictionary<int, GameObject>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Destroy(gameObject);
return;
}
RebuildCache();
}
private void OnValidate()
{
if (!Application.isPlaying)
return;
RebuildCache();
}
private void Update()
{
if (!enableDebugKeyTrigger)
return;
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 1 pressed.");
TriggerTrackCrash(0);
}
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 2 pressed.");
TriggerTrackCrash(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 3 pressed.");
TriggerTrackCrash(2);
}
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 4 pressed.");
TriggerTrackCrash(3);
}
else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 5 pressed.");
TriggerTrackCrash(4);
}
}
private void RebuildCache()
{
trackMap.Clear();
for (int i = 0; i < tracks.Count; i++)
{
TrackEntry entry = tracks[i];
if (entry == null || entry.trackObject == null)
continue;
trackMap[entry.trackIndex] = entry;
}
}
public static void TrackCrash(int trackIndex)
{
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
if (controller == null)
return;
controller.TriggerTrackCrash(trackIndex);
}
public static void RestoreTrack(int trackIndex)
{
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
if (controller == null)
return;
controller.RestoreTrackInternal(trackIndex);
}
public void TriggerTrackCrash(int trackIndex)
{
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
{
if (debugLogs) Debug.LogWarning($"[TrackCrash] Missing track entry or track object for index {trackIndex}.");
return;
}
if (crashedTracks.Contains(trackIndex))
{
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already crashed.");
return;
}
if (processingTracks.Contains(trackIndex))
{
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already processing.");
return;
}
StartCoroutine(TriggerTrackCrashRoutine(trackIndex, entry));
}
private IEnumerator TriggerTrackCrashRoutine(int trackIndex, TrackEntry entry)
{
processingTracks.Add(trackIndex);
try
{
DestroyFractureProxy(trackIndex);
bool fractureSucceeded = !useDestro2D;
SplitHandler splitHandler = null;
int chunkCountBefore = -1;
int chunkCountAfter = -1;
float outerRadius = fractureOuterRadius;
bool waitForChunkDetection = false;
bool usedDestro2D = false;
bool usedRuntimeSpriteFallback = false;
Vector2 fractureCenter = entry.trackObject.transform.position;
Destro2DMain resolvedDestro = null;
SpriteRenderer resolvedSpriteRenderer = null;
TrackCrashShape resolvedShape = TrackCrashShape.Radial;
GameObject fractureTarget = entry.trackObject;
if (useDestro2D)
{
fractureTarget = CreateFractureProxy(trackIndex, entry);
var destro = fractureTarget != null ? fractureTarget.GetComponent<KD.Destro2D.Destro2DMain>() : null;
if (destro == null)
{
destro = fractureTarget != null ? fractureTarget.GetComponentInChildren<KD.Destro2D.Destro2DMain>(true) : null;
}
if (destro != null)
{
usedDestro2D = true;
resolvedDestro = destro;
if (debugLogs) Debug.Log($"[TrackCrash] Fracturing track {trackIndex} on {fractureTarget.name}.");
try
{
splitHandler = destro.GetComponent<SplitHandler>();
TuneSplitHandler(splitHandler);
chunkCountBefore = splitHandler != null ? splitHandler.splitChunks.Count : -1;
var spriteRenderer = ResolveDestroSpriteRenderer(destro, fractureTarget);
resolvedSpriteRenderer = spriteRenderer;
resolvedShape = ResolveCrashShape(spriteRenderer);
if (spriteRenderer != null)
{
fractureCenter = spriteRenderer.bounds.center;
if (useAutoFractureSize)
{
float extent = Mathf.Max(spriteRenderer.bounds.extents.x, spriteRenderer.bounds.extents.y);
outerRadius = Mathf.Max(fractureOuterRadius, extent * fractureOuterRadiusScale);
}
}
TriggerDestro2D(destro, spriteRenderer, fractureCenter, outerRadius);
waitForChunkDetection = true;
}
catch (Exception ex)
{
fractureSucceeded = false;
if (debugLogs) Debug.LogError($"[TrackCrash] Fracture failed for track {trackIndex}: {ex}");
}
}
else if (debugLogs)
{
Debug.LogWarning($"[TrackCrash] No Destro2DMain found on track {trackIndex} object {(fractureTarget != null ? fractureTarget.name : "NULL")} or its children.");
}
}
if (waitForChunkDetection)
{
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
if (!fractureSucceeded && resolvedDestro != null && resolvedSpriteRenderer != null && resolvedShape == TrackCrashShape.RectCut)
{
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} primary cut did not split. Applying aggressive fallback cut.");
}
ApplyAggressiveRectCut(resolvedDestro, resolvedSpriteRenderer, fractureCenter);
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
}
if (fractureSucceeded && splitHandler != null)
{
int visibleChunkCount = CountLiveChunks(splitHandler);
if (visibleChunkCount < 2)
{
fractureSucceeded = false;
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} Destro2D only produced {visibleChunkCount} visible chunk(s); switching to runtime sprite fallback.");
}
}
}
if (fractureSucceeded && splitHandler != null)
{
PrepareChunksForFinalState(splitHandler, fractureCenter);
}
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} chunk count before={chunkCountBefore}, after={chunkCountAfter}, success={fractureSucceeded}, outerRadius={outerRadius}.");
}
}
if (!fractureSucceeded && useRuntimeSpriteFallback)
{
DestroyFractureProxy(trackIndex);
fractureTarget = CreateRuntimeSpriteFallbackProxy(trackIndex, entry, out int pieceCount);
fractureSucceeded = fractureTarget != null && pieceCount >= runtimeFallbackMinVisiblePieces;
usedRuntimeSpriteFallback = fractureSucceeded;
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} runtime sprite fallback pieces={pieceCount}, success={fractureSucceeded}.");
}
}
if (!fractureSucceeded)
{
DestroyFractureProxy(trackIndex);
yield break;
}
crashedTracks.Add(trackIndex);
bool shouldHideOriginalObject = entry.hideOriginalWhenCrashed;
if (usedDestro2D)
{
shouldHideOriginalObject = true;
}
if (shouldHideOriginalObject)
{
entry.trackObject.SetActive(false);
}
if (usedDestro2D && !usedRuntimeSpriteFallback && fractureTarget != null)
{
if (keepDestro2DBaseTrackVisible)
{
fractureTarget.SetActive(true);
}
else
{
fractureTarget.SetActive(false);
}
}
if (usedRuntimeSpriteFallback && fractureTarget != null)
{
fractureTarget.SetActive(true);
}
if (autoRestoreOnTrackCrash)
{
float delay = entry.restoreDelay > 0f ? entry.restoreDelay : defaultRestoreDelay;
if (delay > 0f)
{
if (restoreRoutines.TryGetValue(trackIndex, out var routine) && routine != null)
{
StopCoroutine(routine);
}
restoreRoutines[trackIndex] = StartCoroutine(RestoreAfterDelay(trackIndex, delay));
}
}
}
finally
{
processingTracks.Remove(trackIndex);
}
}
private void TriggerDestro2D(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, float outerRadius)
{
if (destro == null)
return;
TrackCrashShape shape = ResolveCrashShape(spriteRenderer);
if (shape == TrackCrashShape.RectCut)
{
ApplyRectCut(destro, spriteRenderer, fractureCenter);
return;
}
destro.DynamicFracture(fractureCenter, fractureCoreRadius, outerRadius, fractureNoiseScale, fractureThickness, fractureLines);
}
private TrackCrashShape ResolveCrashShape(SpriteRenderer spriteRenderer)
{
if (trackCrashShape != TrackCrashShape.Auto)
return trackCrashShape;
if (spriteRenderer == null)
return TrackCrashShape.Radial;
Vector3 size = spriteRenderer.bounds.size;
return size.y > size.x * 2f || size.x > size.y * 2f
? TrackCrashShape.RectCut
: TrackCrashShape.Radial;
}
private void ApplyRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
ApplyRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(1, rectCutCount), Mathf.Clamp01(rectCutPositionSpan), Mathf.Max(0.005f, rectCutBandScale));
ApplyCrossRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(0, rectCrossCutCount), Mathf.Max(0.01f, rectCrossCutBandScale));
}
private void ApplyAggressiveRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
ApplyRectCutInternal(
destro,
spriteRenderer,
fractureCenter,
Mathf.Max(3, rectCutCount + 2),
Mathf.Max(0.75f, Mathf.Clamp01(rectCutPositionSpan)),
Mathf.Max(0.08f, rectCutBandScale));
ApplyCrossRectCutInternal(
destro,
spriteRenderer,
fractureCenter,
Mathf.Max(1, rectCrossCutCount + 1),
Mathf.Max(0.12f, rectCrossCutBandScale));
}
private void ApplyRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float span, float bandScale)
{
if (destro == null || spriteRenderer == null)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
for (int i = 0; i < cutCount; i++)
{
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
float offsetNormalized = Mathf.Lerp(-span, span, t);
Vector2 cutPosition = fractureCenter;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
cutPosition.y += extents.y * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
}
else
{
cutPosition.x += extents.x * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
}
}
private void ApplyLongitudinalCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
if (destro == null || spriteRenderer == null)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
rect.l = Mathf.Max(fractureThickness * 0.6f, extents.x * 0.1f);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
else
{
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness * 0.6f, extents.y * 0.1f);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(fractureCenter, new List<Destruction> { rect });
}
private void ApplyCrossRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float bandScale)
{
if (destro == null || spriteRenderer == null || cutCount <= 0)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
float usableSpan = 0.6f;
for (int i = 0; i < cutCount; i++)
{
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
float offsetNormalized = Mathf.Lerp(-usableSpan, usableSpan, t);
Vector2 cutPosition = fractureCenter;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
cutPosition.x += extents.x * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
else
{
cutPosition.y += extents.y * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
}
}
private void TuneSplitHandler(SplitHandler splitHandler)
{
if (!autoTuneSplitHandler || splitHandler == null)
return;
splitHandler.MaxChunkCount = Mathf.Max(splitHandler.MaxChunkCount, splitHandlerMaxChunkCount);
splitHandler.minCount = Mathf.Max(1, Mathf.Min(splitHandler.minCount, splitHandlerMinCount));
}
private SpriteRenderer ResolveDestroSpriteRenderer(Destro2DMain destro, GameObject fallbackObject)
{
if (destro != null)
{
SpriteRenderer directRenderer = destro.GetComponent<SpriteRenderer>();
if (directRenderer != null)
return directRenderer;
SpriteRenderer childRenderer = destro.GetComponentInChildren<SpriteRenderer>(true);
if (childRenderer != null)
return childRenderer;
}
if (fallbackObject != null)
{
SpriteRenderer entryRenderer = fallbackObject.GetComponent<SpriteRenderer>();
if (entryRenderer != null)
return entryRenderer;
return fallbackObject.GetComponentInChildren<SpriteRenderer>(true);
}
return null;
}
private void PushChunksAway(SplitHandler splitHandler, Vector2 fractureCenter)
{
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
return;
Vector2 normalizedWindDirection = chunkWindDirection.sqrMagnitude > 0.0001f
? chunkWindDirection.normalized
: Vector2.zero;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
if (rb == null)
continue;
Vector2 worldChunkCenter = chunk.transform.position;
if (chunkSplitHandler != null)
{
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
}
Vector2 direction = worldChunkCenter - fractureCenter;
if (direction.sqrMagnitude < 0.0001f)
{
direction = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 forceVector = direction.normalized * chunkExplodeForce;
if (normalizedWindDirection != Vector2.zero && chunkWindForce > 0f)
{
forceVector += normalizedWindDirection * chunkWindForce;
}
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.AddForce(forceVector, ForceMode2D.Impulse);
if (chunkExplodeTorque > 0f)
{
float torqueSign = UnityEngine.Random.value > 0.5f ? 1f : -1f;
rb.AddTorque(chunkExplodeTorque * torqueSign, ForceMode2D.Impulse);
}
}
}
private void PrepareChunksForFinalState(SplitHandler splitHandler, Vector2 fractureCenter)
{
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
return;
if (manualChunkMotion)
{
StartCoroutine(DriveChunksManually(splitHandler, fractureCenter));
return;
}
if (!keepChunksInPlace)
{
PushChunksAway(splitHandler, fractureCenter);
return;
}
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
if (rb == null)
continue;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
if (disableChunkGravity)
{
rb.gravityScale = 0f;
}
rb.constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
}
}
private System.Collections.IEnumerator RestoreAfterDelay(int trackIndex, float delay)
{
yield return new WaitForSecondsRealtime(delay);
RestoreTrackInternal(trackIndex);
restoreRoutines.Remove(trackIndex);
processingTracks.Remove(trackIndex);
}
private void RestoreTrackInternal(int trackIndex)
{
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
return;
DestroyFractureProxy(trackIndex);
if (entry.trackObject != null)
{
entry.trackObject.SetActive(true);
}
crashedTracks.Remove(trackIndex);
}
public void RebuildFromSceneLookup()
{
RebuildCache();
}
private IEnumerator DriveChunksManually(SplitHandler splitHandler, Vector2 fractureCenter)
{
List<Rigidbody2D> rigidbodies = new List<Rigidbody2D>();
List<Vector2> velocities = new List<Vector2>();
List<float> angularVelocities = new List<float>();
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
? Vector2.zero
: chunkGravityDirection.normalized;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
if (rb == null)
continue;
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
Vector2 worldChunkCenter = chunk.transform.position;
if (chunkSplitHandler != null)
{
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
}
Vector2 radialDirection = worldChunkCenter - fractureCenter;
if (radialDirection.sqrMagnitude < 0.0001f)
{
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 velocity = Vector2.zero;
if (!keepChunksInPlace)
{
velocity += radialDirection.normalized * chunkExplodeForce;
velocity += normalizedWind * chunkWindForce;
}
rigidbodies.Add(rb);
velocities.Add(velocity);
angularVelocities.Add(UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
rb.gravityScale = 0f;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
}
float elapsed = 0f;
while (elapsed < chunkMotionDuration)
{
float dt = Time.deltaTime;
for (int i = 0; i < rigidbodies.Count; i++)
{
Rigidbody2D rb = rigidbodies[i];
if (rb == null)
continue;
Vector2 velocity = velocities[i];
float angularVelocity = angularVelocities[i];
velocity += normalizedGravity * chunkGravityStrength * dt;
velocity += normalizedWind * chunkWindForce * dt;
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
rb.position += velocity * dt;
rb.rotation += angularVelocity * dt;
velocities[i] = velocity;
angularVelocities[i] = angularVelocity;
}
elapsed += dt;
yield return null;
}
for (int i = 0; i < rigidbodies.Count; i++)
{
if (rigidbodies[i] == null)
continue;
rigidbodies[i].linearVelocity = Vector2.zero;
rigidbodies[i].angularVelocity = 0f;
if (keepChunksInPlace)
{
rigidbodies[i].constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
}
}
}
private GameObject CreateFractureProxy(int trackIndex, TrackEntry entry)
{
if (entry == null || entry.trackObject == null)
return null;
GameObject proxy = Instantiate(entry.trackObject, entry.trackObject.transform.parent);
proxy.name = entry.trackObject.name + "_FractureProxy";
proxy.transform.position = entry.trackObject.transform.position;
proxy.transform.rotation = entry.trackObject.transform.rotation;
proxy.transform.localScale = entry.trackObject.transform.localScale;
fractureProxies[trackIndex] = proxy;
return proxy;
}
private void DestroyFractureProxy(int trackIndex)
{
if (!fractureProxies.TryGetValue(trackIndex, out GameObject proxy))
return;
fractureProxies.Remove(trackIndex);
if (proxy != null)
{
Destroy(proxy);
}
}
private int CountLiveChunks(SplitHandler splitHandler)
{
if (splitHandler == null || splitHandler.splitChunks == null)
return 0;
int count = 0;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null || !chunk.activeInHierarchy)
continue;
SpriteRenderer spriteRenderer = chunk.GetComponent<SpriteRenderer>();
if (spriteRenderer == null || spriteRenderer.sprite == null)
continue;
count++;
}
return count;
}
private GameObject CreateRuntimeSpriteFallbackProxy(int trackIndex, TrackEntry entry, out int pieceCount)
{
pieceCount = 0;
if (entry == null || entry.trackObject == null)
return null;
SpriteRenderer sourceRenderer = ResolveDestroSpriteRenderer(null, entry.trackObject);
if (sourceRenderer == null || sourceRenderer.sprite == null || sourceRenderer.sprite.texture == null)
return null;
Sprite sourceSprite = sourceRenderer.sprite;
Rect sourceRect = sourceSprite.rect;
if (sourceRect.width < 2f || sourceRect.height < 2f)
return null;
int columns = Mathf.Max(1, runtimeFallbackColumns);
int rows = Mathf.Max(1, runtimeFallbackRows);
float aspect = sourceRect.height / Mathf.Max(1f, sourceRect.width);
if (aspect > 2f)
{
rows = Mathf.Max(rows, Mathf.CeilToInt(aspect * columns * 1.75f));
}
GameObject proxy = new GameObject(entry.trackObject.name + "_RuntimeFractureProxy");
proxy.transform.SetParent(entry.trackObject.transform.parent, false);
proxy.transform.position = sourceRenderer.transform.position;
proxy.transform.rotation = sourceRenderer.transform.rotation;
proxy.transform.localScale = sourceRenderer.transform.lossyScale;
fractureProxies[trackIndex] = proxy;
TrackCrashRuntimeSpriteCleanup cleanup = proxy.AddComponent<TrackCrashRuntimeSpriteCleanup>();
List<Transform> pieceTransforms = new List<Transform>();
List<Vector2> pieceVelocities = new List<Vector2>();
List<float> pieceAngularVelocities = new List<float>();
float pixelsPerUnit = Mathf.Max(1f, sourceSprite.pixelsPerUnit);
float minLocalX = -sourceSprite.pivot.x / pixelsPerUnit;
float minLocalY = -sourceSprite.pivot.y / pixelsPerUnit;
for (int row = 0; row < rows; row++)
{
float normalizedY0 = (float)row / rows;
float normalizedY1 = (float)(row + 1) / rows;
float yMin = sourceRect.y + (sourceRect.height * normalizedY0);
float yMax = sourceRect.y + (sourceRect.height * normalizedY1);
yMin = Mathf.Clamp(yMin, sourceRect.y, sourceRect.yMax - 1f);
yMax = Mathf.Clamp(yMax, yMin + 1f, sourceRect.yMax);
for (int col = 0; col < columns; col++)
{
float normalizedX0 = (float)col / columns;
float normalizedX1 = (float)(col + 1) / columns;
float xMin = sourceRect.x + (sourceRect.width * normalizedX0);
float xMax = sourceRect.x + (sourceRect.width * normalizedX1);
xMin = Mathf.Clamp(xMin, sourceRect.x, sourceRect.xMax - 1f);
xMax = Mathf.Clamp(xMax, xMin + 1f, sourceRect.xMax);
Rect pieceRect = Rect.MinMaxRect(xMin, yMin, xMax, yMax);
if (pieceRect.width < 1f || pieceRect.height < 1f)
continue;
GameObject piece = new GameObject($"Piece_{row}_{col}");
piece.transform.SetParent(proxy.transform, false);
SpriteRenderer pieceRenderer = piece.AddComponent<SpriteRenderer>();
pieceRenderer.sprite = Sprite.Create(
sourceSprite.texture,
pieceRect,
new Vector2(0.5f, 0.5f),
pixelsPerUnit,
0,
SpriteMeshType.FullRect);
cleanup.Register(pieceRenderer.sprite);
pieceRenderer.sharedMaterial = sourceRenderer.sharedMaterial;
pieceRenderer.color = sourceRenderer.color;
pieceRenderer.flipX = sourceRenderer.flipX;
pieceRenderer.flipY = sourceRenderer.flipY;
pieceRenderer.maskInteraction = sourceRenderer.maskInteraction;
pieceRenderer.sortingLayerID = sourceRenderer.sortingLayerID;
pieceRenderer.sortingOrder = sourceRenderer.sortingOrder;
pieceRenderer.renderingLayerMask = sourceRenderer.renderingLayerMask;
piece.layer = sourceRenderer.gameObject.layer;
float localPieceX = (pieceRect.x - sourceRect.x);
float localPieceY = (pieceRect.y - sourceRect.y);
float localCenterX = minLocalX + (localPieceX + pieceRect.width * 0.5f) / pixelsPerUnit;
float localCenterY = minLocalY + (localPieceY + pieceRect.height * 0.5f) / pixelsPerUnit;
Vector2 randomOffset = runtimeFallbackRandomOffset > 0f
? UnityEngine.Random.insideUnitCircle * runtimeFallbackRandomOffset
: Vector2.zero;
piece.transform.localPosition = new Vector3(localCenterX + randomOffset.x, localCenterY + randomOffset.y, 0f);
piece.transform.localRotation = Quaternion.identity;
piece.transform.localScale = Vector3.one;
pieceTransforms.Add(piece.transform);
Vector2 radialDirection = ((Vector2)piece.transform.position - (Vector2)sourceRenderer.bounds.center);
if (radialDirection.sqrMagnitude < 0.0001f)
{
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 initialVelocity = Vector2.zero;
if (!keepChunksInPlace)
{
initialVelocity += radialDirection.normalized * chunkExplodeForce;
if (chunkWindDirection.sqrMagnitude > 0.0001f)
{
initialVelocity += chunkWindDirection.normalized * chunkWindForce;
}
}
pieceVelocities.Add(initialVelocity);
pieceAngularVelocities.Add(keepChunksInPlace ? 0f : UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
pieceCount++;
}
}
if (pieceCount <= 0)
{
Destroy(proxy);
fractureProxies.Remove(trackIndex);
return null;
}
if (manualChunkMotion && !keepChunksInPlace)
{
StartCoroutine(DriveRuntimeSpritePieces(pieceTransforms, pieceVelocities, pieceAngularVelocities));
}
return proxy;
}
private IEnumerator DriveRuntimeSpritePieces(
List<Transform> pieceTransforms,
List<Vector2> pieceVelocities,
List<float> pieceAngularVelocities)
{
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
? Vector2.zero
: chunkGravityDirection.normalized;
float elapsed = 0f;
while (elapsed < chunkMotionDuration)
{
float dt = Time.deltaTime;
for (int i = 0; i < pieceTransforms.Count; i++)
{
Transform pieceTransform = pieceTransforms[i];
if (pieceTransform == null)
continue;
Vector2 velocity = pieceVelocities[i];
float angularVelocity = pieceAngularVelocities[i];
velocity += normalizedGravity * chunkGravityStrength * dt;
velocity += normalizedWind * chunkWindForce * dt;
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
pieceTransform.position += (Vector3)(velocity * dt);
pieceTransform.Rotate(0f, 0f, angularVelocity * dt, Space.Self);
pieceVelocities[i] = velocity;
pieceAngularVelocities[i] = angularVelocity;
}
elapsed += dt;
yield return null;
}
}
}
public sealed class TrackCrashRuntimeSpriteCleanup : MonoBehaviour
{
private readonly List<Sprite> runtimeSprites = new List<Sprite>();
public void Register(Sprite sprite)
{
if (sprite != null)
{
runtimeSprites.Add(sprite);
}
}
private void OnDestroy()
{
for (int i = 0; i < runtimeSprites.Count; i++)
{
if (runtimeSprites[i] != null)
{
Destroy(runtimeSprites[i]);
}
}
runtimeSprites.Clear();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f3a5dd0d9a44f0e95d4a5fb83a0f001
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,443 @@
using System.Collections;
using System.Collections.Generic;
using SpriteGlow;
using UnityEngine;
public class TrackJudgeHitEffectController : MonoBehaviour
{
public static TrackJudgeHitEffectController Instance { get; private set; }
[Header("Track Sprite FX")]
[Tooltip("0-4 maps to track 1-5.")]
[SerializeField] private GameObject[] trackSpriteFxObjects = new GameObject[5];
[Header("Track Glow FX")]
[Tooltip("0-4 maps to track 1-5.")]
[SerializeField] private SpriteGlowEffect[] trackGlowEffects = new SpriteGlowEffect[5];
[SerializeField, Range(0f, 1f)] private float trackGlowSpriteAlpha = 0f;
[Header("Particle FX")]
[SerializeField] private GameObject trackParticlePrefab;
[SerializeField] private Vector3 trackParticleLocalPosition = new Vector3(0f, -5.29f, -0.13f);
[SerializeField] private Vector3 trackParticleLocalEulerAngles = new Vector3(70f, 0f, 0f);
[SerializeField] private Vector3 trackParticleLocalScale = Vector3.one;
[Header("Track Colors")]
[SerializeField] private Color[] trackColors = new Color[5]
{
new Color(1f, 0.44f, 0.42f, 1f),
new Color(0.22f, 0.96f, 0.79f, 1f),
new Color(1f, 0.88f, 0.63f, 1f),
new Color(0.95f, 0.51f, 0.89f, 1f),
new Color(0.48f, 0.98f, 1f, 1f)
};
[Header("Fade")]
[SerializeField, Range(0f, 1f)] private float initialAlpha = 10f / 255f;
[SerializeField] private float fadeDuration = 0.18f;
[SerializeField] private float glowFadeDuration = 0.18f;
[SerializeField] private bool useUnscaledTime = true;
private readonly Dictionary<int, Coroutine> activeFadeRoutines = new Dictionary<int, Coroutine>();
private readonly Dictionary<int, Coroutine> activeGlowFadeRoutines = new Dictionary<int, Coroutine>();
private readonly Queue<GameObject> particlePool = new Queue<GameObject>();
private Transform particlePoolRoot;
// Per-instance cache: the particle systems and computed lifetime are resolved once
// when an instance is created, so the burst hot path never calls
// GetComponentsInChildren (an allocating call) per hit. Business behavior is
// unchanged: same systems, same tint, same lifetime.
private sealed class ParticleCacheEntry
{
public ParticleSystem[] Systems;
public float Lifetime;
}
private readonly Dictionary<GameObject, ParticleCacheEntry> particleCache =
new Dictionary<GameObject, ParticleCacheEntry>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
ResetAllGlowAlpha();
ResetAllGlowSpriteAlpha();
}
else if (Instance != this)
{
Destroy(gameObject);
return;
}
}
public static void PlayTrackHitFx(int trackIndex)
{
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackJudgeHitEffectController>();
if (controller == null)
return;
controller.PlayTrackHitFxInternal(trackIndex);
}
private void PlayTrackHitFxInternal(int trackIndex)
{
if (trackIndex < 0 || trackIndex >= 5)
return;
GameObject trackFxObject = GetTrackFxObject(trackIndex);
if (trackFxObject == null)
return;
if (activeGlowFadeRoutines.TryGetValue(trackIndex, out Coroutine glowRoutine) && glowRoutine != null)
{
StopCoroutine(glowRoutine);
activeGlowFadeRoutines.Remove(trackIndex);
}
if (activeFadeRoutines.TryGetValue(trackIndex, out Coroutine fadeRoutine) && fadeRoutine != null)
{
StopCoroutine(fadeRoutine);
activeFadeRoutines.Remove(trackIndex);
}
trackFxObject.SetActive(true);
PlayGlowFx(trackIndex);
SpriteRenderer spriteRenderer = trackFxObject.GetComponent<SpriteRenderer>();
if (spriteRenderer == null)
{
spriteRenderer = trackFxObject.GetComponentInChildren<SpriteRenderer>(true);
}
if (spriteRenderer != null)
{
Color c = spriteRenderer.color;
c.a = initialAlpha;
spriteRenderer.color = c;
activeFadeRoutines[trackIndex] = StartCoroutine(FadeSpriteRoutine(trackIndex, spriteRenderer));
}
SpawnTrackParticle(trackIndex, trackFxObject.transform);
}
private void ResetAllGlowAlpha()
{
if (trackGlowEffects == null)
return;
for (int i = 0; i < trackGlowEffects.Length; i++)
{
SetGlowAlpha(trackGlowEffects[i], 0f);
}
}
private void ResetAllGlowSpriteAlpha()
{
if (trackGlowEffects == null)
return;
for (int i = 0; i < trackGlowEffects.Length; i++)
{
ResetGlowSpriteAlpha(trackGlowEffects[i]);
}
}
private GameObject GetTrackFxObject(int trackIndex)
{
if (trackSpriteFxObjects == null || trackIndex < 0 || trackIndex >= trackSpriteFxObjects.Length)
return null;
return trackSpriteFxObjects[trackIndex];
}
private SpriteGlowEffect GetTrackGlowEffect(int trackIndex)
{
if (trackGlowEffects == null || trackIndex < 0 || trackIndex >= trackGlowEffects.Length)
return null;
return trackGlowEffects[trackIndex];
}
private void ResetGlowSpriteAlpha(SpriteGlowEffect glowEffect)
{
if (glowEffect == null || glowEffect.Renderer == null)
return;
Color color = glowEffect.Renderer.color;
color.a = trackGlowSpriteAlpha;
glowEffect.Renderer.color = color;
}
private void PlayGlowFx(int trackIndex)
{
SpriteGlowEffect glowEffect = GetTrackGlowEffect(trackIndex);
if (glowEffect == null)
return;
ResetGlowSpriteAlpha(glowEffect);
SetGlowAlpha(glowEffect, 1f);
if (activeGlowFadeRoutines.TryGetValue(trackIndex, out Coroutine routine) && routine != null)
{
StopCoroutine(routine);
activeGlowFadeRoutines.Remove(trackIndex);
}
activeGlowFadeRoutines[trackIndex] = StartCoroutine(FadeGlowRoutine(trackIndex, glowEffect));
}
private void SetGlowAlpha(SpriteGlowEffect glowEffect, float alpha)
{
if (glowEffect == null)
return;
Color color = glowEffect.GlowColor;
color.a = alpha;
glowEffect.GlowColor = color;
}
private IEnumerator FadeSpriteRoutine(int trackIndex, SpriteRenderer spriteRenderer)
{
float duration = Mathf.Max(0.01f, fadeDuration);
float elapsed = 0f;
Color startColor = spriteRenderer != null ? spriteRenderer.color : Color.white;
Color endColor = startColor;
endColor.a = 0f;
while (elapsed < duration)
{
if (spriteRenderer == null)
yield break;
elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
Color c = Color.LerpUnclamped(startColor, endColor, t);
spriteRenderer.color = c;
yield return null;
}
if (spriteRenderer != null)
{
Color c = spriteRenderer.color;
c.a = 0f;
spriteRenderer.color = c;
}
activeFadeRoutines.Remove(trackIndex);
}
private IEnumerator FadeGlowRoutine(int trackIndex, SpriteGlowEffect glowEffect)
{
float duration = Mathf.Max(0.01f, glowFadeDuration);
float elapsed = 0f;
Color startColor = glowEffect != null ? glowEffect.GlowColor : Color.white;
Color endColor = startColor;
endColor.a = 0f;
while (elapsed < duration)
{
if (glowEffect == null)
yield break;
elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
Color c = Color.LerpUnclamped(startColor, endColor, t);
glowEffect.GlowColor = c;
yield return null;
}
if (glowEffect != null)
{
Color c = glowEffect.GlowColor;
c.a = 0f;
glowEffect.GlowColor = c;
}
activeGlowFadeRoutines.Remove(trackIndex);
}
private void SpawnTrackParticle(int trackIndex, Transform parent)
{
if (trackParticlePrefab == null || parent == null)
return;
GameObject instance = RentParticleInstance(out ParticleCacheEntry entry);
if (instance == null)
return;
instance.transform.SetParent(parent, false);
instance.transform.localPosition = trackParticleLocalPosition;
instance.transform.localRotation = Quaternion.Euler(trackParticleLocalEulerAngles);
instance.transform.localScale = trackParticleLocalScale;
instance.SetActive(true);
Color tint = GetTrackColor(trackIndex);
ApplyParticleColor(entry, tint);
StartCoroutine(RecycleParticleRoutine(instance, entry, Mathf.Max(0.05f, entry.Lifetime)));
}
// Prewarm the track particle pool during load so the first judge does not pay
// the Instantiate + first-Play() cost on the hot path. Business behavior is
// unchanged: the same prefab is used, instances are just reused instead of
// being created and destroyed per hit.
public void PrewarmTrackParticles(int count)
{
if (trackParticlePrefab == null || count <= 0)
return;
EnsurePoolRoot();
for (int i = 0; i < count; i++)
{
GameObject instance = CreateParticleInstance();
if (instance == null)
continue;
instance.transform.SetParent(particlePoolRoot, false);
instance.SetActive(false);
particlePool.Enqueue(instance);
}
}
private void EnsurePoolRoot()
{
if (particlePoolRoot == null)
{
var root = new GameObject("TrackParticlePool");
root.transform.SetParent(transform, false);
root.SetActive(false);
particlePoolRoot = root.transform;
}
}
// Central instance factory: instantiates the prefab, resolves and caches its
// particle systems + computed lifetime once, and leaves it stopped/cleared.
private GameObject CreateParticleInstance()
{
if (trackParticlePrefab == null)
return null;
GameObject instance = Instantiate(trackParticlePrefab);
var systems = instance.GetComponentsInChildren<ParticleSystem>(true);
if (systems == null)
systems = new ParticleSystem[0];
var entry = new ParticleCacheEntry
{
Systems = systems,
Lifetime = ComputeLifetime(systems)
};
// Warm the play path once (forces emission buffer allocation and first-Play
// cost during load), then reset to a clean stopped state ready for reuse.
foreach (var ps in systems)
{
if (ps == null) continue;
ps.Play(true);
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
ps.Clear(true);
}
particleCache[instance] = entry;
return instance;
}
private GameObject RentParticleInstance(out ParticleCacheEntry entry)
{
while (particlePool.Count > 0)
{
GameObject pooled = particlePool.Dequeue();
if (pooled != null && particleCache.TryGetValue(pooled, out entry))
return pooled;
}
GameObject created = CreateParticleInstance();
if (created != null && particleCache.TryGetValue(created, out entry))
return created;
entry = null;
return created;
}
private IEnumerator RecycleParticleRoutine(GameObject instance, ParticleCacheEntry entry, float delay)
{
if (useUnscaledTime)
yield return new WaitForSecondsRealtime(delay);
else
yield return new WaitForSeconds(delay);
if (instance == null)
yield break;
if (entry != null && entry.Systems != null)
{
foreach (var ps in entry.Systems)
{
if (ps == null) continue;
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
ps.Clear(true);
}
}
instance.SetActive(false);
EnsurePoolRoot();
instance.transform.SetParent(particlePoolRoot, false);
particlePool.Enqueue(instance);
}
private Color GetTrackColor(int trackIndex)
{
if (trackColors != null && trackIndex >= 0 && trackIndex < trackColors.Length)
{
return trackColors[trackIndex];
}
return Color.white;
}
private void ApplyParticleColor(ParticleCacheEntry entry, Color tint)
{
if (entry == null || entry.Systems == null)
return;
foreach (var ps in entry.Systems)
{
if (ps == null) continue;
var main = ps.main;
main.startColor = tint;
if (!ps.isPlaying)
{
ps.Play(true);
}
}
}
private static float ComputeLifetime(ParticleSystem[] systems)
{
float maxLifetime = 0.5f;
if (systems == null)
return maxLifetime;
foreach (var ps in systems)
{
if (ps == null) continue;
var main = ps.main;
float startLifetime = 0.5f;
if (main.startLifetime.mode == ParticleSystemCurveMode.Constant)
startLifetime = main.startLifetime.constant;
else if (main.startLifetime.mode == ParticleSystemCurveMode.TwoConstants)
startLifetime = main.startLifetime.constantMax;
else
startLifetime = main.startLifetime.constantMax;
maxLifetime = Mathf.Max(maxLifetime, main.duration + startLifetime + 0.25f);
}
return maxLifetime;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ccce61c2f0d4dd5b4f0b7d9f7c3a001
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,61 @@
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// Touch/click region that drives one gameplay track (0-4) through InputManager,
/// exactly as if the bound keyboard key were pressed/released. Attach to a UI
/// element (Image with Raycast Target on) laid over a lane; set trackIndex in the
/// inspector. Uses EventSystem pointer callbacks so it supports multi-touch (each
/// finger routes its own down/up to the region it started on) and works on Android
/// touch as well as desktop mouse.
///
/// Judgment/scoring is unchanged: this only injects the same press/release the
/// keyboard path produces. On a track already held by keyboard, PressTrack/
/// ReleaseTrack are idempotent so there is no double-trigger.
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class TrackTouchRegion : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[Tooltip("Track this region controls: 0=red 1=green 2=yellow 3=purple 4=blue")]
[Range(0, 4)]
public int trackIndex = 0;
// The pointerId that pressed this region, so we only release on the matching
// pointer's up event (relevant for multi-touch where several fingers are down).
private int activePointerId = int.MinValue;
private bool pressed = false;
public void OnPointerDown(PointerEventData eventData)
{
if (pressed) return; // already driven by another finger; ignore extras
var im = InputManager.Instance;
if (im == null) return;
activePointerId = eventData.pointerId;
pressed = true;
im.PressTrack(trackIndex);
}
public void OnPointerUp(PointerEventData eventData)
{
if (!pressed || eventData.pointerId != activePointerId) return;
var im = InputManager.Instance;
pressed = false;
activePointerId = int.MinValue;
if (im != null) im.ReleaseTrack(trackIndex);
}
private void OnDisable()
{
// If the region is hidden/destroyed mid-hold, make sure the track is released
// so a note is not left stuck in the held state.
if (pressed)
{
pressed = false;
activePointerId = int.MinValue;
var im = InputManager.Instance;
if (im != null) im.ReleaseTrack(trackIndex);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b671f741e30e73840abdef6732a1145f
@@ -25,6 +25,9 @@ public class effectEventController : MonoBehaviour
public float playerSkillCameraOffsetAmount = 0.6f;
public CameraOffsetAxis playerSkillCameraOffsetAxis = CameraOffsetAxis.Y;
[Header("Track Crash")]
[SerializeField] private TrackCrashController trackCrashController;
private Transform cachedShakeTarget;
private Transform cachedCameraDriftPivot;
private Transform cachedCameraSkillPivot;
@@ -54,6 +57,7 @@ public class effectEventController : MonoBehaviour
{
CacheShakeTarget();
ResetCameraDriftIfNeeded();
CacheTrackCrashController();
}
private void Update()
@@ -73,6 +77,14 @@ public class effectEventController : MonoBehaviour
}
}
private void CacheTrackCrashController()
{
if (trackCrashController == null)
{
trackCrashController = SceneObjectLookupCache.FindAny<TrackCrashController>();
}
}
private void PrewarmShakeComponent()
{
if (cachedShakeTarget == null)
@@ -265,6 +277,15 @@ public class effectEventController : MonoBehaviour
controller.TriggerMultiNoteShakeInternal();
}
public static void TryTriggerTrackCrash(int trackIndex)
{
effectEventController controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<effectEventController>();
if (controller == null)
return;
controller.TriggerTrackCrashInternal(trackIndex);
}
private void TriggerMultiNoteShakeInternal()
{
if (!enableMultiNoteScreenShake)
@@ -311,4 +332,13 @@ public class effectEventController : MonoBehaviour
shake.sum = new Vector3[2];
shake.StartShake();
}
private void TriggerTrackCrashInternal(int trackIndex)
{
CacheTrackCrashController();
if (trackCrashController == null)
return;
trackCrashController.TriggerTrackCrash(trackIndex);
}
}
@@ -12,6 +12,9 @@ public class GfxController : MonoBehaviour
{
public static GfxController Instance { get; private set; }
// 复用的属性块:命中特效透明度覆写用,避免每次命中访问 r.materials 克隆材质。
private MaterialPropertyBlock _fxPropertyBlock;
[Header("Combatant Objects")]
public GameObject enemyMoveObject;
public GameObject ally01MoveObject;
@@ -688,23 +691,28 @@ public class GfxController : MonoBehaviour
// 设置排序层级
r.sortingOrder = sortingOrder;
// 处理材质透明度
// 处理材质透明度:用 MaterialPropertyBlock 覆写 alpha,避免访问 r.materials
// 触发材质实例化(每次命中都会克隆并泄漏材质,产生 GC)。视觉结果等价。
if (transparency < 1f)
{
foreach (var mat in r.materials)
var sharedMat = r.sharedMaterial;
if (sharedMat != null)
{
if (mat.HasProperty("_Color"))
if (_fxPropertyBlock == null) _fxPropertyBlock = new MaterialPropertyBlock();
r.GetPropertyBlock(_fxPropertyBlock);
if (sharedMat.HasProperty("_Color"))
{
Color c = mat.color;
Color c = sharedMat.color;
c.a *= transparency;
mat.color = c;
_fxPropertyBlock.SetColor("_Color", c);
}
else if (mat.HasProperty("_BaseColor")) // URP 命名
else if (sharedMat.HasProperty("_BaseColor")) // URP 命名
{
Color c = mat.GetColor("_BaseColor");
Color c = sharedMat.GetColor("_BaseColor");
c.a *= transparency;
mat.SetColor("_BaseColor", c);
_fxPropertyBlock.SetColor("_BaseColor", c);
}
r.SetPropertyBlock(_fxPropertyBlock);
}
}
}
@@ -445,6 +445,10 @@ namespace GameServer.Client
{
song_id = CurrentRoom != null ? CurrentRoom.song_id : string.Empty,
cycle_id = 0,
server_name = "雅莉梦璃高新产业园",
leaderboard_cycle_days = 3,
rank_version_id = 20260101,
next_settle_at = "2026-01-01 00:00:00",
rankings = BuildRoomRankingEntries()
};
return result;
@@ -628,10 +632,24 @@ namespace GameServer.Client
}
await EnsureSocialConnected();
await SendSocialAction("SEND_WORLD_MESSAGE", new
JObject resp = await SendSocialAction("SEND_WORLD_MESSAGE", new
{
content = trimmed
});
JObject messageObject = resp["message"] as JObject ?? resp;
ArenaRoomChatMessage message = messageObject.ToObject<ArenaRoomChatMessage>();
if (message == null)
{
return;
}
if (string.IsNullOrWhiteSpace(message.sender_id))
{
message.sender_id = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
}
AppendWorldChatMessage(message);
}
public async Task<List<ArenaRoomChatMessage>> LoadPrivateHistory(string partnerId, int limit, long? beforeId = null)
@@ -62,6 +62,13 @@ public class GameServerBridge : MonoBehaviour
return;
}
if (!LeaderboardConsentUtility.HasLeaderboardConsent())
{
Debug.Log("[Bridge] 玩家未同意加入排行榜,跳过世界排行榜提交");
_hasSubmitted = true;
return;
}
_hasSubmitted = true;
try
@@ -143,6 +150,10 @@ public class GameServerBridge : MonoBehaviour
await Task.Delay(3000);
SubmitCurrentSettlement();
}
else if (result == "OPT_OUT")
{
Debug.Log("[Bridge] 当前已关闭排行榜加入选项,未提交世界排行榜。");
}
else
{
Debug.LogWarning($"╔══════════════════════════════════════════════════════╗");
@@ -105,6 +105,36 @@ namespace GameServer.Client
}
}
public async Task<LeaderboardData> ForceRefresh(string songId)
{
if (string.IsNullOrWhiteSpace(songId))
{
throw new ArgumentException("songId is required", nameof(songId));
}
NetworkManager nm = NetworkManager.Instance;
if (nm == null)
{
throw new InvalidOperationException("NetworkManager is not available.");
}
Invalidate(songId);
Task<LeaderboardData> task = FetchAndCache(songId, nm);
_inflight[songId] = task;
try
{
return await task;
}
finally
{
if (_inflight.TryGetValue(songId, out Task<LeaderboardData> current) && current == task)
{
_inflight.Remove(songId);
}
}
}
public void Invalidate(string songId)
{
if (string.IsNullOrWhiteSpace(songId))
@@ -113,6 +143,7 @@ namespace GameServer.Client
}
_cache.Remove(songId);
_inflight.Remove(songId);
}
private async void HandleSceneLoaded(Scene scene, LoadSceneMode mode)
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
namespace GameServer.Client
{
@@ -88,12 +90,35 @@ namespace GameServer.Client
[JsonProperty("avatar_url")] public string avatar_url;
[JsonProperty("player_level")] public int player_level;
[JsonProperty("leaderboard_opt_out")] public bool leaderboard_opt_out;
[JsonProperty("leaderboard_can_change_today")] public bool leaderboard_can_change_today;
[JsonProperty("leaderboard_changed_today")] public bool leaderboard_changed_today;
[JsonProperty("leaderboard_next_change_at")] public string leaderboard_next_change_at;
[JsonProperty("leaderboard_cycle_id")] public int leaderboard_cycle_id;
[JsonProperty("total_play_seconds")] public double total_play_seconds;
[JsonProperty("total_plays")] public int total_plays;
[JsonProperty("actime")] public string actime;
[JsonProperty("u_reg_id")] public int u_reg_id;
}
[Serializable]
public class LeaderboardMembershipResponse
{
[JsonProperty("success")] public bool success;
[JsonProperty("error_code")] public string error_code;
[JsonProperty("message")] public string message;
[JsonProperty("steam_id")] public string steam_id;
[JsonProperty("cycle_id")] public int cycle_id;
[JsonProperty("is_joined")] public bool is_joined;
[JsonProperty("leaderboard_opt_out")] public bool leaderboard_opt_out;
[JsonProperty("changed_today")] public bool changed_today;
[JsonProperty("can_change_today")] public bool can_change_today;
[JsonProperty("next_change_at")] public string next_change_at;
[JsonProperty("server_name")] public string server_name;
[JsonProperty("leaderboard_cycle_days")] public int leaderboard_cycle_days;
[JsonProperty("rank_version_id")] public int rank_version_id;
[JsonProperty("next_settle_at")] public string next_settle_at;
}
[Serializable]
public class LeaderboardEntry
{
@@ -118,6 +143,10 @@ namespace GameServer.Client
[JsonProperty("song_id")] public string song_id;
[JsonProperty("cycle_id")] public int cycle_id;
[JsonProperty("is_locked")] public bool is_locked;
[JsonProperty("server_name")] public string server_name;
[JsonProperty("leaderboard_cycle_days")] public int leaderboard_cycle_days;
[JsonProperty("rank_version_id")] public int rank_version_id;
[JsonProperty("next_settle_at")] public string next_settle_at;
[JsonProperty("rankings")] public List<LeaderboardEntry> rankings;
}
@@ -499,4 +528,124 @@ namespace GameServer.Client
// itemID 77001 = 角色 3020677003 = 歌曲177004 = 歌曲2(见 Resources/so/storeSO/
}
public static class LeaderboardConsentUtility
{
public const string RankingConsentPrefKey = "before_everything_agree_ranking";
private const string WarningPrefix = "下次排行榜更新时间:";
public static bool HasLeaderboardConsent()
{
return PlayerPrefs.GetInt(RankingConsentPrefKey, 0) == 1;
}
public static void SetLeaderboardConsent(bool accepted)
{
PlayerPrefs.SetInt(RankingConsentPrefKey, accepted ? 1 : 0);
PlayerPrefs.Save();
}
public static async Task<string> BuildNextSettleWarningTextAsync()
{
string nextSettleAt = await TryGetNextSettleAtAsync();
return string.IsNullOrWhiteSpace(nextSettleAt)
? WarningPrefix + "--"
: WarningPrefix + nextSettleAt;
}
public static async Task<string> TryGetNextSettleAtAsync()
{
LeaderboardData data = await TryGetLeaderboardMetaAsync();
return data != null && !string.IsNullOrWhiteSpace(data.next_settle_at)
? data.next_settle_at.Trim()
: string.Empty;
}
public static async Task<LeaderboardData> TryGetLeaderboardMetaAsync()
{
string songId = ResolveReferenceSongId();
if (string.IsNullOrWhiteSpace(songId))
{
return null;
}
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
if (cache != null
&& cache.TryGetCached(songId, out LeaderboardData cached)
&& cached != null
&& !string.IsNullOrWhiteSpace(cached.next_settle_at))
{
return cached;
}
NetworkManager networkManager = NetworkManager.Instance;
if (networkManager == null)
{
return null;
}
try
{
return await networkManager.FetchLeaderboardFromServer(songId);
}
catch (Exception ex)
{
Debug.LogWarning($"[LeaderboardConsentUtility] Failed to fetch leaderboard meta: {ex.Message}");
return null;
}
}
private static string ResolveReferenceSongId()
{
SongData selectedSong = SongDataHolder.SelectedSongData;
if (selectedSong != null && selectedSong.songID > 0)
{
return selectedSong.songID.ToString();
}
if (BeatmapManager.pendingSongData != null && BeatmapManager.pendingSongData.songID > 0)
{
return BeatmapManager.pendingSongData.songID.ToString();
}
BeatmapManager beatmapManager = BeatmapManager.Instance;
if (beatmapManager != null
&& beatmapManager.assignedSongData != null
&& beatmapManager.assignedSongData.songID > 0)
{
return beatmapManager.assignedSongData.songID.ToString();
}
SongDataLibrary library = SongDataLibrary.Instance;
if (library != null && library.IsLoaded)
{
List<SongData> songs = library.GetAllSongs();
if (songs != null)
{
for (int i = 0; i < songs.Count; i++)
{
SongData song = songs[i];
if (song != null && song.songID > 0)
{
return song.songID.ToString();
}
}
}
}
SongData[] indexedSongs = RuntimeResourcesCache.LoadSongIndex();
if (indexedSongs != null)
{
for (int i = 0; i < indexedSongs.Length; i++)
{
SongData song = indexedSongs[i];
if (song != null && song.songID > 0)
{
return song.songID.ToString();
}
}
}
return string.Empty;
}
}
}
@@ -239,6 +239,57 @@ public class NetworkManager : MonoBehaviour
return data;
}
public async Task<LeaderboardMembershipResponse> GetLeaderboardMembership()
{
if (OnlineModeSettings.IsLocalOnlyMode)
{
return new LeaderboardMembershipResponse
{
success = true,
steam_id = SteamId,
is_joined = LeaderboardConsentUtility.HasLeaderboardConsent(),
leaderboard_opt_out = !LeaderboardConsentUtility.HasLeaderboardConsent(),
can_change_today = true,
changed_today = false,
next_change_at = string.Empty,
};
}
string steamIdQuery = Uri.EscapeDataString(SteamId ?? string.Empty);
return await GetJson<LeaderboardMembershipResponse>(
BuildApiUrl($"/api/profile/leaderboard-membership?steam_id={steamIdQuery}"),
CancellationToken.None);
}
public async Task<LeaderboardMembershipResponse> UpdateLeaderboardMembership(bool isJoined)
{
if (OnlineModeSettings.IsLocalOnlyMode)
{
LeaderboardConsentUtility.SetLeaderboardConsent(isJoined);
return new LeaderboardMembershipResponse
{
success = true,
steam_id = SteamId,
is_joined = isJoined,
leaderboard_opt_out = !isJoined,
can_change_today = true,
changed_today = false,
next_change_at = string.Empty,
};
}
var req = new
{
steam_id = SteamId,
is_joined = isJoined,
leaderboard_opt_out = !isJoined
};
return await PutJson<LeaderboardMembershipResponse>(
BuildApiUrl("/api/profile/leaderboard-membership"),
req,
CancellationToken.None);
}
public async Task<ProfileData> GetPlayerByUid(int uid)
{
if (uid <= 0)
@@ -525,6 +576,12 @@ public class NetworkManager : MonoBehaviour
return "LOCAL_ONLY";
}
if (!LeaderboardConsentUtility.HasLeaderboardConsent())
{
Debug.Log("[NetworkManager] Leaderboard submission skipped because the player has not consented.");
return "OPT_OUT";
}
try
{
await PerformHandshakeAsync(CancellationToken.None);
@@ -673,7 +730,7 @@ public class NetworkManager : MonoBehaviour
if (VerboseLogs) Debug.Log($"[NetworkManager] Ping: {resp.message}");
}
public async Task<LeaderboardData> GetLeaderboard(string songId)
public async Task<LeaderboardData> GetLeaderboard(string songId, bool forceRefresh = false)
{
if (string.IsNullOrWhiteSpace(songId))
{
@@ -692,9 +749,17 @@ public class NetworkManager : MonoBehaviour
{
SetState(ConnectionState.LoadingLeaderboard);
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
LeaderboardData data = cache != null
? await cache.GetOrFetch(songId)
: await FetchLeaderboardFromServer(songId);
LeaderboardData data;
if (cache != null)
{
data = forceRefresh
? await cache.ForceRefresh(songId)
: await cache.GetOrFetch(songId);
}
else
{
data = await FetchLeaderboardFromServer(songId);
}
OnLeaderboardLoaded?.Invoke(data);
SetState(ConnectionState.Ready);
return data;
@@ -1503,6 +1568,42 @@ public class NetworkManager : MonoBehaviour
}
}
private async Task<T> PutJson<T>(string url, object payload, CancellationToken token)
{
if (OnlineModeSettings.IsLocalOnlyMode)
{
throw CreateLocalOnlyException($"HTTP PUT is disabled in local-only mode. url={url}");
}
string json = JsonConvert.SerializeObject(payload);
using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPUT))
{
byte[] body = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(body);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 15;
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP PUT {url}");
await SendRequestAsync(request.SendWebRequest(), token);
string responseText = request.downloadHandler != null ? request.downloadHandler.text : string.Empty;
if (request.result != UnityWebRequest.Result.Success && string.IsNullOrWhiteSpace(responseText))
{
throw new Exception($"{request.result}: {request.error}");
}
try
{
return JsonConvert.DeserializeObject<T>(responseText);
}
catch (Exception ex)
{
throw new Exception($"Failed to parse response: {ex.Message}. Body={responseText}");
}
}
}
private async Task<T> GetJson<T>(string url, CancellationToken token)
{
if (OnlineModeSettings.IsLocalOnlyMode)
@@ -1612,6 +1713,10 @@ public class NetworkManager : MonoBehaviour
song_id = songId,
cycle_id = 0,
is_locked = false,
server_name = "雅莉梦璃高新产业园",
leaderboard_cycle_days = 3,
rank_version_id = 20260101,
next_settle_at = "2026-01-01 00:00:00",
rankings = new List<LeaderboardEntry>()
};
}
@@ -54,12 +54,12 @@ MonoBehaviour:
iManaMinusSprite: {fileID: 21300000, guid: ab8b33f8f7d77e242b0da39cdebae043, type: 3}
iIscorePlusSprite: {fileID: 21300000, guid: e49d4f09f0f1b0646bf8fccb5dc816b7, type: 3}
iIscoreMinusSprite: {fileID: 21300000, guid: 629ef934809d32949a40ad47b1d13356, type: 3}
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 0}
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 0}
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 0}
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 0}
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 0}
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 0}
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 1}
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 1}
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 1}
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 1}
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 1}
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 1}
this_prefab_gravity: -9.81
x_ofst: 0
y_ofst: 0
@@ -70,6 +70,66 @@ MonoBehaviour:
jump_force: 125
fadeout_awaitTime: 0.25
fadeout_time: 0.25
verticalTravelDistance: 110
horizontalDriftMin: 18
horizontalDriftMax: 42
verticalMotionCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 4.5
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 0.58
value: 1
inSlope: 0.12
outSlope: 0.12
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0.42
inSlope: -2.4
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
horizontalMotionCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 2.2
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0.2
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!1 &5029723704172947119
GameObject:
m_ObjectHideFlags: 0
@@ -54,12 +54,12 @@ MonoBehaviour:
iManaMinusSprite: {fileID: 21300000, guid: ab8b33f8f7d77e242b0da39cdebae043, type: 3}
iIscorePlusSprite: {fileID: 21300000, guid: e49d4f09f0f1b0646bf8fccb5dc816b7, type: 3}
iIscoreMinusSprite: {fileID: 21300000, guid: 629ef934809d32949a40ad47b1d13356, type: 3}
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 0}
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 0}
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 0}
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 0}
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 0}
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 0}
iDamageColor: {r: 0.97647065, g: 0.38823533, b: 0.45098042, a: 1}
iHealColor: {r: 0.19215688, g: 0.8117648, b: 0.63529414, a: 1}
iManaPlusColor: {r: 0, g: 0.9490197, b: 0.9960785, a: 1}
iManaMinusColor: {r: 0.30980393, g: 0.67058825, b: 0.9960785, a: 1}
iIscorePlusColor: {r: 0.73333335, g: 0.8000001, b: 1, a: 1}
iIscoreMinusColor: {r: 0.7019608, g: 0.38823533, b: 0.92549026, a: 1}
this_prefab_gravity: -9.81
x_ofst: 0
y_ofst: 0
@@ -70,6 +70,66 @@ MonoBehaviour:
jump_force: 125
fadeout_awaitTime: 0.5
fadeout_time: 0.25
verticalTravelDistance: 110
horizontalDriftMin: 18
horizontalDriftMax: 42
verticalMotionCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 4.5
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 0.58
value: 1
inSlope: 0.12
outSlope: 0.12
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0.42
inSlope: -2.4
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
horizontalMotionCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 2.2
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0.2
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!1 &5029723704172947119
GameObject:
m_ObjectHideFlags: 0
@@ -137,8 +197,8 @@ MonoBehaviour:
m_Calls: []
m_text: 2147483647
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 452fa97492cae71479cf143f29930751, type: 2}
m_sharedMaterial: {fileID: -346136068272202111, guid: 452fa97492cae71479cf143f29930751, type: 2}
m_fontAsset: {fileID: 11400000, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
m_sharedMaterial: {fileID: -346136068272202111, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@@ -39,6 +39,22 @@ public class playerInstantNumbersPrefab : MonoBehaviour
public float fadeout_awaitTime;
public float fadeout_time = 0.25f;
[Header("motion feel")]
[Tooltip("Total height reached by the popup before it starts dropping back down.")]
public float verticalTravelDistance = 110f;
[Tooltip("Random horizontal drift distance applied over the popup lifetime.")]
public float horizontalDriftMin = 18f;
public float horizontalDriftMax = 42f;
[Tooltip("Controls the fast-rise, slow-peak, then drop feel. 0=start, 1=end.")]
public AnimationCurve verticalMotionCurve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 4.5f),
new Keyframe(0.58f, 1f, 0.12f, 0.12f),
new Keyframe(1f, 0.42f, -2.4f, 0f));
[Tooltip("Controls horizontal drift over lifetime. This is multiplied by a random left/right distance.")]
public AnimationCurve horizontalMotionCurve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2.2f),
new Keyframe(1f, 1f, 0.2f, 0f));
private RectTransform _rectTransform;
private CanvasGroup _canvasGroup;
private Coroutine _animationCoroutine;
@@ -151,10 +167,16 @@ public class playerInstantNumbersPrefab : MonoBehaviour
Vector2 baseOffset = new Vector2(x_ofst, y_ofst);
Vector2 randomOffset = Random.insideUnitCircle * xy_rdm_spawnP;
Vector2 finalOffset = baseOffset + randomOffset;
Vector2 spawnOffset = baseOffset + randomOffset;
if (useAnchored) _rectTransform.anchoredPosition = startAnchoredPos + finalOffset;
else transform.localPosition = startLocalPos + new Vector3(finalOffset.x, finalOffset.y, 0f);
float horizontalDirection = Random.value < 0.5f ? -1f : 1f;
float horizontalDistance = horizontalDirection * Random.Range(
Mathf.Min(horizontalDriftMin, horizontalDriftMax),
Mathf.Max(horizontalDriftMin, horizontalDriftMax));
float verticalDistance = Mathf.Max(0f, verticalTravelDistance);
if (useAnchored) _rectTransform.anchoredPosition = startAnchoredPos + spawnOffset;
else transform.localPosition = startLocalPos + new Vector3(spawnOffset.x, spawnOffset.y, 0f);
float zRot = Random.Range(-rdm_rotation_range, rdm_rotation_range);
transform.localRotation = Quaternion.Euler(0f, 0f, zRot);
@@ -162,28 +184,25 @@ public class playerInstantNumbersPrefab : MonoBehaviour
float scale = Random.Range(rdm_scale_min, rdm_scale_max);
transform.localScale = new Vector3(scale, scale, 1f);
float vy = jump_force;
float gravity = this_prefab_gravity;
float fadeDelay = Mathf.Max(0f, fadeout_awaitTime);
float fadeDuration = Mathf.Max(0.0001f, fadeout_time);
float totalDuration = Mathf.Max(0.01f, fadeDelay + fadeDuration);
float t = 0f;
while (t < fadeDelay + fadeDuration)
while (t < totalDuration)
{
float dt = Time.deltaTime;
vy += gravity * dt;
float normalized = Mathf.Clamp01(t / totalDuration);
float x = EvaluateCurve(horizontalMotionCurve, normalized) * horizontalDistance;
float y = EvaluateCurve(verticalMotionCurve, normalized) * verticalDistance;
if (useAnchored)
{
var p = _rectTransform.anchoredPosition;
p.y += vy * dt;
_rectTransform.anchoredPosition = p;
_rectTransform.anchoredPosition = startAnchoredPos + spawnOffset + new Vector2(x, y);
}
else
{
var p = transform.localPosition;
p.y += vy * dt;
transform.localPosition = p;
transform.localPosition = startLocalPos + new Vector3(spawnOffset.x + x, spawnOffset.y + y, 0f);
}
if (t >= fadeDelay)
@@ -197,10 +216,35 @@ public class playerInstantNumbersPrefab : MonoBehaviour
yield return null;
}
float finalX = EvaluateCurve(horizontalMotionCurve, 1f) * horizontalDistance;
float finalY = EvaluateCurve(verticalMotionCurve, 1f) * verticalDistance;
if (useAnchored)
{
_rectTransform.anchoredPosition = startAnchoredPos + spawnOffset + new Vector2(finalX, finalY);
}
else
{
transform.localPosition = startLocalPos + new Vector3(spawnOffset.x + finalX, spawnOffset.y + finalY, 0f);
}
if (_canvasGroup != null)
{
_canvasGroup.alpha = 0f;
}
_animationCoroutine = null;
if (!iNumberPrefabController.ReturnInstance(this))
{
Destroy(gameObject);
}
}
private static float EvaluateCurve(AnimationCurve curve, float time)
{
if (curve == null || curve.length == 0)
{
return time;
}
return curve.Evaluate(time);
}
}
@@ -7,7 +7,8 @@ using UnityEngine.UI;
public class rankingListPrefab : MonoBehaviour
{
private static string RoomRankingServerText => LocalizationService.Get("ranking.room_server", "根据房间成绩动态排名,所在服务端:雅莉梦璃高新产业园");
private const string FallbackServerName = "雅莉梦璃高新产业园";
private const int FallbackCycleDays = 3;
[Header("shutbutton")]
public Button shutbutton;
@@ -116,17 +117,8 @@ public class rankingListPrefab : MonoBehaviour
return;
}
LeaderboardData data = null;
LeaderboardCacheService cache = LeaderboardCacheService.Instance;
if (cache != null && cache.TryGetCached(currentSongId, out LeaderboardData cached))
{
data = cached;
}
else
{
SetEmptyState(false, LocalizationService.Get("ranking.loading", "正在拉取排行榜..."));
data = await nm.GetLeaderboard(currentSongId);
}
SetEmptyState(false, LocalizationService.Get("ranking.loading", "正在拉取排行榜..."));
LeaderboardData data = await nm.GetLeaderboard(currentSongId, true);
if (data == null || data.rankings == null || data.rankings.Count == 0)
{
@@ -246,7 +238,7 @@ public class rankingListPrefab : MonoBehaviour
if (versionandServer != null)
{
versionandServer.text = isRoomRanking ? RoomRankingServerText : string.Empty;
versionandServer.text = BuildVersionAndServerText(data);
}
if (yourPositionText == null)
@@ -286,6 +278,50 @@ public class rankingListPrefab : MonoBehaviour
yourPositionText.text = LocalizationService.GetFormat("ranking.your_position_value", localEntry.total_score, localEntry.rank);
}
private string BuildVersionAndServerText(LeaderboardData data)
{
string serverName = data != null && !string.IsNullOrWhiteSpace(data.server_name)
? data.server_name
: FallbackServerName;
int cycleDays = data != null && data.leaderboard_cycle_days > 0
? data.leaderboard_cycle_days
: FallbackCycleDays;
int rankVersionId = data != null && data.rank_version_id > 0
? data.rank_version_id
: BuildFallbackRankVersionId(cycleDays);
string nextSettleAt = data != null && !string.IsNullOrWhiteSpace(data.next_settle_at)
? data.next_settle_at
: BuildFallbackNextSettleAt(cycleDays);
return $"每{cycleDays}自然日进行一次排行榜更新。排行榜版本{rankVersionId},下次统计日期{nextSettleAt} 所在服务端:{serverName}";
}
private static string BuildFallbackNextSettleAt(int cycleDays)
{
DateTime epoch = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Local);
DateTime now = DateTime.Now;
int elapsedDays = (now.Date - epoch.Date).Days;
int nextSettleDays = ((elapsedDays / Mathf.Max(1, cycleDays)) + 1) * Mathf.Max(1, cycleDays);
DateTime nextSettle = epoch.AddDays(nextSettleDays);
if (nextSettle <= now)
{
nextSettle = nextSettle.AddDays(Mathf.Max(1, cycleDays));
}
return nextSettle.ToString("yyyy-MM-dd HH:mm:ss");
}
private static int BuildFallbackRankVersionId(int cycleDays)
{
string nextSettleAt = BuildFallbackNextSettleAt(cycleDays);
if (DateTime.TryParseExact(nextSettleAt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed))
{
return int.Parse(parsed.ToString("yyyyMMdd"));
}
return 0;
}
private static string FormatSubmitTime(LeaderboardEntry entry)
{
if (entry == null)
@@ -58,7 +58,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -66,8 +66,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -137,7 +137,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -145,8 +145,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -196,7 +196,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -22.589}
m_AnchoredPosition: {x: 0, y: -10}
m_SizeDelta: {x: 1100, y: 539.869}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3236275801312876574
@@ -332,7 +332,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 492b08f39f232d64e9ce8eb248b5daea, type: 3}
m_Sprite: {fileID: 21300000, guid: 3410813869a49f942b4e0198c93eb3ec, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -486,6 +486,81 @@ MonoBehaviour:
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: "\u62C9\u53D6\u6392\u884C\u699C\u5931\u8D25"
--- !u!1 &3019233615837960289
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 455604901716592107}
- component: {fileID: 2199194977054993423}
- component: {fileID: 1477107587712217919}
m_Layer: 0
m_Name: btm
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &455604901716592107
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3019233615837960289}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3018378993363515480}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 10}
m_SizeDelta: {x: 1050, y: 588.9733}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2199194977054993423
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3019233615837960289}
m_CullTransparentMesh: 1
--- !u!114 &1477107587712217919
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3019233615837960289}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.39215687}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7edca03691f782d46ac361a681ce5685, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &3483362754365948650
GameObject:
m_ObjectHideFlags: 0
@@ -525,7 +600,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -53.89, y: 260.83}
m_AnchoredPosition: {x: -53.89, y: 272.9}
m_SizeDelta: {x: 888.204, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &3005028479092236020
@@ -714,7 +789,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 292.2}
m_AnchoredPosition: {x: 0, y: 332.1}
m_SizeDelta: {x: 1400, y: 24}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6664561848449007207
@@ -738,7 +813,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1144,7 +1219,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1152,8 +1227,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -1195,6 +1270,7 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 455604901716592107}
- {fileID: 2611590964116081061}
- {fileID: 4148558001495883808}
- {fileID: 7280264963986874822}
@@ -1237,7 +1313,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Sprite: {fileID: 2139583702370686162, guid: f034ddca36ad627428d85c127caac8da, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -1305,7 +1381,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1313,8 +1389,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -1360,8 +1436,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -324.4}
m_SizeDelta: {x: 1400, y: 24}
m_AnchoredPosition: {x: 0, y: -317}
m_SizeDelta: {x: 1400, y: 28}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &341918732991438692
CanvasRenderer:
@@ -1384,7 +1460,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1393,7 +1469,7 @@ MonoBehaviour:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 22
m_FontSize: 28
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -1463,7 +1539,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1471,8 +1547,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 16
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 18
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -1543,7 +1619,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 0.14117648, g: 0.34117648, b: 0.7764706, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -1551,8 +1627,8 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_FontSize: 18
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
@@ -1998,7 +2074,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 312.4}
m_AnchoredPosition: {x: 0, y: 374.2}
m_SizeDelta: {x: 1400, y: 81.672}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4558535259015521777
@@ -2022,7 +2098,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -2031,12 +2107,12 @@ MonoBehaviour:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 40
m_FontSize: 32
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 230
m_Alignment: 1
m_Alignment: 4
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
@@ -1,4 +1,4 @@
%YAML 1.1
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &118873712428923354
GameObject:
@@ -30,13 +30,13 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4266895856353985475}
- {fileID: 7346852462872466010}
m_Father: {fileID: 5127964937674371758}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -60, y: 8.5}
m_SizeDelta: {x: 15, y: 15}
m_AnchoredPosition: {x: -60, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9219216993890693327
CanvasRenderer:
@@ -87,6 +87,7 @@ GameObject:
- component: {fileID: 4266895856353985475}
- component: {fileID: 3787018861283084079}
- component: {fileID: 267584174794072845}
- component: {fileID: 8475271739115695170}
m_Layer: 0
m_Name: rwdName
m_TagString: Untagged
@@ -101,18 +102,18 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1059233533614862680}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3546975410283370972}
m_Father: {fileID: 7346852462872466010}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 64.438, y: 0}
m_SizeDelta: {x: 105.799, y: 15}
m_Pivot: {x: 0.5, y: 0.5}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: -10}
m_SizeDelta: {x: 91, y: 15}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &3787018861283084079
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -134,7 +135,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -142,7 +143,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 15
m_FontStyle: 0
m_BestFit: 0
@@ -154,7 +155,115 @@ MonoBehaviour:
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: "\u54C1\u540D"
m_Text: "\u54C1\u540D\u662F12123"
--- !u!114 &8475271739115695170
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1059233533614862680}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 0
--- !u!1 &2029636835483568584
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6759631360171910247}
- component: {fileID: 4215255755343680456}
- component: {fileID: 793626109444426662}
- component: {fileID: 2852475067541627356}
m_Layer: 0
m_Name: space
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6759631360171910247
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2029636835483568584}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7346852462872466010}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 91, y: -10}
m_SizeDelta: {x: 8, y: 15}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &4215255755343680456
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2029636835483568584}
m_CullTransparentMesh: 1
--- !u!114 &793626109444426662
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2029636835483568584}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 15
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 0
m_MaxSize: 40
m_Alignment: 3
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: ' '
--- !u!114 &2852475067541627356
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2029636835483568584}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 0
--- !u!1 &5437409420464746555
GameObject:
m_ObjectHideFlags: 0
@@ -185,13 +294,12 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3546975410283370972}
- {fileID: 682261594771255014}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -26.7554}
m_SizeDelta: {x: 150, y: 35}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 150, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &8491477728461931688
MonoBehaviour:
@@ -208,9 +316,9 @@ MonoBehaviour:
reward_icon: {fileID: 6251169732777221245}
reward_name: {fileID: 267584174794072845}
reward_count: {fileID: 6866410370984929931}
coin_sprite: {fileID: 21300000, guid: 16f0765cc15fce04f828ecd44d1dbf91, type: 3}
jiyisuipian: {fileID: 21300000, guid: 6fd4dd33d986d824d95f7b01579acc4d, type: 3}
player_exp: {fileID: 21300000, guid: af3ce18394be1b448a9e38602526eb80, type: 3}
coin_sprite: {fileID: 21300000, guid: ddce9ea8759b6ca47b6d177982d1ed1e, type: 3}
jiyisuipian: {fileID: 21300000, guid: 9753216db376a75409e917958811eb28, type: 3}
player_exp: {fileID: 21300000, guid: de1739af68092d2478ed972daa5a9820, type: 3}
--- !u!1 &7159171791523732843
GameObject:
m_ObjectHideFlags: 0
@@ -222,6 +330,7 @@ GameObject:
- component: {fileID: 682261594771255014}
- component: {fileID: 8122003116666225205}
- component: {fileID: 6866410370984929931}
- component: {fileID: 2071136781458211337}
m_Layer: 0
m_Name: rwdACT
m_TagString: Untagged
@@ -241,13 +350,13 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5127964937674371758}
m_Father: {fileID: 7346852462872466010}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -5.157, y: -8.48}
m_SizeDelta: {x: 124.989, y: 18}
m_Pivot: {x: 0.5, y: 0.5}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 99, y: -10}
m_SizeDelta: {x: 96, y: 18}
m_Pivot: {x: 0, y: 0.5}
--- !u!222 &8122003116666225205
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -269,7 +378,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_Color: {r: 0.41960785, g: 0.5411765, b: 0.8039216, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -277,7 +386,7 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
m_FontSize: 16
m_FontStyle: 0
m_BestFit: 0
@@ -289,4 +398,98 @@ MonoBehaviour:
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: +10086
m_Text: +100861123
--- !u!114 &2071136781458211337
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7159171791523732843}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 0
--- !u!1 &9110073405764519532
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7346852462872466010}
- component: {fileID: 1268090614278476671}
- component: {fileID: 3900282080562181984}
m_Layer: 0
m_Name: hori
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7346852462872466010
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9110073405764519532}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4266895856353985475}
- {fileID: 6759631360171910247}
- {fileID: 682261594771255014}
m_Father: {fileID: 3546975410283370972}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 13.800049, y: 0.00002861023}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0, y: 0.5}
--- !u!114 &1268090614278476671
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9110073405764519532}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 3
m_Spacing: 0
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 0
m_ChildControlHeight: 0
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &3900282080562181984
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9110073405764519532}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 0
@@ -72,6 +72,7 @@ public class settlementController : MonoBehaviour
public Text mvp_score;
public Image mvp_heroIcon;
public Text mvp_heroName;
public Text legacy_text;
// Documentation text normalized.
[Header("Inspector")]
@@ -643,7 +644,7 @@ public class settlementController : MonoBehaviour
goodHitPercent_Text.text = targetGoodPercent.ToString("F1") + "%";
missHitPercent_Text.text = targetMissPercent.ToString("F1") + "%";
accuracy_Text.text = targetAccuracyPercent.ToString("F3") + "%";
accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%";
// Timing Statistics
if (earlyHitCount_Text != null) earlyHitCount_Text.text = sm.countEarly.ToString();
@@ -769,6 +770,29 @@ public class settlementController : MonoBehaviour
settlementRewardsGranted = true;
if (GameConfig.autoPlayEnabled)
{
moneyToGive_thisLevel = 0;
if (reward_money_Text != null)
{
reward_money_Text.text = "+0";
}
if (reward_idolEXP_bottle_Text != null)
{
reward_idolEXP_bottle_Text.text = "+0";
}
if (reward_playerEXP_Text != null)
{
reward_playerEXP_Text.text = "+0";
}
RebuildSettlementRewardVisuals(0, 0, 0);
return;
}
int coinReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.75f);
int materialReward = Mathf.CeilToInt((targetTotalScore / 10000f) * 0.20f);
int playerExpReward = Mathf.FloorToInt(targetTotalScore / 100000f);
@@ -1142,7 +1166,7 @@ public class settlementController : MonoBehaviour
PrepareTextForCountUp(pmScoreSum_Text, false, 0);
PrepareTextForCountUp(idolScoreSum_Text, false, 0);
PrepareTextForCountUp(accuracy_Text, true, 0f, "F3");
PrepareTextForCountUp(accuracy_Text, true, 0f, "F1");
PrepareTextForCountUp(finalScore_Text, false, 0);
PrepareTextForCountUp(thisLevel_currentPercentage_Text, true, 0f, "F2");
PrepareTextForCountUp(perfectHitCount_Text, false, 0);
@@ -1169,7 +1193,7 @@ public class settlementController : MonoBehaviour
Coroutine scoreBottomIn = null;
Coroutine shenstarsIn = null;
if (scoreBottomRt != null)
scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, -432.4f, introMoveDuration, true));
scoreBottomIn = StartCoroutine(AnimateAnchoredXAndFade(scoreBottomRt, -1646.9f, 0f, introMoveDuration, true));
if (shenstarsRt != null)
shenstarsIn = StartCoroutine(AnimateAnchoredXAndFade(shenstarsRt, -1602.1f, -437.5785f, introMoveDuration, true));
if (scoreBottomIn != null || shenstarsIn != null)
@@ -1182,7 +1206,7 @@ public class settlementController : MonoBehaviour
// score numbers: pm -> idol -> accuracy
yield return StartCoroutine(AnimateIntText(pmScoreSum_Text, targetPmScore, introNumberDuration));
yield return StartCoroutine(AnimateIntText(idolScoreSum_Text, targetIdolScore, introNumberDuration));
yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F3"));
yield return StartCoroutine(AnimateFloatPercentText(accuracy_Text, targetAccuracyPercent, introNumberDuration, "F1"));
// total + percent after above, and flash once completed
Coroutine totalScoreIn = StartCoroutine(AnimateIntText(finalScore_Text, targetTotalScore, introNumberDuration));
@@ -1287,7 +1311,7 @@ public class settlementController : MonoBehaviour
if (rightBottomRt != null) { SetAnchoredX(rightBottomRt, 1677.75f); SetCanvasAlpha(rightBottomRt, 1f); }
if (quhuiImageTr != null) SetCanvasAlpha(quhuiImageTr, 1f);
if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, -432.4f); SetCanvasAlpha(scoreBottomRt, 1f); }
if (scoreBottomRt != null) { SetAnchoredX(scoreBottomRt, 0f); SetCanvasAlpha(scoreBottomRt, 1f); }
if (shenstarsRt != null) { SetAnchoredX(shenstarsRt, -437.5785f); SetCanvasAlpha(shenstarsRt, 1f); }
if (rewardRt != null) { SetAnchoredY(rewardRt, 0f); SetCanvasAlpha(rewardRt, 1f); }
if (lihuiTr != null) SetCanvasAlpha(lihuiTr, 1f);
@@ -1311,7 +1335,7 @@ public class settlementController : MonoBehaviour
if (pmScoreSum_Text != null) { pmScoreSum_Text.text = targetPmScore.ToString(); SetCanvasAlpha(pmScoreSum_Text.transform, 1f); }
if (idolScoreSum_Text != null) { idolScoreSum_Text.text = targetIdolScore.ToString(); SetCanvasAlpha(idolScoreSum_Text.transform, 1f); }
if (accuracy_Text != null) { accuracy_Text.text = targetAccuracyPercent.ToString("F3") + "%"; SetCanvasAlpha(accuracy_Text.transform, 1f); }
if (accuracy_Text != null) { accuracy_Text.text = targetAccuracyPercent.ToString("F1") + "%"; SetCanvasAlpha(accuracy_Text.transform, 1f); }
if (finalScore_Text != null) { finalScore_Text.text = targetTotalScore.ToString(); SetCanvasAlpha(finalScore_Text.transform, 1f); }
if (thisLevel_currentPercentage_Text != null) { thisLevel_currentPercentage_Text.text = targetTotalPercent.ToString("F2") + "%"; SetCanvasAlpha(thisLevel_currentPercentage_Text.transform, 1f); }
@@ -2123,6 +2147,8 @@ public class settlementController : MonoBehaviour
mvp_hero_hd_image.sprite = null;
mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f);
}
// Activate legacy_text when no MVP conditions are met
if (legacy_text != null) legacy_text.gameObject.SetActive(true);
return;
}
@@ -2186,6 +2212,9 @@ public class settlementController : MonoBehaviour
if (mvp_heroIcon != null) mvp_heroIcon.sprite = heroSO.ally_hero_squareProfile;
if (mvp_score != null) mvp_score.text = max.ToString();
// Deactivate legacy_text when MVP is successfully displayed
if (legacy_text != null) legacy_text.gameObject.SetActive(false);
Debug.Log($"[SettlementController] MVP assigned from slot {topIndex+1} with score {max}: {heroSO.ally_heroName}");
}
else
@@ -2197,6 +2226,10 @@ public class settlementController : MonoBehaviour
mvp_hero_hd_image.sprite = null;
mvp_hero_hd_image.color = new Color(mvp_hero_hd_image.color.r, mvp_hero_hd_image.color.g, mvp_hero_hd_image.color.b, 0f);
}
// Activate legacy_text when hero SO is not found
if (legacy_text != null) legacy_text.gameObject.SetActive(true);
Debug.LogWarning("[SettlementController] MVP SO or HD image not found");
}
}
@@ -0,0 +1,533 @@
using System.Collections;
using UnityEngine;
public class trackFractureController : MonoBehaviour
{
private const int TrackCount = 5;
private static readonly int FadeId = Shader.PropertyToID("_Fade");
[System.Serializable]
private class TrackSpriteFadeEntry
{
public SpriteRenderer spriteRendererA;
public SpriteRenderer spriteRendererB;
}
[Header("Tracks")]
[SerializeField] private GameObject[] trackObjects = new GameObject[TrackCount];
[SerializeField] private Material[] dissolveMaterials = new Material[TrackCount];
[SerializeField] private TrackSpriteFadeEntry[] trackSpriteFadeEntries = new TrackSpriteFadeEntry[TrackCount];
[SerializeField] private bool autoFetchFractureAndDrift = true;
[SerializeField] private bool autoApplyDissolveMaterialOnStart = true;
[Tooltip("If true, each track gets its own runtime CLONE of its dissolve material. The clone REPLACES the " +
"material in the renderer slot, so you can no longer drive _Fade through the original material asset " +
"reference. Only needed when several tracks share one material asset. If false (default), the assigned " +
"dissolve material is used directly on the renderer, so controlling that material's _Fade (via this " +
"controller or externally) drives the dissolve.")]
[SerializeField] private bool cloneDissolveMaterialAtRuntime = false;
[Header("Fracture Fade")]
[Tooltip("float1: seconds to wait AFTER a track fractures before its dissolve _Fade starts changing.")]
[SerializeField] private float fractureFadeDelay = 0.2f;
[Tooltip("Seconds over which the linked SpriteRenderer pair fades from alpha 1 to 0 after fracture.")]
[SerializeField] private float spriteRendererFadeDuration = 0.2f;
[Tooltip("float2: seconds over which the dissolve _Fade is driven from 1 to 0 once the fade begins. " +
"When this completes, the track's fragments are reclaimed asynchronously (collected one-by-one).")]
[SerializeField] private float fractureFadeDuration = 1f;
[Tooltip("The _Fade value written when the fracture dissolve begins.")]
[SerializeField] private float fractureFadeStartValue = 0f;
[Tooltip("The _Fade value written when the fracture dissolve completes.")]
[SerializeField] private float fractureFadeEndValue = 1f;
[Header("Start Z Move")]
[SerializeField] private bool playStartZMove = true;
[SerializeField] private float startLocalZ = 0f;
[SerializeField] private float endLocalZ = 0.286f;
[SerializeField] private float moveDuration = 0.35f;
[Header("Debug")]
[SerializeField] private bool enableDebugKeyTrigger = true;
[SerializeField] private bool debugLogs = true;
private readonly FractureAndDrift[] fractureDrivers = new FractureAndDrift[TrackCount];
private readonly Renderer[] trackRenderers = new Renderer[TrackCount];
private readonly Material[] runtimeDissolveMaterials = new Material[TrackCount];
// Original _Fade of each ASSIGNED material asset, captured before we touch it, so we can restore it
// on teardown. Needed only when NOT cloning: we drive the asset directly, and Unity persists those
// writes back to the .mat, so the value would otherwise stick after exiting play mode. NaN = not captured.
private readonly float[] originalFadeValues = new float[TrackCount];
private Coroutine startMoveRoutine;
private readonly Coroutine[] fadeRoutines = new Coroutine[TrackCount];
private void Awake()
{
CacheTrackReferences();
}
private void Start()
{
CacheTrackReferences();
ApplyRuntimeDissolveMaterials();
SetAllTrackLocalZ(startLocalZ);
if (playStartZMove)
{
startMoveRoutine = StartCoroutine(AnimateTracksToEndZ());
}
}
private void Update()
{
if (!enableDebugKeyTrigger)
{
return;
}
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
if (debugLogs) Debug.Log("[trackFractureController] Debug key 1 pressed.", this);
TriggerTrackFracture(0);
}
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
if (debugLogs) Debug.Log("[trackFractureController] Debug key 2 pressed.", this);
TriggerTrackFracture(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
if (debugLogs) Debug.Log("[trackFractureController] Debug key 3 pressed.", this);
TriggerTrackFracture(2);
}
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
if (debugLogs) Debug.Log("[trackFractureController] Debug key 4 pressed.", this);
TriggerTrackFracture(3);
}
else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
if (debugLogs) Debug.Log("[trackFractureController] Debug key 5 pressed.", this);
TriggerTrackFracture(4);
}
}
private void OnValidate()
{
EnsureArraySizes();
if (!Application.isPlaying)
{
CacheTrackReferences();
}
}
private void OnDestroy()
{
// When not cloning, we drove the assigned .mat asset directly and Unity persists those writes.
// Restore each asset's original _Fade so the value doesn't stick after play mode ends.
RestoreOriginalFadeValues();
for (int i = 0; i < runtimeDissolveMaterials.Length; i++)
{
if (runtimeDissolveMaterials[i] != null)
{
Destroy(runtimeDissolveMaterials[i]);
runtimeDissolveMaterials[i] = null;
}
}
}
private void RestoreOriginalFadeValues()
{
for (int i = 0; i < TrackCount; i++)
{
// Only restore assets we drove directly (clones are thrown away, no restore needed).
if (runtimeDissolveMaterials[i] != null)
{
continue;
}
Material sourceMaterial = dissolveMaterials[i];
if (sourceMaterial == null || float.IsNaN(originalFadeValues[i]) || !sourceMaterial.HasProperty(FadeId))
{
continue;
}
sourceMaterial.SetFloat(FadeId, originalFadeValues[i]);
originalFadeValues[i] = float.NaN;
}
}
public void TriggerTrackFractureByNumber(int trackNumber)
{
TriggerTrackFracture(trackNumber - 1);
}
public void TriggerTrackFracture(int trackIndex)
{
if (!IsValidTrackIndex(trackIndex))
{
Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this);
return;
}
FractureAndDrift fractureDriver = fractureDrivers[trackIndex];
if (fractureDriver == null)
{
Debug.LogWarning($"[trackFractureController] Missing FractureAndDrift on track {trackIndex + 1}.", this);
return;
}
fractureDriver.Shatter();
StartFractureFade(trackIndex);
}
private void StartFractureFade(int trackIndex)
{
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
if (targetMaterial == null || !targetMaterial.HasProperty(FadeId))
{
return;
}
if (fadeRoutines[trackIndex] != null)
{
StopCoroutine(fadeRoutines[trackIndex]);
}
fadeRoutines[trackIndex] = StartCoroutine(FadeTrackOut(trackIndex, targetMaterial));
}
private IEnumerator FadeTrackOut(int trackIndex, Material targetMaterial)
{
TrackSpriteFadeEntry spriteFadeEntry = GetSpriteFadeEntry(trackIndex);
SetSpriteFadeEntryAlpha(spriteFadeEntry, 1f);
if (spriteRendererFadeDuration > 0f)
{
float spriteFadeElapsed = 0f;
while (spriteFadeElapsed < spriteRendererFadeDuration)
{
spriteFadeElapsed += Time.deltaTime;
float t = Mathf.Clamp01(spriteFadeElapsed / spriteRendererFadeDuration);
SetSpriteFadeEntryAlpha(spriteFadeEntry, Mathf.Lerp(1f, 0f, t));
yield return null;
}
}
else
{
SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f);
}
SetSpriteFadeEntryAlpha(spriteFadeEntry, 0f);
// float1: hold before the dissolve begins. Fragments have already started drifting during this time.
if (fractureFadeDelay > 0f)
{
yield return new WaitForSeconds(fractureFadeDelay);
}
// float2: drive _Fade from the configured start value to the configured end value over this duration.
float duration = Mathf.Max(0.0001f, fractureFadeDuration);
float startFade = Mathf.Clamp01(fractureFadeStartValue);
float endFade = Mathf.Clamp01(fractureFadeEndValue);
float elapsed = 0f;
targetMaterial.SetFloat(FadeId, startFade);
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
targetMaterial.SetFloat(FadeId, Mathf.Lerp(startFade, endFade, t));
yield return null;
}
targetMaterial.SetFloat(FadeId, endFade);
fadeRoutines[trackIndex] = null;
// Fade finished: asynchronously reclaim (collect) this track's fragments rather than destroying
// them all at once.
FractureAndDrift fractureDriver = fractureDrivers[trackIndex];
if (fractureDriver != null)
{
fractureDriver.ReclaimFragments();
}
}
public void SetTrackFadeByNumber(int trackNumber, float fadeValue)
{
SetTrackFade(trackNumber - 1, fadeValue);
}
public void SetTrackFade(int trackIndex, float fadeValue)
{
if (!IsValidTrackIndex(trackIndex))
{
Debug.LogWarning($"[trackFractureController] Invalid track index: {trackIndex}", this);
return;
}
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
if (targetMaterial == null)
{
Debug.LogWarning($"[trackFractureController] Missing dissolve material on track {trackIndex + 1}.", this);
return;
}
if (!targetMaterial.HasProperty(FadeId))
{
Debug.LogWarning($"[trackFractureController] Material on track {trackIndex + 1} has no _Fade property.", targetMaterial);
return;
}
targetMaterial.SetFloat(FadeId, Mathf.Clamp01(fadeValue));
}
public float GetTrackFade(int trackIndex)
{
if (!IsValidTrackIndex(trackIndex))
{
return 0f;
}
Material targetMaterial = GetFadeTargetMaterial(trackIndex);
if (targetMaterial == null || !targetMaterial.HasProperty(FadeId))
{
return 0f;
}
return targetMaterial.GetFloat(FadeId);
}
public void SetAllTrackFade(float fadeValue)
{
for (int i = 0; i < TrackCount; i++)
{
SetTrackFade(i, fadeValue);
}
}
public void RestartStartZMove()
{
SetAllTrackLocalZ(startLocalZ);
if (startMoveRoutine != null)
{
StopCoroutine(startMoveRoutine);
}
startMoveRoutine = StartCoroutine(AnimateTracksToEndZ());
}
public void TriggerTrack1Fracture() => TriggerTrackFracture(0);
public void TriggerTrack2Fracture() => TriggerTrackFracture(1);
public void TriggerTrack3Fracture() => TriggerTrackFracture(2);
public void TriggerTrack4Fracture() => TriggerTrackFracture(3);
public void TriggerTrack5Fracture() => TriggerTrackFracture(4);
private void CacheTrackReferences()
{
EnsureArraySizes();
for (int i = 0; i < TrackCount; i++)
{
GameObject trackObject = trackObjects[i];
fractureDrivers[i] = null;
trackRenderers[i] = null;
if (trackObject == null)
{
continue;
}
if (autoFetchFractureAndDrift)
{
fractureDrivers[i] = trackObject.GetComponent<FractureAndDrift>();
if (fractureDrivers[i] == null)
{
fractureDrivers[i] = trackObject.GetComponentInChildren<FractureAndDrift>(true);
}
}
trackRenderers[i] = trackObject.GetComponent<Renderer>();
if (trackRenderers[i] == null)
{
trackRenderers[i] = trackObject.GetComponentInChildren<Renderer>(true);
}
}
}
private void ApplyRuntimeDissolveMaterials()
{
for (int i = 0; i < TrackCount; i++)
{
if (runtimeDissolveMaterials[i] != null)
{
Destroy(runtimeDissolveMaterials[i]);
runtimeDissolveMaterials[i] = null;
}
originalFadeValues[i] = float.NaN;
Material sourceMaterial = dissolveMaterials[i];
Renderer trackRenderer = trackRenderers[i];
if (sourceMaterial == null)
{
continue;
}
// The material actually used on the renderer AND driven for fade. When cloning is off this is the
// assigned asset itself, so external control of that asset's _Fade drives the dissolve directly.
Material appliedMaterial = sourceMaterial;
if (cloneDissolveMaterialAtRuntime)
{
appliedMaterial = new Material(sourceMaterial) { name = sourceMaterial.name + "_Runtime" };
runtimeDissolveMaterials[i] = appliedMaterial;
}
else if (sourceMaterial.HasProperty(FadeId))
{
// Not cloning: we mutate the asset directly, so remember its original _Fade to restore later.
originalFadeValues[i] = sourceMaterial.GetFloat(FadeId);
}
if (autoApplyDissolveMaterialOnStart && trackRenderer != null)
{
Material[] materials = trackRenderer.sharedMaterials;
if (materials == null || materials.Length == 0)
{
trackRenderer.sharedMaterial = appliedMaterial;
}
else
{
bool replaced = false;
for (int j = 0; j < materials.Length; j++)
{
if (materials[j] == sourceMaterial)
{
materials[j] = appliedMaterial;
replaced = true;
}
}
if (!replaced)
{
materials[0] = appliedMaterial;
}
trackRenderer.sharedMaterials = materials;
}
}
}
}
private IEnumerator AnimateTracksToEndZ()
{
float duration = Mathf.Max(0.0001f, moveDuration);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
float z = Mathf.Lerp(startLocalZ, endLocalZ, t);
SetAllTrackLocalZ(z);
yield return null;
}
SetAllTrackLocalZ(endLocalZ);
startMoveRoutine = null;
}
private void SetAllTrackLocalZ(float zValue)
{
for (int i = 0; i < TrackCount; i++)
{
GameObject trackObject = trackObjects[i];
if (trackObject == null)
{
continue;
}
Transform targetTransform = trackObject.transform;
Vector3 localPosition = targetTransform.localPosition;
localPosition.z = zValue;
targetTransform.localPosition = localPosition;
}
}
private Material GetFadeTargetMaterial(int trackIndex)
{
if (!IsValidTrackIndex(trackIndex))
{
return null;
}
// When cloning is on, drive the runtime clone. Otherwise drive the assigned asset directly - which is
// also the material now used on the renderer, so controlling that asset's _Fade drives the dissolve.
return runtimeDissolveMaterials[trackIndex] != null
? runtimeDissolveMaterials[trackIndex]
: dissolveMaterials[trackIndex];
}
private void EnsureArraySizes()
{
if (trackObjects == null || trackObjects.Length != TrackCount)
{
System.Array.Resize(ref trackObjects, TrackCount);
}
if (dissolveMaterials == null || dissolveMaterials.Length != TrackCount)
{
System.Array.Resize(ref dissolveMaterials, TrackCount);
}
if (trackSpriteFadeEntries == null || trackSpriteFadeEntries.Length != TrackCount)
{
System.Array.Resize(ref trackSpriteFadeEntries, TrackCount);
}
}
private static bool IsValidTrackIndex(int trackIndex)
{
return trackIndex >= 0 && trackIndex < TrackCount;
}
private TrackSpriteFadeEntry GetSpriteFadeEntry(int trackIndex)
{
if (!IsValidTrackIndex(trackIndex) || trackSpriteFadeEntries == null || trackIndex >= trackSpriteFadeEntries.Length)
{
return null;
}
return trackSpriteFadeEntries[trackIndex];
}
private static void SetSpriteFadeEntryAlpha(TrackSpriteFadeEntry entry, float alpha)
{
if (entry == null)
{
return;
}
SetSpriteRendererAlpha(entry.spriteRendererA, alpha);
SetSpriteRendererAlpha(entry.spriteRendererB, alpha);
}
private static void SetSpriteRendererAlpha(SpriteRenderer spriteRenderer, float alpha)
{
if (spriteRenderer == null)
{
return;
}
Color color = spriteRenderer.color;
color.a = Mathf.Clamp01(alpha);
spriteRenderer.color = color;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f43164d05a229940a9b89f7c35236ae