222 lines
16 KiB
C#
222 lines
16 KiB
C#
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
|
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
|
|
|
[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를 (이 스크립트를 넣으면 자동으로)
|
|
public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerMovement를
|
|
{
|
|
[Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 참조 === 를
|
|
|
|
[SerializeField] private Stats stats; // 변수를 선언하고 인스펙터에 노출할거에요 -> 이동 속도 정보를 가진 Stats 스크립트를
|
|
[SerializeField] private PlayerHealth health; // 변수를 선언하고 인스펙터에 노출할거에요 -> 사망 및 피격 상태를 알기 위한 PlayerHealth를
|
|
[SerializeField] private PlayerAnimator pAnim; // 변수를 선언하고 인스펙터에 노출할거에요 -> 애니메이션을 제어할 PlayerAnimator를
|
|
[SerializeField] private PlayerAttack attackScript; // 변수를 선언하고 인스펙터에 노출할거에요 -> 공격 상태를 알기 위한 PlayerAttack을
|
|
|
|
[Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요 -> === CharacterController === 를
|
|
private CharacterController _controller; // 변수를 선언할거에요 -> 이동을 담당할 CharacterController 컴포넌트를 담을 _controller를
|
|
|
|
[Header("=== 대시 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 대시 설정 === 을
|
|
[SerializeField] private float dashDistance = 3f; // 변수를 선언할거에요 -> 대시로 이동할 거리(3.0)를 dashDistance에
|
|
[SerializeField] private float dashDuration = 0.08f; // 변수를 선언할거에요 -> 대시가 지속될 시간(0.08초)을 dashDuration에
|
|
[SerializeField] private float dashCooldown = 1.5f; // 변수를 선언할거에요 -> 대시 재사용 대기시간(1.5초)을 dashCooldown에
|
|
|
|
[Header("=== 차징 감속 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 차징 감속 설정 === 을
|
|
[Range(0.1f, 1f)] // 슬라이더를 만들거에요 -> 0.1부터 1 사이의 값으로 조절하는
|
|
[SerializeField] private float minSpeedMultiplier = 0.3f; // 변수를 선언할거에요 -> 차징 중 속도 배율(30%)을 minSpeedMultiplier에
|
|
|
|
[Header("=== 중력 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 중력 설정 === 을
|
|
[SerializeField] private float gravity = -20f; // 변수를 선언할거에요 -> 중력 가속도(-20)를 gravity에
|
|
|
|
[Header("=== 충돌 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 충돌 설정 === 을
|
|
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
|
|
[SerializeField] private LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌을 무시할 무기 레이어를 weaponLayer에
|
|
|
|
// 내부 상태 변수 (로직 계산용)
|
|
private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력받은 이동 방향을 저장할 _moveDir을
|
|
private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 중인지 여부를 저장할 _isSprinting을
|
|
private bool _isDashing; // 변수를 선언할거에요 -> 현재 대시 중인지 여부를 저장할 _isDashing을
|
|
private float _lastDashTime; // 변수를 선언할거에요 -> 마지막으로 대시를 쓴 시간을 저장할 _lastDashTime을
|
|
private float _verticalVelocity; // 변수를 선언할거에요 -> 수직(낙하) 속도를 저장할 _verticalVelocity를
|
|
|
|
// 디버그용 (초기 위치 저장)
|
|
private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를 저장할 _initialYPosition을
|
|
|
|
private void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
|
|
{
|
|
_controller = GetComponent<CharacterController>(); // 컴포넌트를 가져와서 저장할거에요 -> 내 몸에 있는 CharacterController를 _controller에
|
|
|
|
if (_controller == null) // 조건이 맞으면 실행할거에요 -> 컨트롤러를 찾지 못했다면
|
|
{
|
|
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러 로그를 띄울거에요 -> 컴포넌트가 필요하다는 메시지를
|
|
}
|
|
|
|
_initialYPosition = transform.position.y; // 값을 저장할거에요 -> 현재의 Y축 위치를 _initialYPosition에
|
|
}
|
|
|
|
private void Start() // 함수를 실행할거에요 -> 첫 프레임 시작 전에 호출되는 Start를
|
|
{
|
|
SetupLayerCollision(); // 함수를 실행할거에요 -> 레이어 충돌 설정을 담당하는 SetupLayerCollision을
|
|
}
|
|
|
|
/// 레이어 충돌 무시 설정 (플레이어 자살 버그 방지)
|
|
private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌을 설정하는 SetupLayerCollision을
|
|
{
|
|
int playerLayer = gameObject.layer; // 값을 가져올거에요 -> 내 게임 오브젝트의 레이어 번호를 playerLayer에
|
|
|
|
if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정되어 있다면(0이 아니라면)
|
|
{
|
|
int weaponLayerIndex = GetLayerFromMask(weaponLayer); // 함수를 실행해서 값을 받을거에요 -> 비트마스크를 정수 인덱스로 변환해서 weaponLayerIndex에
|
|
|
|
if (weaponLayerIndex >= 0) // 조건이 맞으면 실행할거에요 -> 유효한 레이어 인덱스(0 이상)라면
|
|
{
|
|
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true); // 설정을 변경할거에요 -> 플레이어와 무기 레이어 간의 충돌을 무시하도록(true)
|
|
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료"); // 로그를 출력할거에요 -> 충돌 무시 설정이 완료되었다는 메시지를
|
|
}
|
|
}
|
|
}
|
|
|
|
// 비트 마스크(이진수)를 정수 인덱스로 변환하는 수학 함수
|
|
private int GetLayerFromMask(LayerMask mask) // 함수를 선언할거에요 -> 마스크를 인덱스로 바꾸는 GetLayerFromMask를
|
|
{
|
|
int layerNumber = 0; // 변수를 초기화할거에요 -> 레이어 번호를 셀 layerNumber를 0으로
|
|
int layer = mask.value; // 값을 가져올거에요 -> 마스크의 실제 정수값을 layer에
|
|
while (layer > 1) // 반복할거에요 -> layer 값이 1보다 클 때까지
|
|
{
|
|
layer = layer >> 1; // 비트 연산을 할거에요 -> 비트를 오른쪽으로 한 칸 밀어서(나누기 2)
|
|
layerNumber++; // 값을 증가시킬거에요 -> 레이어 번호 카운트를 1만큼
|
|
}
|
|
return layerNumber; // 값을 반환할거에요 -> 계산된 레이어 번호를
|
|
}
|
|
|
|
// 외부(InputHandler)에서 키보드 입력을 넣어주는 함수
|
|
public void SetMoveInput(Vector3 dir, bool sprint) // 함수를 선언할거에요 -> 이동 입력을 받아오는 SetMoveInput을
|
|
{
|
|
_moveDir = dir; // 값을 저장할거에요 -> 입력받은 방향(dir)을 _moveDir에
|
|
_isSprinting = sprint; // 값을 저장할거에요 -> 달리기 여부(sprint)를 _isSprinting에
|
|
}
|
|
|
|
public void AttemptDash() // 함수를 선언할거에요 -> 대시를 시도하는 AttemptDash를
|
|
{
|
|
if (CanDash()) StartCoroutine(DashRoutine()); // 조건이 맞으면 실행할거에요 -> 대시가 가능하다면(CanDash) 대시 코루틴(DashRoutine)을
|
|
}
|
|
|
|
private bool CanDash() // 함수를 선언할거에요 -> 대시 가능 여부를 판단하는 CanDash를
|
|
{
|
|
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown; // 조건을 검사할거에요 -> 현재 시간이 쿨타임 이후인지 확인해서 isCooldownOver에
|
|
bool isAlive = health != null && !health.IsDead; // 조건을 검사할거에요 -> 체력 스크립트가 있고 살아있는지 확인해서 isAlive에
|
|
return isCooldownOver && !_isDashing && isAlive; // 결과를 반환할거에요 -> 쿨타임 끝남, 대시 안 함, 살아있음이 모두 참일 때만 true를
|
|
}
|
|
|
|
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
|
{
|
|
// 1. 행동 불가 상태 체크 (가장 먼저 해서 불필요한 연산 방지)
|
|
if (health != null && (health.IsDead || health.isHit || _isDashing)) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있고 (죽었거나, 맞았거나, 대시 중이라면)
|
|
{
|
|
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있고 죽지 않았다면 이동 모션을 0(정지)으로
|
|
ApplyGravityOnly(); // 함수를 실행할거에요 -> 이동은 안 해도 중력은 적용하는 ApplyGravityOnly를
|
|
return; // 중단할거에요 -> 더 이상 움직임 코드를 실행하지 않도록 함수를
|
|
}
|
|
|
|
// 2. 공격 중 이동 차단
|
|
if (attackScript != null && attackScript.IsAttacking) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 공격 중이라면
|
|
{
|
|
if (pAnim != null) pAnim.UpdateMove(0f); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 이동 모션을 0(정지)으로
|
|
ApplyGravityOnly(); // 함수를 실행할거에요 -> 제자리에서 중력만 받는 ApplyGravityOnly를
|
|
return; // 중단할거에요 -> 이동을 막기 위해 함수를
|
|
}
|
|
|
|
// 3. 이동 속도 계산
|
|
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; // 값을 결정할거에요 -> 달리기 중이면 런 스피드를, 아니면 이동 스피드를 speed에
|
|
|
|
// 공격 차징 중이라면 속도를 느리게 만듦 (긴장감 조성)
|
|
if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 공격 스크립트가 있고 차징 중이라면
|
|
{
|
|
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); // 값을 계산할거에요 -> 차징 진행도에 따라 속도 배율을 줄여서 speedReduction에
|
|
speed *= speedReduction; // 값을 곱할거에요 -> 현재 속도(speed)에 감속 배율을
|
|
}
|
|
|
|
// 4. 최종 이동 벡터 계산
|
|
Vector3 motion = _moveDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 방향 * 속도 * 시간을 곱해서 이번 프레임 이동량을 motion에
|
|
|
|
// 중력 계산 (Y축)
|
|
ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
|
|
motion.y = _verticalVelocity * Time.deltaTime; // 값을 넣을거에요 -> 수직 이동량(속도 * 시간)을 motion의 y값에
|
|
|
|
// ⭐ 실제 이동 실행 (여기서 벽 충돌 처리가 자동 수행됨)
|
|
_controller.Move(motion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 motion 벡터만큼
|
|
|
|
// 6. 애니메이션 업데이트 (걷기/뛰기 모션)
|
|
UpdateAnimation(); // 함수를 실행할거에요 -> 이동 애니메이션을 갱신하는 UpdateAnimation을
|
|
}
|
|
|
|
private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산하는 ApplyGravity를
|
|
{
|
|
if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 캐릭터가 땅에 닿아있다면
|
|
{
|
|
_verticalVelocity = -2f; // 값을 설정할거에요 -> 바닥에 딱 붙어있도록 약한 하향 속도(-2)를 _verticalVelocity에
|
|
}
|
|
else // 조건이 틀리면 실행할거에요 -> 공중에 떠 있다면
|
|
{
|
|
_verticalVelocity += gravity * Time.deltaTime; // 값을 더할거에요 -> 중력 가속도를 시간에 맞춰 _verticalVelocity에
|
|
}
|
|
}
|
|
|
|
// 키보드 이동 없이 중력만 적용하는 함수 (피격/공격 중 사용)
|
|
private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력만 적용해서 움직이는 ApplyGravityOnly를
|
|
{
|
|
ApplyGravity(); // 함수를 실행할거에요 -> 수직 속도를 계산하는 ApplyGravity를
|
|
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0)); // 이동시킬거에요 -> 수직 방향으로만 캐릭터를
|
|
}
|
|
|
|
private void UpdateAnimation() // 함수를 선언할거에요 -> 애니메이션 파라미터를 조절하는 UpdateAnimation을
|
|
{
|
|
if (pAnim == null) return; // 조건이 맞으면 중단할거에요 -> 애니메이터 스크립트가 없다면
|
|
|
|
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; // 값을 결정할거에요 -> 움직임이 있으면 (달리기면 1.0, 걷기면 0.5), 없으면 0을 animVal에
|
|
|
|
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; // 조건이 맞으면 실행할거에요 -> 차징 중이라면 애니메이션 속도를 절반으로 줄여서
|
|
|
|
pAnim.UpdateMove(animVal); // 함수를 실행할거에요 -> 계산된 애니메이션 값(animVal)을 전달하는 UpdateMove를
|
|
}
|
|
|
|
// 대시 관련
|
|
private IEnumerator DashRoutine() // 코루틴 함수를 선언할거에요 -> 대시 로직을 수행할 DashRoutine을
|
|
{
|
|
_isDashing = true; // 상태를 바꿀거에요 -> 대시 중 상태를 참(true)으로
|
|
_lastDashTime = Time.time; // 값을 저장할거에요 -> 현재 시간을 마지막 대시 시간으로
|
|
|
|
// ⭐ [추가됨] 행동 트래커에 대쉬 기록 알림
|
|
if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 트래커가 존재한다면
|
|
{
|
|
PlayerBehaviorTracker.Instance.RecordDodge(); // 실행할거에요 -> 회피 기록 함수를
|
|
}
|
|
|
|
// 이동 중이면 그 방향으로, 멈춰있으면 뒤로 회피(백스탭)
|
|
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward; // 벡터를 결정할거에요 -> 이동 중이면 이동 방향, 아니면 뒤쪽 방향을 dashDir에
|
|
|
|
// 무적 판정 켜기 (소울류 게임 회피 느낌)
|
|
if (health != null) health.isInvincible = true; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 켜기(true)로
|
|
|
|
float startTime = Time.time; // 값을 저장할거에요 -> 대시 시작 시간을 startTime에
|
|
|
|
while (Time.time < startTime + dashDuration) // 반복할거에요 -> 현재 시간이 (시작시간 + 대시지속시간)보다 작을 동안
|
|
{
|
|
float speed = dashDistance / dashDuration; // 값을 계산할거에요 -> 거리 나누기 시간으로 대시 속도를 구해서 speed에
|
|
Vector3 dashMotion = dashDir * speed * Time.deltaTime; // 벡터를 계산할거에요 -> 대시 방향 * 속도 * 시간을 곱해 이동량을 dashMotion에
|
|
|
|
// ⭐ transform.position += ... 대신 Move()를 쓰는 이유:
|
|
// Move를 써야 대시 도중 벽을 만나면 뚫지 않고 멈줌
|
|
CollisionFlags flags = _controller.Move(dashMotion); // 이동시킬거에요 -> 캐릭터 컨트롤러를 대시 이동량만큼
|
|
|
|
yield return null; // 대기할거에요 -> 다음 프레임까지 한 턴을
|
|
}
|
|
|
|
// 무적 끄기 및 상태 해제
|
|
if (health != null) health.isInvincible = false; // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면 무적 상태를 끄기(false)로
|
|
_isDashing = false; // 상태를 바꿀거에요 -> 대시 중 상태를 거짓(false)으로
|
|
}
|
|
|
|
// 다른 스크립트에서 상태를 물어볼 때 쓰는 함수들
|
|
public bool IsGrounded() => _controller.isGrounded; // 값을 반환할거에요 -> 캐릭터가 땅에 닿아있는지 여부를
|
|
public bool IsDashing() => _isDashing; // 값을 반환할거에요 -> 현재 대시 중인지 여부를
|
|
public float GetCurrentSpeed() => _controller.velocity.magnitude; // 값을 반환할거에요 -> 현재 캐릭터의 실제 이동 속도 크기를
|
|
} |