Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/Animation_GenerateJudgementSituationPrefab.cs
T
2026-07-26 03:44:27 +08:00

748 lines
25 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
{
private sealed class PooledJudgeTag : MonoBehaviour
{
public GameObject prefabSource;
}
private sealed class PooledTrackSkillTag : MonoBehaviour
{
public GameObject prefabSource;
}
public static Animation_GenerateJudgementSituationPrefab Instance;
[Header("Inspector")]
public GameObject redEffect;
public GameObject greenEffect;
public GameObject yellowEffect;
public GameObject purpleEffect;
public GameObject blueEffect;
[Header("Inspector")]
public GameObject perfect_judge_prefab;
public GameObject great_judge_prefab;
public GameObject good_judge_prefab;
public GameObject miss_judge_prefab;
[Header("Inspector")]
public GameObject trackSkill_prefab;
[Header("Inspector")]
public float baseScale = 1.0f;
public bool useRandomScale = true;
public float minScaleMultiplier = 0.8f;
public float maxScaleMultiplier = 1.2f;
[System.Serializable]
public class PopupMotionSettings
{
public float jumpForce = 5.0f;
public float gravity = -16f;
[Tooltip("Horizontal drift distance range for popup text. Adds a real arc instead of a straight pop.")]
public Vector2 horizontalDriftRange = new Vector2(0.25f, 0.65f);
[Tooltip("Random Z rotation range applied over the popup lifetime.")]
public float rotationRange = 8f;
[Tooltip("Temporary scale punch at the beginning of the popup.")]
public float scalePunch = 0.18f;
[Tooltip("How long the initial scale punch takes before settling back.")]
public float scalePunchDuration = 0.18f;
public float fadeInTime = 0.1f;
public float fadeOutStartTime = 0.6f;
public float fadeOutDuration = 0.35f;
}
[Header("Judge Popup")]
public PopupMotionSettings judgePopup = new PopupMotionSettings()
{
jumpForce = 7.2f,
gravity = -16f,
horizontalDriftRange = new Vector2(0.25f, 0.65f),
rotationRange = 8f,
scalePunch = 0.18f,
scalePunchDuration = 0.18f,
fadeInTime = 0.1f,
fadeOutStartTime = 0.3f,
fadeOutDuration = 0.1f
};
[Header("Track Skill Popup")]
public PopupMotionSettings trackSkillPopup = new PopupMotionSettings()
{
jumpForce = 6.8f,
gravity = -12.5f,
horizontalDriftRange = new Vector2(0.18f, 0.48f),
rotationRange = 6f,
scalePunch = 0.14f,
scalePunchDuration = 0.16f,
fadeInTime = 0.08f,
fadeOutStartTime = 0.36f,
fadeOutDuration = 0.1f
};
[Header("Track Skill UI")]
[Tooltip("UI prefab spawned under world-space Effect nodes needs a small world scale. Judge SpriteRenderer prefabs still use baseScale directly.")]
public float trackSkillUiScaleMultiplier = 0.01f;
public int trackSkillSortingOrder = 9000;
[Header("Track Skill Burst Stagger")]
[Tooltip("If multiple track-skill popups spawn within this time window, increase later popups' Y launch speed to reduce overlap.")]
public float trackSkillBurstWindow = 0.08f;
[Tooltip("Extra jump force added per near-simultaneous popup.")]
public float trackSkillBurstJumpForceStep = 0.65f;
[Tooltip("Maximum burst step applied to later popups.")]
public int trackSkillBurstMaxStep = 3;
private Transform redSpawn;
private Transform greenSpawn;
private Transform yellowSpawn;
private Transform purpleSpawn;
private Transform blueSpawn;
private int lastSceneIndex = -1;
private Coroutine prewarmJudgeCoroutine;
private readonly Dictionary<GameObject, Stack<GameObject>> judgePools = new Dictionary<GameObject, Stack<GameObject>>();
private readonly HashSet<GameObject> rentedJudges = new HashSet<GameObject>();
private readonly Dictionary<GameObject, Stack<GameObject>> trackSkillPools = new Dictionary<GameObject, Stack<GameObject>>();
private readonly HashSet<GameObject> rentedTrackSkills = new HashSet<GameObject>();
private float lastTrackSkillSpawnTime = -999f;
private int trackSkillBurstCount = 0;
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Destroy(gameObject);
return;
}
CacheSpawnPoints();
lastSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex;
}
private void Start()
{
CacheSpawnPointsIfNeeded();
}
private void OnEnable()
{
int currentSceneIndex = UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex;
if (currentSceneIndex != lastSceneIndex)
{
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Scene changed detected (from {lastSceneIndex} to {currentSceneIndex}), refreshing spawn point cache");
lastSceneIndex = currentSceneIndex;
CacheSpawnPoints();
}
else
{
CacheSpawnPointsIfNeeded();
}
}
private void CacheSpawnPointsIfNeeded()
{
if (redSpawn == null || greenSpawn == null || yellowSpawn == null || purpleSpawn == null || blueSpawn == null)
{
bool anyInvalid = (redEffect == null || redEffect.transform == null) ||
(greenEffect == null || greenEffect.transform == null) ||
(yellowEffect == null || yellowEffect.transform == null) ||
(purpleEffect == null || purpleEffect.transform == null) ||
(blueEffect == null || blueEffect.transform == null);
if (anyInvalid || redSpawn == null)
{
CacheSpawnPoints();
}
}
}
private void CacheSpawnPoints()
{
redSpawn = redEffect != null ? redEffect.transform : null;
greenSpawn = greenEffect != null ? greenEffect.transform : null;
yellowSpawn = yellowEffect != null ? yellowEffect.transform : null;
purpleSpawn = purpleEffect != null ? purpleEffect.transform : null;
blueSpawn = blueEffect != null ? blueEffect.transform : null;
if (JudgeManager.IsDebugEnabled)
{
Debug.Log($"[Animation_Generate] Cached spawn points: red={redSpawn != null}, green={greenSpawn != null}, yellow={yellowSpawn != null}, purple={purpleSpawn != null}, blue={blueSpawn != null}");
}
}
public void SpawnJudgePrefab(string color, string judgeResult)
{
if (string.IsNullOrEmpty(color))
{
Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty color!");
return;
}
if (string.IsNullOrEmpty(judgeResult))
{
Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty judgeResult!");
return;
}
Transform spawnPoint = GetSpawnPoint(color);
if (spawnPoint == null)
{
Debug.LogError($"[Animation_Generate] Could not find spawn point for color: {color}");
return;
}
GameObject prefabToUse = GetJudgePrefab(judgeResult);
if (prefabToUse == null)
{
Debug.LogError($"[Animation_Generate] Could not find judge prefab for result: {judgeResult}. Available: perfect={perfect_judge_prefab != null}, great={great_judge_prefab != null}, good={good_judge_prefab != null}, miss={miss_judge_prefab != null}");
return;
}
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Animation_Generate] Spawning judge prefab: color={color}, result={judgeResult}, prefab={prefabToUse.name}");
GameObject instance = GetPooledJudge(prefabToUse, spawnPoint);
if (instance == null)
return;
instance.transform.localPosition = Vector3.zero;
instance.transform.localRotation = Quaternion.identity;
float finalScale = baseScale;
if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier);
instance.transform.localScale = new Vector3(finalScale, finalScale, 1f);
StartCoroutine(AnimatePopup(instance, judgePopup, false));
}
public void SpawnTrackSkillPrefab(string color, Sprite skillIcon, string skillName)
{
if (string.IsNullOrEmpty(color) || trackSkill_prefab == null)
return;
Transform spawnPoint = GetSpawnPoint(color);
if (spawnPoint == null)
return;
GameObject instance = GetPooledTrackSkill(trackSkill_prefab, spawnPoint);
if (instance == null)
return;
EnsureRenderableTrackSkillInstance(instance);
BindTrackSkillContent(instance, skillIcon, skillName);
instance.transform.localPosition = Vector3.zero;
instance.transform.localRotation = Quaternion.identity;
float finalScale = baseScale;
if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier);
ApplyTrackSkillScale(instance, finalScale);
instance.SetActive(true);
PopupMotionSettings popupSettings = CreateTrackSkillPopupSettings();
StartCoroutine(AnimatePopup(instance, popupSettings, true));
}
private PopupMotionSettings CreateTrackSkillPopupSettings()
{
PopupMotionSettings popupSettings = new PopupMotionSettings
{
jumpForce = trackSkillPopup.jumpForce,
gravity = trackSkillPopup.gravity,
horizontalDriftRange = trackSkillPopup.horizontalDriftRange,
rotationRange = trackSkillPopup.rotationRange,
scalePunch = trackSkillPopup.scalePunch,
scalePunchDuration = trackSkillPopup.scalePunchDuration,
fadeInTime = trackSkillPopup.fadeInTime,
fadeOutStartTime = trackSkillPopup.fadeOutStartTime,
fadeOutDuration = trackSkillPopup.fadeOutDuration
};
float now = Time.unscaledTime;
float window = Mathf.Max(0f, trackSkillBurstWindow);
trackSkillBurstCount = now - lastTrackSkillSpawnTime <= window ? trackSkillBurstCount + 1 : 0;
lastTrackSkillSpawnTime = now;
int appliedStep = Mathf.Clamp(trackSkillBurstCount, 0, Mathf.Max(0, trackSkillBurstMaxStep));
popupSettings.jumpForce += Mathf.Max(0f, trackSkillBurstJumpForceStep) * appliedStep;
return popupSettings;
}
public void PrewarmJudgePrefabs(int perPrefab = 1)
{
if (!Application.isPlaying)
return;
if (prewarmJudgeCoroutine != null)
{
StopCoroutine(prewarmJudgeCoroutine);
}
prewarmJudgeCoroutine = StartCoroutine(PrewarmJudgePrefabsRoutine(Mathf.Max(1, perPrefab)));
}
public IEnumerator PrewarmJudgePrefabsRoutine(int perPrefab = 1)
{
GameObject[] prefabs =
{
perfect_judge_prefab,
great_judge_prefab,
good_judge_prefab,
miss_judge_prefab,
trackSkill_prefab
};
int count = Mathf.Max(1, perPrefab);
foreach (GameObject prefab in prefabs)
{
if (prefab == null)
continue;
for (int i = 0; i < count; i++)
{
GameObject instance = null;
try
{
if (prefab == trackSkill_prefab)
{
instance = CreatePooledTrackSkill(prefab);
if (instance != null)
{
rentedTrackSkills.Add(instance);
}
ReturnTrackSkillToPool(instance);
}
else
{
instance = CreatePooledJudge(prefab);
if (instance != null)
{
rentedJudges.Add(instance);
}
ReturnJudgeToPool(instance);
}
}
catch { }
yield return null;
}
}
prewarmJudgeCoroutine = null;
}
private IEnumerator AnimatePopup(GameObject obj, PopupMotionSettings settings, bool destroyAtEnd = false)
{
if (obj == null) yield break;
if (settings == null) settings = judgePopup;
SpriteRenderer[] renderers = obj.GetComponentsInChildren<SpriteRenderer>(true);
Graphic[] graphics = obj.GetComponentsInChildren<Graphic>(true);
CanvasGroup canvasGroup = obj.GetComponent<CanvasGroup>();
float elapsed = 0f;
float vVelocity = settings.jumpForce;
float fadeDuration = Mathf.Max(0.001f, settings.fadeOutDuration);
float estimatedApexTime = settings.gravity < -0.001f ? Mathf.Max(0f, settings.jumpForce / -settings.gravity) : settings.fadeOutStartTime;
float totalLifeTime = Mathf.Max(settings.fadeInTime + fadeDuration, estimatedApexTime + fadeDuration);
float driftSign = Random.value < 0.5f ? -1f : 1f;
float driftDistance = Random.Range(settings.horizontalDriftRange.x, settings.horizontalDriftRange.y) * driftSign;
float horizontalVelocity = totalLifeTime > 0.001f ? driftDistance / totalLifeTime : 0f;
float targetRotation = Random.Range(-settings.rotationRange, settings.rotationRange);
Vector3 currentLocalPos = Vector3.zero;
Vector3 baseScaleValue = obj.transform.localScale;
bool fadeOutStarted = false;
float fadeOutElapsed = 0f;
float fadeOutStartAlpha = 1f;
float currentAlpha = 0f;
SetRenderersAlpha(renderers, 0f);
SetGraphicsAlpha(graphics, canvasGroup, 0f);
while (!fadeOutStarted || fadeOutElapsed < fadeDuration)
{
if (obj == null) yield break;
float dt = Time.deltaTime;
elapsed += dt;
float previousVelocity = vVelocity;
vVelocity += settings.gravity * dt;
currentLocalPos.x += horizontalVelocity * dt;
currentLocalPos.y += vVelocity * dt;
obj.transform.localPosition = currentLocalPos;
obj.transform.localRotation = Quaternion.Euler(0f, 0f, Mathf.Lerp(0f, targetRotation, Mathf.Clamp01(elapsed / totalLifeTime)));
float scaleT = settings.scalePunchDuration > 0.001f ? Mathf.Clamp01(elapsed / settings.scalePunchDuration) : 1f;
float punch = Mathf.Sin(scaleT * Mathf.PI) * settings.scalePunch;
obj.transform.localScale = baseScaleValue * (1f + punch);
bool reachedApex = previousVelocity > 0f && vVelocity <= 0f;
if (!fadeOutStarted && (reachedApex || elapsed >= totalLifeTime - fadeDuration))
{
fadeOutStarted = true;
fadeOutElapsed = 0f;
fadeOutStartAlpha = currentAlpha;
}
float alpha;
if (fadeOutStarted)
{
fadeOutElapsed += dt;
alpha = Mathf.Lerp(fadeOutStartAlpha, 0f, Mathf.Clamp01(fadeOutElapsed / fadeDuration));
}
else if (elapsed < settings.fadeInTime)
{
alpha = Mathf.InverseLerp(0f, settings.fadeInTime, elapsed);
}
else
{
alpha = 1f;
}
alpha = Mathf.Clamp01(alpha);
currentAlpha = alpha;
SetRenderersAlpha(renderers, alpha);
SetGraphicsAlpha(graphics, canvasGroup, alpha);
yield return null;
}
if (obj != null)
{
obj.transform.localScale = baseScaleValue;
obj.transform.localRotation = Quaternion.identity;
SetRenderersAlpha(renderers, 0f);
SetGraphicsAlpha(graphics, canvasGroup, 0f);
}
if (obj != null)
{
if (destroyAtEnd)
ReturnTrackSkillToPool(obj);
else
ReturnJudgeToPool(obj);
}
}
private static void SetRenderersAlpha(SpriteRenderer[] renderers, float alpha)
{
if (renderers == null)
return;
for (int i = 0; i < renderers.Length; i++)
{
SpriteRenderer sr = renderers[i];
if (sr == null)
continue;
Color c = sr.color;
c.a = alpha;
sr.color = c;
}
}
private static void SetGraphicsAlpha(Graphic[] graphics, CanvasGroup canvasGroup, float alpha)
{
if (canvasGroup != null)
{
canvasGroup.alpha = alpha;
return;
}
if (graphics == null)
return;
for (int i = 0; i < graphics.Length; i++)
{
Graphic graphic = graphics[i];
if (graphic == null)
continue;
Color c = graphic.color;
c.a = alpha;
graphic.color = c;
}
}
private void EnsureRenderableTrackSkillInstance(GameObject instance)
{
if (instance == null)
return;
if (instance.GetComponentInChildren<Graphic>(true) == null)
return;
Canvas canvas = instance.GetComponent<Canvas>();
if (canvas == null)
canvas = instance.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.overrideSorting = true;
canvas.sortingOrder = trackSkillSortingOrder;
canvas.pixelPerfect = false;
CanvasGroup canvasGroup = instance.GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = instance.AddComponent<CanvasGroup>();
canvasGroup.alpha = 1f;
}
private void ApplyTrackSkillScale(GameObject instance, float finalScale)
{
if (instance == null)
return;
bool isUiPrefab = instance.GetComponentInChildren<Graphic>(true) != null;
float appliedScale = isUiPrefab ? finalScale * trackSkillUiScaleMultiplier : finalScale;
instance.transform.localScale = new Vector3(appliedScale, appliedScale, 1f);
}
private void BindTrackSkillContent(GameObject instance, Sprite skillIcon, string skillName)
{
if (instance == null)
return;
Image targetImage = null;
Text targetText = null;
TrackSkillPrefab modern = instance.GetComponent<TrackSkillPrefab>();
if (modern != null)
{
modern.Bind(skillIcon, skillName);
targetImage = modern.skillImage;
targetText = modern.skillText;
}
else
{
trackSkillPrefab legacy = instance.GetComponent<trackSkillPrefab>();
if (legacy != null)
{
targetImage = legacy.track_skillIcon;
targetText = legacy.track_skillName;
}
}
if (targetImage == null)
targetImage = instance.GetComponentInChildren<Image>(true);
if (targetText == null)
targetText = instance.GetComponentInChildren<Text>(true);
if (targetImage != null)
{
targetImage.sprite = skillIcon;
targetImage.enabled = skillIcon != null;
}
if (targetText != null)
{
targetText.text = string.IsNullOrWhiteSpace(skillName) ? string.Empty : skillName;
}
}
private GameObject GetPooledJudge(GameObject prefab, Transform parent)
{
if (prefab == null || parent == null)
return null;
if (!judgePools.TryGetValue(prefab, out var pool))
{
pool = new Stack<GameObject>();
judgePools[prefab] = pool;
}
GameObject instance = null;
while (pool.Count > 0 && instance == null)
{
instance = pool.Pop();
}
if (instance == null)
{
instance = CreatePooledJudge(prefab);
}
if (instance == null)
return null;
instance.transform.SetParent(parent, false);
instance.SetActive(true);
rentedJudges.Add(instance);
return instance;
}
private GameObject CreatePooledJudge(GameObject prefab)
{
if (prefab == null)
return null;
GameObject instance = Instantiate(prefab, transform);
var tag = instance.GetComponent<PooledJudgeTag>();
if (tag == null)
tag = instance.AddComponent<PooledJudgeTag>();
tag.prefabSource = prefab;
instance.SetActive(false);
return instance;
}
private GameObject GetPooledTrackSkill(GameObject prefab, Transform parent)
{
if (prefab == null || parent == null)
return null;
if (!trackSkillPools.TryGetValue(prefab, out var pool))
{
pool = new Stack<GameObject>();
trackSkillPools[prefab] = pool;
}
GameObject instance = null;
while (pool.Count > 0 && instance == null)
{
instance = pool.Pop();
}
if (instance == null)
{
instance = CreatePooledTrackSkill(prefab);
}
if (instance == null)
return null;
instance.transform.SetParent(parent, false);
instance.SetActive(true);
rentedTrackSkills.Add(instance);
return instance;
}
private GameObject CreatePooledTrackSkill(GameObject prefab)
{
if (prefab == null)
return null;
GameObject instance = Instantiate(prefab, transform);
var tag = instance.GetComponent<PooledTrackSkillTag>();
if (tag == null)
tag = instance.AddComponent<PooledTrackSkillTag>();
tag.prefabSource = prefab;
EnsureRenderableTrackSkillInstance(instance);
instance.SetActive(false);
return instance;
}
private void ReturnTrackSkillToPool(GameObject instance)
{
if (instance == null)
return;
if (!rentedTrackSkills.Remove(instance))
return;
var tag = instance.GetComponent<PooledTrackSkillTag>();
if (tag == null || tag.prefabSource == null)
{
Destroy(instance);
return;
}
if (!trackSkillPools.TryGetValue(tag.prefabSource, out var pool))
{
pool = new Stack<GameObject>();
trackSkillPools[tag.prefabSource] = pool;
}
instance.transform.SetParent(transform, false);
instance.transform.localPosition = new Vector3(99999f, 99999f, 0f);
instance.transform.localRotation = Quaternion.identity;
instance.SetActive(false);
pool.Push(instance);
}
private void ReturnJudgeToPool(GameObject instance)
{
if (instance == null)
return;
if (!rentedJudges.Remove(instance))
return;
var tag = instance.GetComponent<PooledJudgeTag>();
if (tag == null || tag.prefabSource == null)
{
Destroy(instance);
return;
}
if (!judgePools.TryGetValue(tag.prefabSource, out var pool))
{
pool = new Stack<GameObject>();
judgePools[tag.prefabSource] = pool;
}
instance.transform.SetParent(transform, false);
instance.transform.localPosition = new Vector3(99999f, 99999f, 0f);
instance.transform.localRotation = Quaternion.identity;
instance.SetActive(false);
pool.Push(instance);
}
private Transform GetSpawnPoint(string color)
{
if (string.IsNullOrEmpty(color)) return null;
Transform point = null;
switch (color.ToLower())
{
case "red": point = redSpawn; break;
case "green": point = greenSpawn; break;
case "yellow": point = yellowSpawn; break;
case "purple": point = purpleSpawn; break;
case "blue": point = blueSpawn; break;
default:
Debug.LogWarning($"[Animation_Generate] Unknown color: {color}");
return null;
}
if (point == null)
{
Debug.LogError($"[Animation_Generate] Spawn point for color {color} is null. Check if effects are assigned in inspector. Make sure the effect object is active in the scene.");
}
return point;
}
private GameObject GetJudgePrefab(string result)
{
if (string.IsNullOrEmpty(result)) return null;
GameObject prefab = null;
switch (result.ToLower())
{
case "perfect": prefab = perfect_judge_prefab; break;
case "great": prefab = great_judge_prefab; break;
case "good": prefab = good_judge_prefab; break;
case "miss": prefab = miss_judge_prefab; break;
default:
Debug.LogWarning($"[Animation_Generate] Unknown judge result: {result}");
return null;
}
if (prefab == null)
{
Debug.LogError($"[Animation_Generate] Judge prefab for '{result}' is not assigned in inspector! Check Animation_GenerateJudgementSituationPrefab component.");
}
return prefab;
}
}