367 lines
11 KiB
C#
367 lines
11 KiB
C#
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<float> 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;
|
|
public bool waitingForGameOver = false;
|
|
|
|
int runShortHash, fallShortHash;
|
|
public int 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<Rigidbody>();
|
|
animator = GetComponent<Animator>();
|
|
scoreManager = FindObjectOfType<ChaseScoreManager>();
|
|
|
|
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);
|
|
}
|
|
}
|
|
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);
|
|
|
|
// 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;
|
|
}
|
|
}
|