ui基本完毕,修了一大把的bug
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = 角色 30206;77003 = 歌曲1;77004 = 歌曲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>()
|
||||
};
|
||||
}
|
||||
|
||||
+66
-6
@@ -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
|
||||
|
||||
+68
-8
@@ -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: []
|
||||
|
||||
+57
-13
@@ -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
|
||||
Reference in New Issue
Block a user