Projext/Assets/Scripts/Player/Controller/PlayerMovement.cs
2026-02-02 19:56:06 +09:00

226 lines
7.6 KiB
C#

using UnityEngine;
using System.Collections;
/// <summary>
/// CharacterController 기반 플레이어 이동 (완전판)
/// - 벽 뚫림 방지
/// - 물리 오브젝트 밀기
/// - 중력 처리
/// - 대시 충돌 감지
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
[Header("=== 참조 ===")]
[SerializeField] private Stats stats;
[SerializeField] private PlayerHealth health;
[SerializeField] private PlayerAnimator pAnim;
[SerializeField] private PlayerAttack attackScript;
[Header("=== CharacterController ===")]
private CharacterController _controller;
[Header("=== 대시 설정 ===")]
[SerializeField] private float dashDistance = 3f;
[SerializeField] private float dashDuration = 0.08f;
[SerializeField] private float dashCooldown = 1.5f;
[Header("=== 차징 감속 설정 ===")]
[Range(0.1f, 1f)]
[SerializeField] private float minSpeedMultiplier = 0.3f;
[Header("=== 중력 설정 ===")]
[SerializeField] private float gravity = -20f;
[Header("=== 물리 상호작용 설정 ===")]
[Tooltip("일반 이동 시 물체 밀기 힘")]
[SerializeField] private float pushPower = 2f;
[Tooltip("대시 중 물체 밀기 힘 (더 강함)")]
[SerializeField] private float dashPushPower = 5f;
[Tooltip("밀 수 있는 최대 질량 (kg)")]
[SerializeField] private float maxPushMass = 10f;
// 이동 상태
private Vector3 _moveDir;
private bool _isSprinting;
private bool _isDashing;
private float _lastDashTime;
private float _verticalVelocity;
private void Awake()
{
_controller = GetComponent<CharacterController>();
if (_controller == null)
{
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!");
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 입력 처리
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public void SetMoveInput(Vector3 dir, bool sprint)
{
_moveDir = dir;
_isSprinting = sprint;
}
public void AttemptDash()
{
if (CanDash()) StartCoroutine(DashRoutine());
}
private bool CanDash()
{
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown;
bool isAlive = health != null && !health.IsDead;
return isCooldownOver && !_isDashing && isAlive;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 메인 이동
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
private void Update()
{
// 1. 사망/대시/피격 중 이동 차단
if (health != null && (health.IsDead || health.isHit || _isDashing))
{
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f);
ApplyGravityOnly();
return;
}
// 2. 공격 중 이동 차단
if (attackScript != null && attackScript.IsAttacking)
{
if (pAnim != null) pAnim.UpdateMove(0f);
ApplyGravityOnly();
return;
}
// 3. 이동 속도 계산
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
if (attackScript != null && attackScript.IsCharging)
{
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress);
speed *= speedReduction;
}
// 4. ✅ CharacterController로 이동 (벽 충돌 처리!)
Vector3 motion = _moveDir * speed * Time.deltaTime;
ApplyGravity();
motion.y = _verticalVelocity * Time.deltaTime;
_controller.Move(motion);
// 5. 애니메이션
UpdateAnimation();
}
private void ApplyGravity()
{
if (_controller.isGrounded)
{
_verticalVelocity = -2f;
}
else
{
_verticalVelocity += gravity * Time.deltaTime;
}
}
private void ApplyGravityOnly()
{
ApplyGravity();
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0));
}
private void UpdateAnimation()
{
if (pAnim == null) return;
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f;
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f;
pAnim.UpdateMove(animVal);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 대시
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
private IEnumerator DashRoutine()
{
_isDashing = true;
_lastDashTime = Time.time;
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward;
if (health != null) health.isInvincible = true;
float startTime = Time.time;
while (Time.time < startTime + dashDuration)
{
float speed = dashDistance / dashDuration;
Vector3 dashMotion = dashDir * speed * Time.deltaTime;
// ✅ 벽 충돌 감지
CollisionFlags flags = _controller.Move(dashMotion);
// 벽에 부딪히면 대시 조기 종료 (선택사항)
// if ((flags & CollisionFlags.Sides) != 0) break;
yield return null;
}
if (health != null) health.isInvincible = false;
_isDashing = false;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ⭐ 물리 상호작용 (핵심!)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/// <summary>
/// CharacterController가 물체와 충돌할 때 호출됨
/// </summary>
private void OnControllerColliderHit(ControllerColliderHit hit)
{
// Rigidbody 확인
Rigidbody body = hit.collider.attachedRigidbody;
// Rigidbody 없거나 Kinematic이면 밀 수 없음
if (body == null || body.isKinematic) return;
// 너무 무거운 물체는 밀 수 없음
if (body.mass > maxPushMass) return;
// 바닥/천장 충돌은 무시
if (hit.moveDirection.y < -0.3f || hit.moveDirection.y > 0.3f) return;
// 💪 밀기 방향 계산
Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
pushDir.Normalize();
// 대시 중이면 더 강한 힘
float power = _isDashing ? dashPushPower : pushPower;
// ✅ 힘 적용!
body.AddForce(pushDir * power, ForceMode.Impulse);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 유틸리티
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public bool IsGrounded() => _controller.isGrounded;
public bool IsDashing() => _isDashing;
public float GetCurrentSpeed() => _controller.velocity.magnitude;
}