325 lines
12 KiB
C#
325 lines
12 KiB
C#
![]() |
using UnityEngine;
|
|||
|
using System.Collections;
|
|||
|
using UnityEngine.SceneManagement;
|
|||
|
using System.Collections.Generic;
|
|||
|
|
|||
|
public class CrateEscapeGameManager : MonoBehaviour
|
|||
|
{
|
|||
|
public static CrateEscapeGameManager Instance { get; private set; }
|
|||
|
|
|||
|
// ---------- Level Selection ----------
|
|||
|
[Header("Levels")]
|
|||
|
[Tooltip("Drag all level GameObjects here (only one will be active).")]
|
|||
|
public List<GameObject> levelObjects = new List<GameObject>();
|
|||
|
|
|||
|
[Tooltip("PlayerPrefs key used to remember which level index is active.")]
|
|||
|
public string levelIndexPrefKey = "CrateEscape_LevelIndex";
|
|||
|
|
|||
|
[Tooltip("If true, will clamp/loop index at startup as well.")]
|
|||
|
public bool applyIndexOnAwake = true;
|
|||
|
|
|||
|
// ---------- Animation (death) ----------
|
|||
|
[Header("Animation (ZibuAnimDriver)")]
|
|||
|
public ZibuAnimDriver animDriver; // Assign your ZibuAnimDriver on the player
|
|||
|
public AnimationState deathState = AnimationState.Falling; // Play Falling on death
|
|||
|
[Range(0f, 1f)] public float deathCrossfade = 0.1f;
|
|||
|
|
|||
|
[Tooltip("If > 0, this exact time (seconds) is used instead of auto-detecting the clip length.")]
|
|||
|
public float deathWaitOverride = 0f;
|
|||
|
|
|||
|
[Header("Fallbacks")]
|
|||
|
[Tooltip("Used if clip length can't be determined and no override set.")]
|
|||
|
public float fallbackDeathDelay = 1.2f;
|
|||
|
|
|||
|
// ---------- UI / State ----------
|
|||
|
[Header("State/UI")]
|
|||
|
public bool isGameOver = false;
|
|||
|
public GameObject GameOverPanel;
|
|||
|
|
|||
|
[Tooltip("Shown when the player reaches the door / finishes the level.")]
|
|||
|
public GameObject LevelCompletePanel;
|
|||
|
|
|||
|
// Internal guards
|
|||
|
bool _isDying = false;
|
|||
|
bool _levelComplete = false;
|
|||
|
[Header("Death Anim (Direct State Name)")]
|
|||
|
public string deathStateName = "Falling"; // EXACT state name in Animator
|
|||
|
public int deathLayerIndex = 0;
|
|||
|
// Add somewhere near your other fields:
|
|||
|
[Header("Death Anim (robust)")]
|
|||
|
public string[] deathStateCandidates = new[] { "Falling", "Fall", "FallingLoop", "Squashed" };
|
|||
|
public bool useUnscaledIfTimescaleZero = true; // play death even if timeScale == 0
|
|||
|
|
|||
|
void Awake()
|
|||
|
{
|
|||
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
|||
|
Instance = this;
|
|||
|
|
|||
|
if (GameOverPanel) GameOverPanel.SetActive(false);
|
|||
|
if (LevelCompletePanel) LevelCompletePanel.SetActive(false);
|
|||
|
|
|||
|
if (applyIndexOnAwake)
|
|||
|
ApplyLevelIndexFromPrefs();
|
|||
|
|
|||
|
|
|||
|
if (!animDriver)
|
|||
|
{
|
|||
|
animDriver = FindObjectOfType<ZibuAnimDriver>();
|
|||
|
if (!animDriver)
|
|||
|
Debug.LogWarning("[CrateEscape] No ZibuAnimDriver assigned to GameManager.");
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// ---------- LEVEL HANDLING ----------
|
|||
|
public int GetCurrentLevelIndex()
|
|||
|
{
|
|||
|
return PlayerPrefs.GetInt(levelIndexPrefKey, 0);
|
|||
|
}
|
|||
|
|
|||
|
public void ApplyLevelIndexFromPrefs()
|
|||
|
{
|
|||
|
if (levelObjects == null || levelObjects.Count == 0) return;
|
|||
|
|
|||
|
int count = levelObjects.Count;
|
|||
|
int idx = GetCurrentLevelIndex();
|
|||
|
idx = Mod(idx, count);
|
|||
|
|
|||
|
for (int i = 0; i < count; i++)
|
|||
|
if (levelObjects[i]) levelObjects[i].SetActive(i == idx);
|
|||
|
}
|
|||
|
|
|||
|
static int Mod(int a, int n) => (n == 0) ? 0 : ((a % n) + n) % n;
|
|||
|
|
|||
|
// ---------- GAME OVER ----------
|
|||
|
public void GameOver()
|
|||
|
{
|
|||
|
if (isGameOver || _levelComplete) return; // don<6F>t double end if already completed
|
|||
|
isGameOver = true;
|
|||
|
|
|||
|
Time.timeScale = 0f;
|
|||
|
Debug.Log("[CrateEscape] GAME OVER");
|
|||
|
if (GameOverPanel) GameOverPanel.SetActive(true);
|
|||
|
}
|
|||
|
|
|||
|
public void Restarter()
|
|||
|
{
|
|||
|
Time.timeScale = 1f;
|
|||
|
Scene current = SceneManager.GetActiveScene();
|
|||
|
SceneManager.LoadScene(current.buildIndex);
|
|||
|
}
|
|||
|
|
|||
|
public void OnPlayerHitByLaser()
|
|||
|
{
|
|||
|
if (_isDying || isGameOver || _levelComplete) return;
|
|||
|
StartCoroutine(DeathSequenceThenGameOver());
|
|||
|
// inside CrateEscapeGameManager when death begins:
|
|||
|
var joy = FindObjectOfType<CrateEscapePlayerControllerJoystick>();
|
|||
|
if (joy) joy.LockControls(true);
|
|||
|
|
|||
|
}
|
|||
|
private void ForceEnterAnimatorState(Animator anim, int layer, string stateName, float xfade)
|
|||
|
{
|
|||
|
// Build both hashes: full path ("Base Layer.Falling") and short ("Falling")
|
|||
|
string layerName = anim.GetLayerName(layer);
|
|||
|
string fullPath = string.IsNullOrEmpty(layerName) ? stateName : $"{layerName}.{stateName}";
|
|||
|
|
|||
|
int fullHash = Animator.StringToHash(fullPath);
|
|||
|
int shortHash = Animator.StringToHash(stateName);
|
|||
|
|
|||
|
bool hasFull = anim.HasState(layer, fullHash);
|
|||
|
bool hasShort = anim.HasState(layer, shortHash); // often false; Unity prefers full path
|
|||
|
|
|||
|
// Try full path first
|
|||
|
if (hasFull)
|
|||
|
{
|
|||
|
anim.CrossFadeInFixedTime(fullHash, xfade, layer, 0f);
|
|||
|
// if still not in, hard play
|
|||
|
StartCoroutine(_EnsureState(anim, layer, fullHash, shortHash, stateName, xfade, triedFull: true));
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Try short name next
|
|||
|
if (hasShort)
|
|||
|
{
|
|||
|
anim.CrossFadeInFixedTime(shortHash, xfade, layer, 0f);
|
|||
|
StartCoroutine(_EnsureState(anim, layer, shortHash, fullHash, stateName, xfade, triedFull: false));
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
Debug.LogWarning($"[CrateEscape] Animator state not found: Layer={layerName} ({layer}), State='{stateName}'. " +
|
|||
|
"Check exact spelling/case or set 'deathStateName' to your real clip state name.");
|
|||
|
}
|
|||
|
private bool ForceEnterAny(Animator anim, int layer, string[] names, float xfade)
|
|||
|
{
|
|||
|
// Try full path first ("<LayerName>.<StateName>"), then short name.
|
|||
|
string layerName = anim.GetLayerName(layer);
|
|||
|
foreach (var name in names)
|
|||
|
{
|
|||
|
if (string.IsNullOrEmpty(name)) continue;
|
|||
|
|
|||
|
string fullPath = string.IsNullOrEmpty(layerName) ? name : $"{layerName}.{name}";
|
|||
|
int fullHash = Animator.StringToHash(fullPath);
|
|||
|
int shortHash = Animator.StringToHash(name);
|
|||
|
|
|||
|
bool hasFull = anim.HasState(layer, fullHash);
|
|||
|
bool hasShort = anim.HasState(layer, shortHash);
|
|||
|
|
|||
|
if (hasFull || hasShort)
|
|||
|
{
|
|||
|
int chosen = hasFull ? fullHash : shortHash;
|
|||
|
anim.CrossFadeInFixedTime(chosen, xfade, layer, 0f);
|
|||
|
// verify in a frame
|
|||
|
StartCoroutine(VerifyOrPlay(anim, layer, chosen, xfade));
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
private IEnumerator VerifyOrPlay(Animator anim, int layer, int hash, float xfade)
|
|||
|
{
|
|||
|
yield return null;
|
|||
|
var st = anim.GetCurrentAnimatorStateInfo(layer);
|
|||
|
var nxt = anim.GetNextAnimatorStateInfo(layer);
|
|||
|
bool inTarget = st.shortNameHash == hash ||
|
|||
|
(anim.IsInTransition(layer) && nxt.shortNameHash == hash);
|
|||
|
if (!inTarget)
|
|||
|
{
|
|||
|
anim.Play(hash, layer, 0f);
|
|||
|
yield return null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private IEnumerator _EnsureState(Animator anim, int layer, int primaryHash, int altHash, string stateName, float xfade, bool triedFull)
|
|||
|
{
|
|||
|
yield return null; // wait one frame
|
|||
|
var st = anim.GetCurrentAnimatorStateInfo(layer);
|
|||
|
var nxt = anim.GetNextAnimatorStateInfo(layer);
|
|||
|
|
|||
|
bool inTarget = st.shortNameHash == primaryHash ||
|
|||
|
(anim.IsInTransition(layer) && (nxt.shortNameHash == primaryHash));
|
|||
|
|
|||
|
if (!inTarget)
|
|||
|
{
|
|||
|
// try hard Play on the same hash
|
|||
|
anim.Play(primaryHash, layer, 0f);
|
|||
|
yield return null;
|
|||
|
|
|||
|
st = anim.GetCurrentAnimatorStateInfo(layer);
|
|||
|
nxt = anim.GetNextAnimatorStateInfo(layer);
|
|||
|
inTarget = st.shortNameHash == primaryHash ||
|
|||
|
(anim.IsInTransition(layer) && (nxt.shortNameHash == primaryHash));
|
|||
|
|
|||
|
if (!inTarget && altHash != 0)
|
|||
|
{
|
|||
|
// last resort: try the other hash type
|
|||
|
anim.CrossFadeInFixedTime(altHash, xfade, layer, 0f);
|
|||
|
yield return null;
|
|||
|
st = anim.GetCurrentAnimatorStateInfo(layer);
|
|||
|
nxt = anim.GetNextAnimatorStateInfo(layer);
|
|||
|
inTarget = st.shortNameHash == altHash ||
|
|||
|
(anim.IsInTransition(layer) && (nxt.shortNameHash == altHash));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!inTarget)
|
|||
|
{
|
|||
|
string layerName = anim.GetLayerName(layer);
|
|||
|
Debug.LogWarning($"[CrateEscape] Could not enter '{stateName}' on layer '{layerName}' (idx {layer}). " +
|
|||
|
"Verify state name/layer. If it's inside a sub-state machine, the short name must still be EXACT.");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private IEnumerator DeathSequenceThenGameOver()
|
|||
|
{
|
|||
|
_isDying = true;
|
|||
|
|
|||
|
// 1) Stop anything that could fight the death state
|
|||
|
var ctrl = animDriver ? animDriver.GetComponent<CratePlayerController>() : null;
|
|||
|
if (ctrl) ctrl.enabled = false;
|
|||
|
|
|||
|
// 2) Try very hard to enter a valid death state
|
|||
|
Animator anim = animDriver ? animDriver.animator : null;
|
|||
|
if (anim)
|
|||
|
{
|
|||
|
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
|||
|
anim.updateMode = AnimatorUpdateMode.Normal; // will switch to UnscaledTime below if needed
|
|||
|
|
|||
|
animDriver.SetGrounded(false);
|
|||
|
animDriver.SetSpeed(0f);
|
|||
|
|
|||
|
bool entered = ForceEnterAny(anim, deathLayerIndex, deathStateCandidates, deathCrossfade);
|
|||
|
yield return null; // let the state machine settle one frame
|
|||
|
|
|||
|
if (!entered)
|
|||
|
Debug.LogWarning("[CrateEscape] Could not enter any death state. Check names/layer.");
|
|||
|
}
|
|||
|
|
|||
|
// 3) Decide wait duration (prefer actual clip length if we<77>re in a real state)
|
|||
|
float wait = fallbackDeathDelay;
|
|||
|
if (deathWaitOverride > 0f)
|
|||
|
{
|
|||
|
wait = deathWaitOverride;
|
|||
|
}
|
|||
|
else if (anim)
|
|||
|
{
|
|||
|
var st = anim.GetCurrentAnimatorStateInfo(deathLayerIndex);
|
|||
|
var nxt = anim.GetNextAnimatorStateInfo(deathLayerIndex);
|
|||
|
float len = st.length;
|
|||
|
if (anim.IsInTransition(deathLayerIndex)) len = Mathf.Max(len, nxt.length);
|
|||
|
if (len > 0.05f) wait = len;
|
|||
|
}
|
|||
|
|
|||
|
// 4) Wait (works even if someone set timeScale = 0)
|
|||
|
float clamped = Mathf.Max(0.05f, wait);
|
|||
|
if (useUnscaledIfTimescaleZero && Time.timeScale == 0f && anim)
|
|||
|
{
|
|||
|
var prev = anim.updateMode;
|
|||
|
anim.updateMode = AnimatorUpdateMode.UnscaledTime;
|
|||
|
yield return new WaitForSecondsRealtime(clamped);
|
|||
|
anim.updateMode = prev;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
yield return new WaitForSeconds(clamped);
|
|||
|
}
|
|||
|
|
|||
|
GameOver();
|
|||
|
}
|
|||
|
|
|||
|
// ---------- LEVEL COMPLETE FLOW ----------
|
|||
|
/// <summary>Call this from the door trigger when the player enters.</summary>
|
|||
|
public void OnLevelCompleteTriggered()
|
|||
|
{
|
|||
|
if (_levelComplete || isGameOver) return;
|
|||
|
_levelComplete = true;
|
|||
|
|
|||
|
Time.timeScale = 0f;
|
|||
|
if (LevelCompletePanel) LevelCompletePanel.SetActive(true);
|
|||
|
Debug.Log("[CrateEscape] LEVEL COMPLETE");
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Hook this to the Continue button on the Level Complete UI.</summary>
|
|||
|
public void ContinueToNextLevel()
|
|||
|
{
|
|||
|
if (levelObjects == null || levelObjects.Count == 0)
|
|||
|
{
|
|||
|
// No levels listed: just reload scene unchanged.
|
|||
|
Time.timeScale = 1f;
|
|||
|
Restarter();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
int count = levelObjects.Count;
|
|||
|
int idx = GetCurrentLevelIndex();
|
|||
|
int next = Mod(idx + 1, count);
|
|||
|
|
|||
|
PlayerPrefs.SetInt(levelIndexPrefKey, next);
|
|||
|
PlayerPrefs.Save();
|
|||
|
|
|||
|
Time.timeScale = 1f;
|
|||
|
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
|||
|
}
|
|||
|
}
|