MiniGames/Assets/Scripts/ChaseOn/ChasePlayerController.cs
2025-09-15 21:14:32 +05:00

679 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using DG.Tweening;
using UnityEngine;
/// <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))]
public class ChasePlayerController : MonoBehaviour
{
// ===== Movement =====
[Header("Movement")]
public float moveSpeed = 5f;
[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 =====
[Header("Animator Driving")]
[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")]
public string fallStateName = "Fall";
public string fallingStateName = "Falling";
[SerializeField] private float secondHitWindow = 10f;
[SerializeField] private float stateWaitTimeout = 3f;
[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
public static System.Action<float> OnMoveSpeedChanged;
// ----- 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;
private float lastObstacleHitTime = -999f;
[HideInInspector] public bool waitingForGameOver = false;
[SerializeField] private bool validateStatesOnStart = true;
[SerializeField] private string runTag = "Run";
[SerializeField] private string fallTag = "Fall";
[SerializeField] private string fallingTag = "Falling";
public int runShortHash, fallShortHash, fallingShortHash;
private bool hasSpeedFloat, hasIsGroundedBool, hasJumpTrigger, hasLandTrigger;
private float originalMoveSpeed;
private bool unableToMove = false;
// NEW: hard pause flag to block forward steps & threat boost
private bool speedPaused = false;
private ChaseScoreManager scoreManager;
// 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)
private int jumpCounter=0;
public int numOfJumpsAllowed;
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()
{
if (jumpCounter >= numOfJumpsAllowed) return;
jumpCounter++;
jumpPressTime = Time.time;
TryStartJump();
jumpHeld = true;
jumpHoldTimer = 0f;
}
public void OnJumpRelease()
{
jumpHeld = false;
}
// ========= Unity =========
private void Start()
{
// Components
rb = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
col = GetComponent<Collider>();
scoreManager = FindObjectOfType<ChaseScoreManager>();
// Rigidbody setup (smooth & robust)
rb.interpolation = RigidbodyInterpolation.Interpolate;
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
rb.constraints = RigidbodyConstraints.FreezeRotation;
// Animator hashes
runShortHash = Animator.StringToHash(runStateName);
fallShortHash = Animator.StringToHash(fallStateName);
fallingShortHash = Animator.StringToHash(fallingStateName);
if (bombEnemy == null)
{
var bomb = GameObject.FindGameObjectWithTag("BombEnemy");
if (bomb) bombEnemy = bomb.transform;
}
// Validate states (optional)
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");
}
// Cache animator parameters once
hasSpeedFloat = HasParam(speedParamName, AnimatorControllerParameterType.Float);
hasIsGroundedBool = HasParam("IsGrounded", AnimatorControllerParameterType.Bool);
hasJumpTrigger = HasParam("Jump", AnimatorControllerParameterType.Trigger);
hasLandTrigger = HasParam("Land", AnimatorControllerParameterType.Trigger);
// Baselines
baseMoveSpeed = moveSpeed; // IMPORTANT: baseline for threat boost
originalMoveSpeed = moveSpeed;
targetLaneX = LaneToX(currentLane);
// Ensure physics-driven motion (root motion off)
if (animator) animator.applyRootMotion = false;
ForceRunStart();
}
public void SetMoveSpeed(float newSpeed)
{
moveSpeed = newSpeed;
OnMoveSpeedChanged?.Invoke(newSpeed);
}
private void Update()
{
UpdateGrounded();
DriveRunAnimation();
if (!unableToMove)
{
ReadKeyboardInput();
ReadTouchInput();
HandleJumpHold();
}
ApplyBetterJumpGravity();
ClampVerticalSpeed();
ApplyThreatSpeedup();
}
private void ApplyThreatSpeedup()
{
if (!speedUpFromBomb || bombEnemy == null) return;
if (waitingForGameOver || speedPaused) return;
if (baseMoveSpeed <= 0.001f) return; // don't auto-start when baseline is zero
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);
}
private void FixedUpdate()
{
MoveForwardAndSide();
}
// ========= Input =========
private void ReadKeyboardInput()
{
// Lane switching (in-air allowed if enabled)
if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.LeftArrow) && currentLane > 0)
SetLane(currentLane - 1);
if ((isGrounded || allowAirLaneSwitch) && Input.GetKeyDown(KeyCode.RightArrow) && currentLane < 2)
SetLane(currentLane + 1);
// Jump
if (Input.GetKeyDown(KeyCode.Space))
{
jumpPressTime = Time.time;
TryStartJump();
jumpHeld = true;
jumpHoldTimer = 0f;
}
if (Input.GetKeyUp(KeyCode.Space))
{
jumpHeld = false;
}
}
private void ReadTouchInput()
{
if (Input.touchCount != 1) return;
var t = Input.GetTouch(0);
// ===== When jump is routed via UI image =====
if (jumpViaUI)
{
// Well 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) =====
switch (t.phase)
{
case TouchPhase.Began:
startTouchPosition = t.position;
swipeDetected = true;
jumpHeld = true;
jumpHoldTimer = 0f;
jumpPressTime = Time.time;
break;
case TouchPhase.Ended:
if (!swipeDetected) { jumpHeld = false; break; }
Vector2 swipe = t.position - startTouchPosition;
if (swipe.magnitude >= MIN_SWIPE)
{
if (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);
}
else if (swipe.y > 0) TryStartJump();
}
else
{
// Tap
TryStartJump();
}
swipeDetected = false;
jumpHeld = false;
break;
}
}
// ========= 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()
{
// Forward (Z) — blocked when paused
Vector3 forwardStep = speedPaused ? Vector3.zero : Vector3.back * moveSpeed * Time.fixedDeltaTime;
// 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()
{
var v = rb.velocity;
if (v.y > maxVerticalSpeed) v.y = maxVerticalSpeed;
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;
// Buffered jump pressed just before landing
if ((Time.time - jumpPressTime) <= jumpBufferTime)
{
jumpPressTime = -999f;
TryStartJump();
}
}
else
{
isGrounded = false;
if (hasIsGroundedBool) animator.SetBool("IsGrounded", false);
}
}
private bool GroundProbe(out Vector3 hitPoint)
{
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;
}
private void OnCollisionStay(Collision collision)
{
// 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;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGrounded = true;
lastGroundedTime = Time.time;
jumpCounter = 0;
isJumping = false;
if (hasIsGroundedBool) animator.SetBool("IsGrounded", true);
ForceRunStart();
}
}
// ========= Animator =========
private void ForceRunStart(bool ignoreGuards = false)
{
if (!ignoreGuards && (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || waitingForGameOver))
return;
if (useSpeedBlendTree && hasSpeedFloat)
animator.SetFloat(speedParamName, runSpeedParamValue);
else if (!string.IsNullOrEmpty(runStateName))
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
}
private void DriveRunAnimation()
{
if (IsInOrGoingTo(fallShortHash) || IsInOrGoingTo(fallingShortHash) || isJumping) return;
if (useSpeedBlendTree && hasSpeedFloat)
{
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);
}
}
private bool HasParam(string name, AnimatorControllerParameterType type)
{
foreach (var p in animator.parameters)
if (p.type == type && p.name == name) return true;
return false;
}
private 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;
}
// ======= Squish Kill =======
public void KillBySquish(Transform squashTarget = null, float squashTime = 0.25f, float postDelay = 0.35f)
{
//if (waitingForGameOver) return;
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();
});
}
// ========= Obstacle / Fall flow (kept) =========
public void OnObstacleHit()
{
if (waitingForGameOver) return;
Camera.main.DOShakePosition(0.25f, new Vector3(0.15f, 0.15f, 0.15f), 20);
if (Time.time - lastObstacleHitTime <= secondHitWindow)
{
// Deadly second hit → stop everything and go to game over sequence
waitingForGameOver = true;
speedPaused = true;
moveSpeed = 0;
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
StartCoroutine(PlayStateAndGameOver(fallingStateName, fallingShortHash));
}
else
{
// First hit → pause movement, play fall, then resume after
lastObstacleHitTime = Time.time;
speedPaused = true;
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
PlayStateOnce(fallStateName);
// Remember normal run speed as baseline to restore
originalMoveSpeed = baseMoveSpeed;
SetMoveSpeed(0);
StartCoroutine(ResumeRunAfter(fallStateName, fallShortHash));
}
}
private void PlayStateOnce(string stateName, float xfade = 0.08f)
{
if (!string.IsNullOrEmpty(stateName))
animator.CrossFadeInFixedTime(stateName, xfade, baseLayer, 0f);
}
private IEnumerator ResumeRunAfter(string stateName, int shortHash, float xfade = 0.1f)
{
// optional fixed pause before checking state completion
yield return new WaitForSeconds(1.5f);
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 (!waitingForGameOver)
{
// resume
speedPaused = false;
ForceRunStart(ignoreGuards: true);
// restore baseline & current speed
baseMoveSpeed = originalMoveSpeed; // baseline for threat boost
SetMoveSpeed(originalMoveSpeed); // actual run speed now
animator.CrossFadeInFixedTime(runStateName, 0.1f, baseLayer, 0f);
}
}
public 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);
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();
}
private bool StateFinished(int shortHash)
{
var st = animator.GetCurrentAnimatorStateInfo(baseLayer);
return st.shortNameHash == shortHash && !animator.IsInTransition(baseLayer) && st.normalizedTime >= 1f;
}
private bool TimedOut(float startTime) =>
stateWaitTimeout > 0f && (Time.time - startTime) > stateWaitTimeout;
}