using System.Collections; using DG.Tweening; using UnityEngine; public class ChasePlayerController : MonoBehaviour { [Header("Movement")] public float moveSpeed = 5f; public float laneDistance = 2.5f; // 0=Left,1=Mid,2=Right public float jumpPower = 0.6f; public float jumpDuration = 1.2f; public float laneSwitchSpeed = 10f; // used as units per second [Header("Animator Driving")] public bool useSpeedBlendTree = true; public string speedParamName = "Speed"; public float runSpeedParamValue = 2f; public string runStateName = "Locomotion"; public int baseLayer = 0; [Header("Fall / GameOver Logic")] public string fallStateName = "Fall"; public string fallingStateName = "Falling"; public float secondHitWindow = 10f; public float stateWaitTimeout = 3f; public static System.Action OnMoveSpeedChanged; int currentLane = 1; Rigidbody rb; Animator animator; bool isGrounded = true; bool isJumping = false; Vector2 startTouchPosition; bool swipeDetected = false; float minSwipeDistance = 50f; float lastObstacleHitTime = -999f; bool waitingForGameOver = false; int runShortHash, fallShortHash, fallingShortHash; [SerializeField] bool validateStatesOnStart = true; [SerializeField] string runTag = "Run"; [SerializeField] string fallTag = "Fall"; [SerializeField] string fallingTag = "Falling"; float originalMoveSpeed; bool unableToMove = false; ChaseScoreManager scoreManager; void Start() { rb = GetComponent(); animator = GetComponent(); scoreManager = FindObjectOfType(); runShortHash = Animator.StringToHash(runStateName); fallShortHash = Animator.StringToHash(fallStateName); fallingShortHash = Animator.StringToHash(fallingStateName); if (validateStatesOnStart) { if (!animator.HasState(baseLayer, runShortHash)) Debug.LogError($"Run state '{runStateName}' not found"); if (!animator.HasState(baseLayer, fallShortHash)) Debug.LogError($"Fall state '{fallStateName}' not found"); if (!animator.HasState(baseLayer, fallingShortHash)) Debug.LogError($"Falling state '{fallingStateName}' not found"); } originalMoveSpeed = moveSpeed; ForceRunStart(); } public void SetMoveSpeed(float newSpeed) { moveSpeed = newSpeed; OnMoveSpeedChanged?.Invoke(newSpeed); } void Update() { DriveRunAnimation(); if (!unableToMove) { HandleInput(); HandleSwipe(); } } void FixedUpdate() { MoveForward(); } void HandleInput() { if (isJumping) return; if (Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0) { currentLane--; TweenToLaneX((currentLane - 1) * laneDistance); } if (Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2) { currentLane++; TweenToLaneX((currentLane - 1) * laneDistance); } if (Input.GetKeyDown(KeyCode.Space) && isGrounded) Jump(); } void HandleSwipe() { if (Input.touchCount != 1) return; var touch = Input.GetTouch(0); switch (touch.phase) { case TouchPhase.Began: startTouchPosition = touch.position; swipeDetected = true; break; case TouchPhase.Ended: if (!swipeDetected) return; Vector2 swipe = touch.position - startTouchPosition; if (swipe.magnitude >= minSwipeDistance && !isJumping) { if (Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y)) { if (swipe.x > 0 && currentLane < 2) { currentLane++; TweenToLaneX((currentLane - 1) * laneDistance); } else if (swipe.x < 0 && currentLane > 0) { currentLane--; TweenToLaneX((currentLane - 1) * laneDistance); } } else if (swipe.y > 0 && isGrounded) { Jump(); } } swipeDetected = false; break; } } void TweenToLaneX(float targetX) { float distance = Mathf.Abs(rb.position.x - targetX); float duration = distance / laneSwitchSpeed; rb.DOMoveX(targetX, duration).SetEase(Ease.OutQuad); } void MoveForward() { rb.MovePosition(rb.position + Vector3.back * moveSpeed * Time.fixedDeltaTime); } void Jump() { if (!isGrounded) return; isGrounded = false; isJumping = true; rb.velocity = Vector3.zero; rb.useGravity = false; float forwardDisplacement = moveSpeed * jumpDuration; Vector3 jumpTarget = rb.position + Vector3.back * forwardDisplacement; rb.DOJump(jumpTarget, jumpPower, 1, jumpDuration) .SetEase(Ease.Linear) .OnStart(() => { SafeSetTrigger("Jump"); SafeSetBool("IsGrounded", false); }) .OnComplete(() => { rb.useGravity = true; isGrounded = true; isJumping = false; SafeSetTrigger("Land"); SafeSetBool("IsGrounded", true); ForceRunStart(); }); } void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = true; SafeSetBool("IsGrounded", true); ForceRunStart(); } } void ForceRunStart(bool ignoreGuards = false) { if (!ignoreGuards && (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || waitingForGameOver)) return; if (useSpeedBlendTree && HasParameter(speedParamName, AnimatorControllerParameterType.Float)) { animator.SetFloat(speedParamName, runSpeedParamValue); } else if (!string.IsNullOrEmpty(runStateName)) { animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } void DriveRunAnimation() { if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || isJumping) return; if (useSpeedBlendTree && HasParameter(speedParamName, AnimatorControllerParameterType.Float)) { animator.SetFloat(speedParamName, runSpeedParamValue, 0.1f, Time.deltaTime); } else { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); var nxt = animator.GetNextAnimatorStateInfo(baseLayer); bool inRunNow = st.shortNameHash == runShortHash || st.IsTag(runTag); bool goingToRun = animator.IsInTransition(baseLayer) && (nxt.shortNameHash == runShortHash || nxt.IsTag(runTag)); if (!isJumping && !inRunNow && !goingToRun) { animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } } bool HasParameter(string name, AnimatorControllerParameterType type) { foreach (var p in animator.parameters) if (p.type == type && p.name == name) return true; return false; } void SafeSetTrigger(string trig) { if (HasParameter(trig, AnimatorControllerParameterType.Trigger)) animator.SetTrigger(trig); } void SafeSetBool(string param, bool v) { if (HasParameter(param, AnimatorControllerParameterType.Bool)) animator.SetBool(param, v); } bool IsInOrGoingTo(int shortHash) { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); if (st.shortNameHash == shortHash) return true; if (animator.IsInTransition(baseLayer)) { var nxt = animator.GetNextAnimatorStateInfo(baseLayer); if (nxt.shortNameHash == shortHash) return true; } return false; } // ----------------- Obstacle/Fall flow ----------------- public void OnObstacleHit() { if (waitingForGameOver) return; // Second hit within window → Falling then GameOver if (Time.time - lastObstacleHitTime <= secondHitWindow) { waitingForGameOver = true; //SetMoveSpeed(0); moveSpeed = 0; StartCoroutine(PlayStateAndGameOver(fallingStateName, fallingShortHash)); } else { // First hit → Fall only (no GameOver), then resume run when Fall finishes lastObstacleHitTime = Time.time; PlayStateOnce(fallStateName); originalMoveSpeed = moveSpeed; // remember whatever it was right now //SetMoveSpeed(0f); // optional: pause forward motion during stumble moveSpeed = 0; StartCoroutine(ResumeRunAfter(fallStateName, fallShortHash)); } } void PlayStateOnce(string stateName, float xfade = 0.08f) { if (string.IsNullOrEmpty(stateName) || animator == null) return; animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f); } IEnumerator ResumeRunAfter(string stateName, int shortHash, float xfade = 0.1f) { float t0 = Time.time; while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null; while (animator.IsInTransition(baseLayer)) yield return null; // Wait for completion OR force resume after timeout t0 = Time.time; while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null; if (!waitingForGameOver) { ForceRunStart(ignoreGuards: true); SetMoveSpeed(originalMoveSpeed); animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } IEnumerator PlayStateAndGameOver(string stateName, int shortHash, float xfade = 0.08f) { unableToMove = true; if (string.IsNullOrEmpty(stateName) || animator == null) { if (scoreManager) scoreManager.GameOver(); yield break; } animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f); // Wait to enter target state float t0 = Time.time; while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null; while (animator.IsInTransition(baseLayer)) yield return null; // Wait for it to finish (non-looping recommended) t0 = Time.time; while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null; if (scoreManager) scoreManager.GameOver(); } bool StateFinished(int shortHash) { var st = animator.GetCurrentAnimatorStateInfo(baseLayer); return st.shortNameHash == shortHash && !animator.IsInTransition(baseLayer) && st.normalizedTime >= 1f; } bool TimedOut(float startTime) { return stateWaitTimeout > 0f && (Time.time - startTime) > stateWaitTimeout; } }