Projext/Assets/Scripts/Player/Controller/PlayerMovement.cs
2026-02-03 23:41:49 +09:00

277 lines
11 KiB
C#

using UnityEngine;
using System.Collections;
[RequireComponent(typeof(CharacterController))] // 이 스크립트를 넣으면 CharacterController도 자동으로 추가됨 (실수 방지)
public class PlayerMovement : MonoBehaviour
{
[Header("=== 참조 ===")]
// 외부 스크립트들을 상속 시키기 위한 변수들
// 'private'에 [SerializeField]를 붙여 에디터 인스펙터 창에서 설정 가능
[SerializeField] private Stats stats; // 이동 속도(Stat)를 가져오기 위해 필요
[SerializeField] private PlayerHealth health; // 죽거나 맞았을 때 이동을 멈추기 위해 필요
[SerializeField] private PlayerAnimator pAnim; // 이동에 맞춰 뛰는 모션을 재생하기 위해 필요
[SerializeField] private PlayerAttack attackScript; // 공격 중일 때 이동을 막기 위해 필요
[Header("=== CharacterController ===")]
//CharacterControlle는 유니티가 제공하는 인간형 이동 컴포넌트
// Rigidbody와 달리 경사면, 계단 처리가 자동으로 됨.
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)] // 슬라이더로 조절하게 만듦 (1이면 감속 없음, 0.1이면 엄청 느려짐)
[SerializeField] private float minSpeedMultiplier = 0.3f; // 차징 중일 때 속도를 30%로 줄임
[Header("=== 중력 설정 ===")]
[SerializeField] private float gravity = -20f; // 지구 중력(-9.81)보다 세게 해서 점프 후 빨리 떨어지게 함 (조작감 향상)
[Header("=== 충돌 설정 ===")]
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")]
[SerializeField] private LayerMask weaponLayer; // 무기를 밟고 올라가는 버그를 막기 위해 무기 레이어를 지정
//[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")]
//[SerializeField] private float minGroundHeight = 0.1f; // 땅 보정 시 바닥에 파묻히지 않게 살짝 띄워주는 값
// 내부 상태 변수 (로직 계산용)
private Vector3 _moveDir; // 키보드 입력(WASD)으로 결정된 이동 방향
private bool _isSprinting; // Shift 키를 눌렀는지 여부
private bool _isDashing; // 지금 대시 중인가? (중복 대시 방지)
private float _lastDashTime; // 마지막으로 대시를 쓴 시간 (쿨타임 계산용)
private float _verticalVelocity; // 수직 속도 (중력 가속도 계산용)
// 디버그용 (초기 위치 저장)
private float _initialYPosition;
private void Awake()
{
// 내 몸에 붙어있는 CharacterController를 가져옴
_controller = GetComponent<CharacterController>();
// 만약 없다면 콘솔창에 에러를 띄움
if (_controller == null)
{
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!");
}
_initialYPosition = transform.position.y;
}
private void Start()
{
// 게임 시작 시 딱 한 번 실행: 무기와 플레이어끼리 충돌 끄기
SetupLayerCollision();
}
/// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지)
private void SetupLayerCollision()
{
int playerLayer = gameObject.layer; // 내 레이어 번호 가져오기
// 무기 레이어가 설정되어 있다면
if (weaponLayer != 0)
{
// 비트 연산으로 되어있는 LayerMask에서 숫자 인덱스를 뽑음
int weaponLayerIndex = GetLayerFromMask(weaponLayer);
// 유효한 레이어라면 물리 엔진에게 충돌하지 말라고 명령.
if (weaponLayerIndex >= 0)
{
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true);
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료");
}
}
}
// 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수
private int GetLayerFromMask(LayerMask mask)
{
int layerNumber = 0;
int layer = mask.value;
while (layer > 1)
{
layer = layer >> 1; // 비트를 오른쪽으로 밀면서 횟수를 셉니다.
layerNumber++;
}
return layerNumber;
}
// 외부(InputHandler)에서 키보드 입력을 넣어주는 함수
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;
// 쿨타임 끝남 && 대시 중 아님 && 살아있음 -> true
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. 이동 속도 계산
// Shift 눌렀으면 달리기 속도, 아니면 걷기 속도
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
// 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성)
if (attackScript != null && attackScript.IsCharging)
{
// Lerp를 써서 차징 단계에 따라 부드럽게 감속
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress);
speed *= speedReduction;
}
// 4. 최종 이동 벡터 계산
// 방향 * 속도 * 시간(프레임 보정) = 이번 프레임에 움직일 거리
Vector3 motion = _moveDir * speed * Time.deltaTime;
// 중력 계산 (Y축)
ApplyGravity();
motion.y = _verticalVelocity * Time.deltaTime;
// ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨)
_controller.Move(motion);
// 5. 안전장치 가동 (승천 버그 체크)
//CheckAbnormalHeight();
// 6. 애니메이션 업데이트 (걷기/뛰기 모션)
UpdateAnimation();
}
private void ApplyGravity()
{
if (_controller.isGrounded)
{
// 땅에 닿아있어도 -2f 정도로 계속 눌러줘야 경사면에서 붕 뜨지 않음
_verticalVelocity = -2f;
}
else
{
// 공중에 있다면 중력 가속도 누적 (점점 빨라짐)
_verticalVelocity += gravity * Time.deltaTime;
}
}
// 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용)
private void ApplyGravityOnly()
{
ApplyGravity();
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0));
// CheckAbnormalHeight(); // 넉백되다가 날아가지 않게 체크
}
/// ⭐ 비정상 높이 감지 및 보정
//private void CheckAbnormalHeight()
//{
// // 내 발밑으로 레이저를 쏴서 땅까지의 거리를 잽니다.
// RaycastHit hit;
// if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f))
// {
// float heightAboveGround = transform.position.y - hit.point.y;
// // 땅에서 3미터 이상 떠있는데, 시스템상으론 점프 상태가 아니라면? (버그!)
// if (heightAboveGround > 3f && !_controller.isGrounded)
// {
// // 강제로 땅바닥 위치로 좌표를 계산
// Vector3 correctedPos = transform.position;
// correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight;
// // 순간이동 시킬 땐 CharacterController를 잠시 꺼야 안전함
// _controller.enabled = false;
// transform.position = correctedPos;
// _controller.enabled = true;
// _verticalVelocity = 0f; // 낙하 속도 초기화
// Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함");
// }
// }
//}
private void UpdateAnimation()
{
if (pAnim == null) return;
// 이동 입력이 있으면 1(달리기) 또는 0.5(걷기), 없으면 0(대기)
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f;
// 차징 중이면 무조건 걷기 모션(0.5) 이하로 만듦
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;
// 정해진 시간(0.08초) 동안 반복
while (Time.time < startTime + dashDuration)
{
// 속도 = 거리 / 시간
float speed = dashDistance / dashDuration;
Vector3 dashMotion = dashDir * speed * Time.deltaTime;
// ⭐ transform.position += ... 대신 Move()를 쓰는 이유:
// Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌
CollisionFlags flags = _controller.Move(dashMotion);
yield return null; // 한 프레임 대기
}
// 무적 끄기 및 상태 해제
if (health != null) health.isInvincible = false;
_isDashing = false;
}
// 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들
public bool IsGrounded() => _controller.isGrounded;
public bool IsDashing() => _isDashing;
public float GetCurrentSpeed() => _controller.velocity.magnitude;
}