2025-08-14 20:29:09 +05:00
|
|
|
|
using System.Collections;
|
2025-09-04 17:08:49 +05:00
|
|
|
|
using DG.Tweening;
|
2025-07-30 01:38:12 +05:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Endless runner controller:
|
|
|
|
|
/// - Forward motion (−Z), 3 lanes with smooth switching (ground & air)
|
|
|
|
|
/// - Tap = small jump, Hold (within window) = higher jump
|
|
|
|
|
/// - Coyote time + jump buffer for forgiving inputs
|
|
|
|
|
/// - Robust ground detection (collider-based sphere cast with optional probe)
|
|
|
|
|
/// - Animator driving preserved; obstacle/fall flow preserved
|
|
|
|
|
/// </summary>
|
|
|
|
|
[RequireComponent(typeof(Rigidbody))]
|
2025-07-30 01:38:12 +05:00
|
|
|
|
public class ChasePlayerController : MonoBehaviour
|
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ===== Movement =====
|
2025-08-14 20:29:09 +05:00
|
|
|
|
[Header("Movement")]
|
2025-07-30 01:38:12 +05:00
|
|
|
|
public float moveSpeed = 5f;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
[SerializeField] private float laneDistance = 2.5f; // 0=Left,1=Mid,2=Right
|
|
|
|
|
[SerializeField] private float laneSwitchSpeed = 10f; // units/sec
|
|
|
|
|
|
|
|
|
|
// ===== Jump (Tap / Hold) =====
|
|
|
|
|
[Header("Jump (Tap/Hold)")]
|
|
|
|
|
[SerializeField] private float maxHoldTime = 0.25f; // hold window
|
|
|
|
|
[SerializeField] private float initialJumpVelocity = 6.5f;
|
|
|
|
|
[SerializeField] private float holdUpwardAcceleration = 35f;
|
|
|
|
|
[SerializeField] private float lowJumpGravityMultiplier = 2.0f;
|
|
|
|
|
[SerializeField] private float fallGravityMultiplier = 2.5f;
|
|
|
|
|
[SerializeField] private float coyoteTime = 0.12f; // after leaving ground
|
|
|
|
|
[SerializeField] private float jumpBufferTime = 0.12f; // before landing
|
|
|
|
|
[SerializeField] private float maxVerticalSpeed = 15f;
|
|
|
|
|
|
|
|
|
|
[Header("Air Control")]
|
|
|
|
|
[SerializeField] private bool allowAirLaneSwitch = true;
|
|
|
|
|
|
|
|
|
|
// ===== Ground Check =====
|
|
|
|
|
[Header("Ground Check")]
|
|
|
|
|
[Tooltip("Optional feet probe; if null, uses collider bounds.")]
|
|
|
|
|
[SerializeField] private Transform groundProbe;
|
|
|
|
|
[SerializeField] private LayerMask groundMask = ~0;
|
|
|
|
|
[SerializeField] private float groundCheckRadius = 0.22f;
|
|
|
|
|
[SerializeField] private float groundCheckExtraDown = 0.06f;
|
|
|
|
|
|
|
|
|
|
// ===== Animator Driving =====
|
2025-08-14 20:29:09 +05:00
|
|
|
|
[Header("Animator Driving")]
|
2025-09-04 16:21:53 +05:00
|
|
|
|
[SerializeField] private bool useSpeedBlendTree = true;
|
|
|
|
|
[SerializeField] private string speedParamName = "Speed";
|
|
|
|
|
[SerializeField] private float runSpeedParamValue = 2f;
|
|
|
|
|
[SerializeField] private string runStateName = "Locomotion";
|
|
|
|
|
[SerializeField] private int baseLayer = 0;
|
|
|
|
|
|
|
|
|
|
// ===== Fall / GameOver =====
|
|
|
|
|
[Header("Fall / GameOver")]
|
2025-08-14 20:29:09 +05:00
|
|
|
|
public string fallStateName = "Fall";
|
|
|
|
|
public string fallingStateName = "Falling";
|
2025-09-04 16:21:53 +05:00
|
|
|
|
[SerializeField] private float secondHitWindow = 10f;
|
|
|
|
|
[SerializeField] private float stateWaitTimeout = 3f;
|
|
|
|
|
|
2025-09-04 17:01:33 +05:00
|
|
|
|
[Header("Threat-Based Speedup")]
|
|
|
|
|
[SerializeField] private bool speedUpFromBomb = true;
|
|
|
|
|
[SerializeField] private Transform bombEnemy; // assign in Inspector or auto-find by tag "BombEnemy"
|
|
|
|
|
[SerializeField] private float boostDistance = 12f; // within this range -> up to max boost
|
|
|
|
|
[SerializeField] private float maxSpeedBoost = 4f; // extra speed added at zero distance
|
|
|
|
|
[SerializeField] private float boostLerp = 5f; // smoothing (higher = snappier)
|
|
|
|
|
|
|
|
|
|
private float baseMoveSpeed; // remembers your normal run speed
|
|
|
|
|
|
2025-08-18 17:52:40 +05:00
|
|
|
|
public static System.Action<float> OnMoveSpeedChanged;
|
2025-07-30 01:38:12 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ----- runtime state -----
|
|
|
|
|
private int currentLane = 1; // 0,1,2
|
|
|
|
|
private float targetLaneX = 0f;
|
|
|
|
|
|
|
|
|
|
private Rigidbody rb;
|
|
|
|
|
private Animator animator;
|
|
|
|
|
private Collider col; // any non-trigger collider
|
|
|
|
|
|
|
|
|
|
private bool isGrounded = true;
|
|
|
|
|
private bool isJumping = false;
|
|
|
|
|
private float lastGroundedTime = -999f;
|
|
|
|
|
private float jumpPressTime = -999f;
|
|
|
|
|
private float jumpHoldTimer = 0f;
|
|
|
|
|
private bool jumpHeld = false;
|
|
|
|
|
|
|
|
|
|
private Vector2 startTouchPosition;
|
|
|
|
|
private bool swipeDetected = false;
|
|
|
|
|
private const float MIN_SWIPE = 50f;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private float lastObstacleHitTime = -999f;
|
|
|
|
|
[HideInInspector] public bool waitingForGameOver = false;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
[SerializeField] private bool validateStatesOnStart = true;
|
|
|
|
|
[SerializeField] private string runTag = "Run";
|
|
|
|
|
[SerializeField] private string fallTag = "Fall";
|
|
|
|
|
[SerializeField] private string fallingTag = "Falling";
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
public int runShortHash, fallShortHash, fallingShortHash;
|
|
|
|
|
private bool hasSpeedFloat, hasIsGroundedBool, hasJumpTrigger, hasLandTrigger;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private float originalMoveSpeed;
|
|
|
|
|
private bool unableToMove = false;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// NEW: hard pause flag to block forward steps & threat boost
|
|
|
|
|
private bool speedPaused = false;
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private ChaseScoreManager scoreManager;
|
2025-09-12 10:08:01 +05:00
|
|
|
|
// Add near your other fields:
|
|
|
|
|
[SerializeField] private bool jumpViaUI = false;
|
|
|
|
|
[SerializeField] private RectTransform jumpImageRect; // <-- drag your Image here
|
|
|
|
|
[SerializeField] private Canvas jumpImageCanvas; // optional; drag the Canvas (for camera)
|
2025-09-15 21:14:32 +05:00
|
|
|
|
private int jumpCounter=0;
|
|
|
|
|
public int numOfJumpsAllowed;
|
2025-09-12 10:08:01 +05:00
|
|
|
|
private bool TouchBeganOnJumpImage(Vector2 screenPos)
|
|
|
|
|
{
|
|
|
|
|
if (jumpImageRect == null) return false;
|
|
|
|
|
var cam = jumpImageCanvas ? jumpImageCanvas.worldCamera : null;
|
|
|
|
|
return RectTransformUtility.RectangleContainsScreenPoint(jumpImageRect, screenPos, cam);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add these public methods anywhere in the class:
|
|
|
|
|
public void OnJumpPress()
|
|
|
|
|
{
|
2025-09-15 21:14:32 +05:00
|
|
|
|
if (jumpCounter >= numOfJumpsAllowed) return;
|
|
|
|
|
jumpCounter++;
|
2025-09-12 10:08:01 +05:00
|
|
|
|
jumpPressTime = Time.time;
|
|
|
|
|
TryStartJump();
|
|
|
|
|
jumpHeld = true;
|
|
|
|
|
jumpHoldTimer = 0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void OnJumpRelease()
|
|
|
|
|
{
|
|
|
|
|
jumpHeld = false;
|
|
|
|
|
}
|
2025-08-06 19:14:48 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ========= Unity =========
|
|
|
|
|
private void Start()
|
2025-07-30 01:38:12 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Components
|
2025-07-30 01:38:12 +05:00
|
|
|
|
rb = GetComponent<Rigidbody>();
|
|
|
|
|
animator = GetComponent<Animator>();
|
2025-09-04 16:21:53 +05:00
|
|
|
|
col = GetComponent<Collider>();
|
2025-08-14 20:29:09 +05:00
|
|
|
|
scoreManager = FindObjectOfType<ChaseScoreManager>();
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Rigidbody setup (smooth & robust)
|
|
|
|
|
rb.interpolation = RigidbodyInterpolation.Interpolate;
|
|
|
|
|
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
|
|
|
|
rb.constraints = RigidbodyConstraints.FreezeRotation;
|
|
|
|
|
|
|
|
|
|
// Animator hashes
|
2025-09-11 18:46:20 +05:00
|
|
|
|
runShortHash = Animator.StringToHash(runStateName);
|
|
|
|
|
fallShortHash = Animator.StringToHash(fallStateName);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
fallingShortHash = Animator.StringToHash(fallingStateName);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
2025-09-04 17:01:33 +05:00
|
|
|
|
if (bombEnemy == null)
|
|
|
|
|
{
|
|
|
|
|
var bomb = GameObject.FindGameObjectWithTag("BombEnemy");
|
|
|
|
|
if (bomb) bombEnemy = bomb.transform;
|
|
|
|
|
}
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Validate states (optional)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
if (validateStatesOnStart)
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (!animator.HasState(baseLayer, runShortHash)) Debug.LogError($"Run state '{runStateName}' not found");
|
|
|
|
|
if (!animator.HasState(baseLayer, fallShortHash)) Debug.LogError($"Fall state '{fallStateName}' not found");
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (!animator.HasState(baseLayer, fallingShortHash)) Debug.LogError($"Falling state '{fallingStateName}' not found");
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Cache animator parameters once
|
2025-09-11 18:46:20 +05:00
|
|
|
|
hasSpeedFloat = HasParam(speedParamName, AnimatorControllerParameterType.Float);
|
2025-09-04 16:21:53 +05:00
|
|
|
|
hasIsGroundedBool = HasParam("IsGrounded", AnimatorControllerParameterType.Bool);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
hasJumpTrigger = HasParam("Jump", AnimatorControllerParameterType.Trigger);
|
|
|
|
|
hasLandTrigger = HasParam("Land", AnimatorControllerParameterType.Trigger);
|
2025-09-04 16:21:53 +05:00
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Baselines
|
|
|
|
|
baseMoveSpeed = moveSpeed; // IMPORTANT: baseline for threat boost
|
2025-08-18 22:48:45 +05:00
|
|
|
|
originalMoveSpeed = moveSpeed;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
targetLaneX = LaneToX(currentLane);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
// Ensure physics-driven motion (root motion off)
|
|
|
|
|
if (animator) animator.applyRootMotion = false;
|
|
|
|
|
|
2025-08-18 22:48:45 +05:00
|
|
|
|
ForceRunStart();
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
2025-08-18 22:48:45 +05:00
|
|
|
|
|
2025-08-18 17:52:40 +05:00
|
|
|
|
public void SetMoveSpeed(float newSpeed)
|
|
|
|
|
{
|
|
|
|
|
moveSpeed = newSpeed;
|
|
|
|
|
OnMoveSpeedChanged?.Invoke(newSpeed);
|
|
|
|
|
}
|
2025-08-18 22:48:45 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void Update()
|
2025-07-30 01:38:12 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
UpdateGrounded();
|
2025-08-14 20:29:09 +05:00
|
|
|
|
DriveRunAnimation();
|
2025-08-18 17:52:40 +05:00
|
|
|
|
|
2025-08-18 22:48:45 +05:00
|
|
|
|
if (!unableToMove)
|
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
ReadKeyboardInput();
|
|
|
|
|
ReadTouchInput();
|
|
|
|
|
HandleJumpHold();
|
2025-08-18 17:52:40 +05:00
|
|
|
|
}
|
2025-09-04 16:21:53 +05:00
|
|
|
|
|
|
|
|
|
ApplyBetterJumpGravity();
|
|
|
|
|
ClampVerticalSpeed();
|
2025-09-04 17:01:33 +05:00
|
|
|
|
ApplyThreatSpeedup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyThreatSpeedup()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (!speedUpFromBomb || bombEnemy == null) return;
|
|
|
|
|
if (waitingForGameOver || speedPaused) return;
|
|
|
|
|
if (baseMoveSpeed <= 0.001f) return; // don't auto-start when baseline is zero
|
2025-09-04 17:01:33 +05:00
|
|
|
|
|
|
|
|
|
float dist = Vector3.Distance(transform.position, bombEnemy.position);
|
|
|
|
|
// 0 when far, 1 when enemy is on top of us
|
|
|
|
|
float t = Mathf.Clamp01((boostDistance - dist) / boostDistance);
|
|
|
|
|
|
|
|
|
|
float target = baseMoveSpeed + maxSpeedBoost * t;
|
|
|
|
|
// Smoothly approach the target; no event fire here so enemy doesn't copy this boost
|
|
|
|
|
moveSpeed = Mathf.Lerp(moveSpeed, target, Time.deltaTime * boostLerp);
|
2025-08-07 19:23:38 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void FixedUpdate()
|
2025-08-07 19:23:38 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
MoveForwardAndSide();
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ========= Input =========
|
|
|
|
|
private void ReadKeyboardInput()
|
2025-07-30 01:38:12 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Lane switching (in-air allowed if enabled)
|
|
|
|
|
if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0)
|
|
|
|
|
SetLane(currentLane - 1);
|
2025-07-30 01:38:12 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2)
|
|
|
|
|
SetLane(currentLane + 1);
|
|
|
|
|
|
|
|
|
|
// Jump
|
|
|
|
|
if (Input.GetKeyDown(KeyCode.Space))
|
2025-08-18 22:48:45 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
jumpPressTime = Time.time;
|
|
|
|
|
TryStartJump();
|
|
|
|
|
jumpHeld = true;
|
|
|
|
|
jumpHoldTimer = 0f;
|
2025-08-18 22:48:45 +05:00
|
|
|
|
}
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (Input.GetKeyUp(KeyCode.Space))
|
2025-08-18 22:48:45 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
jumpHeld = false;
|
2025-08-18 22:48:45 +05:00
|
|
|
|
}
|
2025-08-06 19:14:48 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void ReadTouchInput()
|
2025-08-06 19:14:48 +05:00
|
|
|
|
{
|
2025-08-14 20:29:09 +05:00
|
|
|
|
if (Input.touchCount != 1) return;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
var t = Input.GetTouch(0);
|
2025-08-06 19:14:48 +05:00
|
|
|
|
|
2025-09-12 10:08:01 +05:00
|
|
|
|
// ===== When jump is routed via UI image =====
|
|
|
|
|
if (jumpViaUI)
|
|
|
|
|
{
|
|
|
|
|
// We’ll only use touches that START outside the jump image for lane swipes.
|
|
|
|
|
// Touches that start on the image are ignored here (the Image script calls OnJumpPress/OnJumpRelease).
|
|
|
|
|
switch (t.phase)
|
|
|
|
|
{
|
|
|
|
|
case TouchPhase.Began:
|
|
|
|
|
startTouchPosition = t.position;
|
|
|
|
|
swipeDetected = !TouchBeganOnJumpImage(t.position);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case TouchPhase.Ended:
|
|
|
|
|
if (!swipeDetected) break; // began on image -> no swipe
|
|
|
|
|
|
|
|
|
|
Vector2 swipe = t.position - startTouchPosition;
|
|
|
|
|
|
|
|
|
|
if (swipe.magnitude >= MIN_SWIPE && Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y))
|
|
|
|
|
{
|
|
|
|
|
if ((isGrounded || allowAirLaneSwitch) && swipe.x > 0 && currentLane < 2)
|
|
|
|
|
SetLane(currentLane + 1);
|
|
|
|
|
else if ((isGrounded || allowAirLaneSwitch) && swipe.x < 0 && currentLane > 0)
|
|
|
|
|
SetLane(currentLane - 1);
|
|
|
|
|
}
|
|
|
|
|
swipeDetected = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return; // don't fall through to the non-UI path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Original touch logic (no UI jump) =====
|
2025-09-04 16:21:53 +05:00
|
|
|
|
switch (t.phase)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
case TouchPhase.Began:
|
2025-09-04 16:21:53 +05:00
|
|
|
|
startTouchPosition = t.position;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
swipeDetected = true;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
jumpHeld = true;
|
|
|
|
|
jumpHoldTimer = 0f;
|
|
|
|
|
jumpPressTime = Time.time;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
break;
|
2025-08-18 22:48:45 +05:00
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
case TouchPhase.Ended:
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (!swipeDetected) { jumpHeld = false; break; }
|
|
|
|
|
Vector2 swipe = t.position - startTouchPosition;
|
2025-08-07 19:23:38 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (swipe.magnitude >= MIN_SWIPE)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
if (Mathf.Abs(swipe.x) > Mathf.Abs(swipe.y))
|
2025-08-06 19:14:48 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if ((isGrounded || allowAirLaneSwitch) && swipe.x > 0 && currentLane < 2) SetLane(currentLane + 1);
|
|
|
|
|
else if ((isGrounded || allowAirLaneSwitch) && swipe.x < 0 && currentLane > 0) SetLane(currentLane - 1);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|
2025-09-04 16:21:53 +05:00
|
|
|
|
else if (swipe.y > 0) TryStartJump();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Tap
|
|
|
|
|
TryStartJump();
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|
2025-08-18 22:48:45 +05:00
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
swipeDetected = false;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
jumpHeld = false;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
break;
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ========= Movement =========
|
|
|
|
|
private float LaneToX(int laneIndex) => (laneIndex - 1) * laneDistance;
|
|
|
|
|
|
|
|
|
|
private void SetLane(int laneIndex)
|
|
|
|
|
{
|
|
|
|
|
currentLane = Mathf.Clamp(laneIndex, 0, 2);
|
|
|
|
|
targetLaneX = LaneToX(currentLane);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void MoveForwardAndSide()
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Forward (−Z) — blocked when paused
|
|
|
|
|
Vector3 forwardStep = speedPaused ? Vector3.zero : Vector3.back * moveSpeed * Time.fixedDeltaTime;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
|
|
|
|
|
// Side toward target lane X
|
|
|
|
|
float newX = Mathf.MoveTowards(rb.position.x, targetLaneX, laneSwitchSpeed * Time.fixedDeltaTime);
|
|
|
|
|
Vector3 targetPos = new Vector3(newX, rb.position.y, rb.position.z) + forwardStep;
|
|
|
|
|
|
|
|
|
|
rb.MovePosition(targetPos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========= Jump System =========
|
|
|
|
|
private void TryStartJump()
|
|
|
|
|
{
|
|
|
|
|
bool canCoyote = (Time.time - lastGroundedTime) <= coyoteTime;
|
|
|
|
|
|
|
|
|
|
if ((isGrounded || canCoyote) && !waitingForGameOver)
|
|
|
|
|
{
|
|
|
|
|
var v = rb.velocity;
|
|
|
|
|
v.y = initialJumpVelocity;
|
|
|
|
|
rb.velocity = v;
|
|
|
|
|
|
|
|
|
|
isJumping = true;
|
|
|
|
|
isGrounded = false;
|
|
|
|
|
jumpPressTime = -999f; // consume buffer
|
|
|
|
|
|
|
|
|
|
if (hasJumpTrigger) animator.SetTrigger("Jump");
|
|
|
|
|
if (hasIsGroundedBool) animator.SetBool("IsGrounded", false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleJumpHold()
|
|
|
|
|
{
|
|
|
|
|
if (!isJumping || !jumpHeld) return;
|
|
|
|
|
|
|
|
|
|
if (jumpHoldTimer < maxHoldTime && rb.velocity.y > 0f)
|
|
|
|
|
{
|
|
|
|
|
rb.AddForce(Vector3.up * holdUpwardAcceleration, ForceMode.Acceleration);
|
|
|
|
|
jumpHoldTimer += Time.deltaTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyBetterJumpGravity()
|
|
|
|
|
{
|
|
|
|
|
if (rb == null) return;
|
|
|
|
|
|
|
|
|
|
if (rb.velocity.y < 0f)
|
|
|
|
|
{
|
|
|
|
|
rb.AddForce(Physics.gravity * (fallGravityMultiplier - 1f), ForceMode.Acceleration);
|
|
|
|
|
}
|
|
|
|
|
else if (!jumpHeld && rb.velocity.y > 0f)
|
|
|
|
|
{
|
|
|
|
|
rb.AddForce(Physics.gravity * (lowJumpGravityMultiplier - 1f), ForceMode.Acceleration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ClampVerticalSpeed()
|
2025-08-06 19:14:48 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
var v = rb.velocity;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
if (v.y > maxVerticalSpeed) v.y = maxVerticalSpeed;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (v.y < -maxVerticalSpeed) v.y = -maxVerticalSpeed;
|
|
|
|
|
rb.velocity = v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========= Grounding =========
|
|
|
|
|
private void UpdateGrounded()
|
|
|
|
|
{
|
|
|
|
|
bool groundedNow = GroundProbe(out _);
|
|
|
|
|
|
|
|
|
|
if (groundedNow)
|
|
|
|
|
{
|
|
|
|
|
if (!isGrounded)
|
|
|
|
|
{
|
|
|
|
|
if (hasLandTrigger) animator.SetTrigger("Land");
|
|
|
|
|
if (hasIsGroundedBool) animator.SetBool("IsGrounded", true);
|
|
|
|
|
isJumping = false;
|
|
|
|
|
ForceRunStart();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isGrounded = true;
|
|
|
|
|
lastGroundedTime = Time.time;
|
2025-08-06 19:14:48 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Buffered jump pressed just before landing
|
|
|
|
|
if ((Time.time - jumpPressTime) <= jumpBufferTime)
|
|
|
|
|
{
|
|
|
|
|
jumpPressTime = -999f;
|
|
|
|
|
TryStartJump();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
isGrounded = false;
|
|
|
|
|
if (hasIsGroundedBool) animator.SetBool("IsGrounded", false);
|
|
|
|
|
}
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private bool GroundProbe(out Vector3 hitPoint)
|
2025-07-30 01:38:12 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
hitPoint = transform.position;
|
|
|
|
|
|
|
|
|
|
if (col != null)
|
|
|
|
|
{
|
|
|
|
|
Bounds b = col.bounds;
|
|
|
|
|
Vector3 feet = new Vector3(b.center.x, b.min.y + 0.02f, b.center.z);
|
|
|
|
|
Vector3 origin = groundProbe ? groundProbe.position : feet;
|
|
|
|
|
|
|
|
|
|
// SphereCast small distance down
|
|
|
|
|
if (Physics.SphereCast(origin, groundCheckRadius, Vector3.down, out RaycastHit hit,
|
|
|
|
|
groundCheckExtraDown + 0.02f, groundMask, QueryTriggerInteraction.Ignore))
|
|
|
|
|
{
|
|
|
|
|
hitPoint = hit.point;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback overlap
|
|
|
|
|
if (Physics.CheckSphere(origin + Vector3.down * groundCheckExtraDown, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore))
|
|
|
|
|
{
|
|
|
|
|
hitPoint = origin + Vector3.down * groundCheckExtraDown;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No collider → simple overlap at probe or near feet
|
|
|
|
|
Vector3 o = groundProbe ? groundProbe.position : (transform.position + Vector3.down * 0.95f);
|
|
|
|
|
bool ok = Physics.CheckSphere(o, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore);
|
|
|
|
|
if (ok) hitPoint = o;
|
|
|
|
|
return ok;
|
2025-08-07 19:23:38 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void OnCollisionStay(Collision collision)
|
2025-08-07 19:23:38 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// Extra safety: contact with ground layer & low vertical speed => grounded
|
|
|
|
|
if (((1 << collision.gameObject.layer) & groundMask) != 0 && Mathf.Abs(rb.velocity.y) < 0.1f)
|
|
|
|
|
{
|
|
|
|
|
isGrounded = true;
|
|
|
|
|
lastGroundedTime = Time.time;
|
|
|
|
|
}
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void OnCollisionEnter(Collision collision)
|
2025-07-30 01:38:12 +05:00
|
|
|
|
{
|
|
|
|
|
if (collision.gameObject.CompareTag("Ground"))
|
|
|
|
|
{
|
|
|
|
|
isGrounded = true;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
lastGroundedTime = Time.time;
|
2025-09-15 21:14:32 +05:00
|
|
|
|
jumpCounter = 0;
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
isJumping = false;
|
|
|
|
|
if (hasIsGroundedBool) animator.SetBool("IsGrounded", true);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
ForceRunStart();
|
2025-07-30 01:38:12 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-07 19:23:38 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ========= Animator =========
|
|
|
|
|
private void ForceRunStart(bool ignoreGuards = false)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
2025-08-18 22:48:45 +05:00
|
|
|
|
if (!ignoreGuards && (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || waitingForGameOver))
|
|
|
|
|
return;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (useSpeedBlendTree && hasSpeedFloat)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
animator.SetFloat(speedParamName, runSpeedParamValue);
|
|
|
|
|
else if (!string.IsNullOrEmpty(runStateName))
|
|
|
|
|
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void DriveRunAnimation()
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || isJumping) return;
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (useSpeedBlendTree && hasSpeedFloat)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
animator.SetFloat(speedParamName, runSpeedParamValue, 0.1f, Time.deltaTime);
|
|
|
|
|
}
|
2025-08-18 22:48:45 +05:00
|
|
|
|
else
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
var st = animator.GetCurrentAnimatorStateInfo(baseLayer);
|
2025-08-18 22:48:45 +05:00
|
|
|
|
var nxt = animator.GetNextAnimatorStateInfo(baseLayer);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
bool inRunNow = st.shortNameHash == runShortHash || st.IsTag(runTag);
|
2025-08-18 22:48:45 +05:00
|
|
|
|
bool goingToRun = animator.IsInTransition(baseLayer) && (nxt.shortNameHash == runShortHash || nxt.IsTag(runTag));
|
|
|
|
|
|
|
|
|
|
if (!isJumping && !inRunNow && !goingToRun)
|
|
|
|
|
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private bool HasParam(string name, AnimatorControllerParameterType type)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
foreach (var p in animator.parameters)
|
|
|
|
|
if (p.type == type && p.name == name) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private bool IsInOrGoingTo(int shortHash)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// ======= Squish Kill =======
|
|
|
|
|
public void KillBySquish(Transform squashTarget = null, float squashTime = 0.25f, float postDelay = 0.35f)
|
|
|
|
|
{
|
2025-09-15 21:14:32 +05:00
|
|
|
|
//if (waitingForGameOver) return;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
waitingForGameOver = true;
|
|
|
|
|
unableToMove = true;
|
|
|
|
|
speedPaused = true;
|
|
|
|
|
|
|
|
|
|
// Halt motion & physics jank
|
|
|
|
|
if (rb)
|
|
|
|
|
{
|
|
|
|
|
rb.velocity = Vector3.zero;
|
|
|
|
|
rb.angularVelocity = Vector3.zero;
|
|
|
|
|
rb.isKinematic = true; // lock out further physics bumps
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional: stop lane movement immediately
|
|
|
|
|
SetMoveSpeed(0);
|
|
|
|
|
|
|
|
|
|
// Optional: stop player colliders from re-triggering anything else
|
|
|
|
|
var allCols = GetComponentsInChildren<Collider>();
|
|
|
|
|
for (int i = 0; i < allCols.Length; i++) allCols[i].enabled = false;
|
|
|
|
|
|
|
|
|
|
// Target to squash (player root if not provided)
|
|
|
|
|
Transform t = squashTarget ? squashTarget : transform;
|
|
|
|
|
|
|
|
|
|
// Preserve original scale to avoid double-squish if something else calls us
|
|
|
|
|
Vector3 start = t.localScale;
|
|
|
|
|
|
|
|
|
|
// A little “pancake spread” feels nice; tweak if you want pure Y=0 only
|
|
|
|
|
float widen = 1.1f;
|
|
|
|
|
Sequence seq = DOTween.Sequence();
|
|
|
|
|
seq.Append(t.DOScaleY(0f, squashTime).SetEase(Ease.InCubic))
|
|
|
|
|
.Join(t.DOScaleX(start.x * widen, squashTime))
|
|
|
|
|
.Join(t.DOScaleZ(start.z * widen, squashTime))
|
|
|
|
|
.AppendInterval(postDelay)
|
|
|
|
|
.OnComplete(() =>
|
|
|
|
|
{
|
|
|
|
|
if (scoreManager) scoreManager.GameOver();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
// ========= Obstacle / Fall flow (kept) =========
|
2025-08-14 20:29:09 +05:00
|
|
|
|
public void OnObstacleHit()
|
|
|
|
|
{
|
|
|
|
|
if (waitingForGameOver) return;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
Camera.main.DOShakePosition(0.25f, new Vector3(0.15f, 0.15f, 0.15f), 20);
|
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
if (Time.time - lastObstacleHitTime <= secondHitWindow)
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// Deadly second hit → stop everything and go to game over sequence
|
2025-08-14 20:29:09 +05:00
|
|
|
|
waitingForGameOver = true;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
speedPaused = true;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
moveSpeed = 0;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
|
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
StartCoroutine(PlayStateAndGameOver(fallingStateName, fallingShortHash));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// First hit → pause movement, play fall, then resume after
|
2025-08-14 20:29:09 +05:00
|
|
|
|
lastObstacleHitTime = Time.time;
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
speedPaused = true;
|
|
|
|
|
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
|
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
PlayStateOnce(fallStateName);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
// Remember normal run speed as baseline to restore
|
|
|
|
|
originalMoveSpeed = baseMoveSpeed;
|
|
|
|
|
|
|
|
|
|
SetMoveSpeed(0);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
StartCoroutine(ResumeRunAfter(fallStateName, fallShortHash));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private void PlayStateOnce(string stateName, float xfade = 0.08f)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
2025-09-04 16:21:53 +05:00
|
|
|
|
if (!string.IsNullOrEmpty(stateName))
|
|
|
|
|
animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f);
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private IEnumerator ResumeRunAfter(string stateName, int shortHash, float xfade = 0.1f)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// optional fixed pause before checking state completion
|
|
|
|
|
yield return new WaitForSeconds(1.5f);
|
|
|
|
|
|
2025-08-14 20:29:09 +05:00
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null;
|
|
|
|
|
while (animator.IsInTransition(baseLayer)) yield return null;
|
|
|
|
|
|
|
|
|
|
t0 = Time.time;
|
2025-09-04 16:21:53 +05:00
|
|
|
|
while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
|
|
|
|
|
if (!waitingForGameOver)
|
|
|
|
|
{
|
2025-09-11 18:46:20 +05:00
|
|
|
|
// resume
|
|
|
|
|
speedPaused = false;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
ForceRunStart(ignoreGuards: true);
|
2025-09-11 18:46:20 +05:00
|
|
|
|
|
|
|
|
|
// restore baseline & current speed
|
|
|
|
|
baseMoveSpeed = originalMoveSpeed; // baseline for threat boost
|
|
|
|
|
SetMoveSpeed(originalMoveSpeed); // actual run speed now
|
2025-08-14 20:29:09 +05:00
|
|
|
|
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-04 16:21:53 +05:00
|
|
|
|
|
2025-08-19 16:59:43 +05:00
|
|
|
|
public IEnumerator PlayStateAndGameOver(string stateName, int shortHash, float xfade = 0.08f)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
2025-08-18 17:52:40 +05:00
|
|
|
|
unableToMove = true;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
if (string.IsNullOrEmpty(stateName) || animator == null)
|
|
|
|
|
{
|
|
|
|
|
if (scoreManager) scoreManager.GameOver();
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f);
|
|
|
|
|
|
|
|
|
|
float t0 = Time.time;
|
|
|
|
|
while (!IsInOrGoingTo(shortHash) && !TimedOut(t0)) yield return null;
|
|
|
|
|
while (animator.IsInTransition(baseLayer)) yield return null;
|
|
|
|
|
|
|
|
|
|
t0 = Time.time;
|
|
|
|
|
while (!StateFinished(shortHash) && !TimedOut(t0)) yield return null;
|
|
|
|
|
|
|
|
|
|
if (scoreManager) scoreManager.GameOver();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private bool StateFinished(int shortHash)
|
2025-08-14 20:29:09 +05:00
|
|
|
|
{
|
|
|
|
|
var st = animator.GetCurrentAnimatorStateInfo(baseLayer);
|
|
|
|
|
return st.shortNameHash == shortHash && !animator.IsInTransition(baseLayer) && st.normalizedTime >= 1f;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 16:21:53 +05:00
|
|
|
|
private bool TimedOut(float startTime) =>
|
|
|
|
|
stateWaitTimeout > 0f && (Time.time - startTime) > stateWaitTimeout;
|
2025-08-14 20:29:09 +05:00
|
|
|
|
}
|