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; [Header("Animator Driving")] [Tooltip("If true, sets a float parameter (e.g., 'Speed') each frame. If false, crossfades to a run state by name.")] public bool useSpeedBlendTree = true; [Tooltip("Animator float parameter name for the locomotion blend tree.")] public string speedParamName = "Speed"; [Tooltip("Value to push while running (match your thresholds: 1=Run, 2=RunFast, 3=Sprint).")] public float runSpeedParamValue = 2f; [Tooltip("Fallback run/locomotion state name if not using a Speed parameter.")] public string runStateName = "Locomotion"; [Tooltip("Base layer index for the states above.")] public int baseLayer = 0; [Header("Fall / GameOver Logic")] public string fallStateName = "Fall"; public string fallingStateName = "Falling"; public float secondHitWindow = 10f; public float stateWaitTimeout = 3f; // ------------------------------------------------- int currentLane = 1; Rigidbody rb; Animator animator; bool isGrounded = true; bool isJumping = false; Vector3 targetLanePosition; // touch swipe Vector2 startTouchPosition; bool swipeDetected = false; float minSwipeDistance = 50f; // fall tracking float lastObstacleHitTime = -999f; bool waitingForGameOver = false; // state hashes int runShortHash, fallShortHash, fallingShortHash; [SerializeField] bool validateStatesOnStart = true; [SerializeField] string runTag = "Run"; [SerializeField] string fallTag = "Fall"; [SerializeField] string fallingTag = "Falling"; // NEW: remember original forward speed so we can restore after stumble float originalMoveSpeed; ChaseScoreManager scoreManager; void Start() { rb = GetComponent(); animator = GetComponent(); scoreManager = FindObjectOfType(); targetLanePosition = transform.position; // cache hashes runShortHash = Animator.StringToHash(runStateName); fallShortHash = Animator.StringToHash(fallStateName); fallingShortHash = Animator.StringToHash(fallingStateName); if (validateStatesOnStart) { if (!animator.HasState(baseLayer, runShortHash)) Debug.LogError($"[ChasePlayerController] Run state '{runStateName}' not found on layer {baseLayer}"); if (!animator.HasState(baseLayer, fallShortHash)) Debug.LogError($"[ChasePlayerController] Fall state '{fallStateName}' not found on layer {baseLayer}"); if (!animator.HasState(baseLayer, fallingShortHash)) Debug.LogError($"[ChasePlayerController] Falling state '{fallingStateName}' not found on layer {baseLayer}"); } originalMoveSpeed = moveSpeed; // NEW ForceRunStart(); // autorunner } void Update() { DriveRunAnimation(); HandleInput(); HandleSwipe(); UpdateLaneTarget(); } void FixedUpdate() { MoveForward(); SmoothLaneSwitch(); } // ----------------- Input ----------------- void HandleInput() { if (isJumping) return; if (Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0) currentLane--; if (Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2) currentLane++; 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) { if (isJumping) { swipeDetected = false; return; } if (Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y)) { if (swipe.x > 0 && currentLane < 2) currentLane++; else if (swipe.x < 0 && currentLane > 0) currentLane--; } else { if (swipe.y > 0 && isGrounded) Jump(); } } swipeDetected = false; break; } } // ----------------- Movement ----------------- void UpdateLaneTarget() { if (isJumping) return; float targetX = (currentLane - 1) * laneDistance; targetLanePosition = new Vector3(targetX, rb.position.y, rb.position.z); } void SmoothLaneSwitch() { if (isJumping) return; Vector3 newPos = Vector3.Lerp(rb.position, targetLanePosition, laneSwitchSpeed * Time.fixedDeltaTime); rb.MovePosition(newPos); } 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).SetDelay(0.2f) .OnStart(() => { SafeSetTrigger("Jump"); SafeSetBool("IsGrounded", false); }) .OnComplete(() => { rb.useGravity = true; isGrounded = true; isJumping = false; SafeSetTrigger("Land"); // if present SafeSetBool("IsGrounded", true); ForceRunStart(); }); } void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = true; SafeSetBool("IsGrounded", true); ForceRunStart(); } } // ----------------- Animator driving ----------------- // CHANGED: can bypass guards when 'ignoreGuards' is true void ForceRunStart(bool ignoreGuards = false) { if (!ignoreGuards) { if (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); return; } 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; 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 moveSpeed = 0f; // optional: pause forward motion during stumble 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); moveSpeed = originalMoveSpeed; animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f); } } IEnumerator PlayStateAndGameOver(string stateName, int shortHash, float xfade = 0.08f) { 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; } }