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