Projext/Assets/02_Scripts/Player/Controller/PlayerMovement.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

386 lines
25 KiB
C#

using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
[RequireComponent(typeof(CharacterController))] // 컴포넌트를 강제로 추가할거에요 -> CharacterController를
public class PlayerMovement : MonoBehaviour // 클래스를 선언할거에요 -> PlayerMovement를
{
[Header("=== 참조 ===")] // 인스펙터 창에 제목을 표시할거에요 -> 참조를
[SerializeField] private Stats stats; // 변수를 선언할거에요 -> 이동 속도 Stats를
[SerializeField] private PlayerHealth health; // 변수를 선언할거에요 -> 체력/피격 상태 PlayerHealth를
[SerializeField] private PlayerAnimator pAnim; // 변수를 선언할거에요 -> 애니메이션 제어 PlayerAnimator를
[SerializeField] private PlayerAttack attackScript; // 변수를 선언할거에요 -> 공격 상태 PlayerAttack를
[SerializeField] private PlayerSoundFX soundFX; // 변수를 선언할거에요 -> 사운드 스크립트를
[Header("=== CharacterController ===")] // 인스펙터 창에 제목을 표시할거에요
private CharacterController _controller; // 변수를 선언할거에요 -> 이동 담당 CharacterController를
[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 LayerMask weaponLayer; // 변수를 선언할거에요 -> 충돌 무시할 무기 레이어를
// ─────────────────────────────────────────────────────────
// ⭐ 경직 시스템
// ─────────────────────────────────────────────────────────
[Header("=== 경직 설정 ===")] // 인스펙터 창에 제목을 표시할거에요 -> 경직 설정을
[Tooltip("돌진 몬스터 피격 시 경직 지속 시간 (초)")]
[SerializeField] private float staggerDuration = 0.8f; // 변수를 선언할거에요 -> 경직 지속 시간을 (Inspector에서 조절 가능)
private bool _isStaggered = false; // 변수를 선언할거에요 -> 현재 경직 상태인지 여부를
private Coroutine _staggerCoroutine = null; // 변수를 선언할거에요 -> 경직 코루틴 핸들을 (중복 방지용)
public bool IsStaggered => _isStaggered; // 프로퍼티를 선언할거에요 -> 경직 상태를 외부에서 읽을 수 있도록
public float StaggerDuration => staggerDuration; // 프로퍼티를 선언할거에요 -> 경직 지속 시간을 외부에서 읽을 수 있도록
[SerializeField] private StaggerIndicator staggerIndicator; // 변수를 선언할거에요 -> 경직 UI 연동용을
// ─────────────────────────────────────────────────────────
private Vector3 _moveDir; // 변수를 선언할거에요 -> 입력 이동 방향을
private bool _isSprinting; // 변수를 선언할거에요 -> 달리기 여부를
private bool _isDashing; // 변수를 선언할거에요 -> 대시 여부를
private float _lastDashTime; // 변수를 선언할거에요 -> 마지막 대시 시간을
private float _verticalVelocity; // 변수를 선언할거에요 -> 수직 낙하 속도를
// ── 넉백 전용 ──────────────────────────────────────────────────
// CharacterController는 Rigidbody가 없어서 AddForce가 불가해요.
// BossIronBall이 ApplyKnockback()을 호출하면 여기에 수평 속도를 저장하고
// ApplyGravityOnly() 안에서 매 프레임 _controller.Move()에 합산해요.
private Vector3 _knockbackVelocity = Vector3.zero; // 변수를 선언할거에요 -> 현재 넉백 수평 속도를 (ApplyGravityOnly에서 합산됨)
[Header("=== 애니메이션 보간 ===")] // 인스펙터 창에 제목을 표시할거에요
[SerializeField] private float animSmoothSpeed = 8f; // 변수를 선언할거에요 -> 애니메이션 보간 속도를
private float _currentAnimVal = 0f; // 변수를 초기화할거에요 -> 현재 보간 애니메이션 값을
private float _initialYPosition; // 변수를 선언할거에요 -> 시작 높이를
private void Awake() // 함수를 실행할거에요 -> 초기화를
{
_controller = GetComponent<CharacterController>(); // 가져올거에요 -> CharacterController를
if (_controller == null) // 조건이 맞으면 실행할거에요 -> 없으면
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!"); // 에러를 출력할거에요
_initialYPosition = transform.position.y; // 저장할거에요 -> 초기 Y 위치를
}
private void Start() // 함수를 실행할거에요 -> 시작 시
{
SetupLayerCollision(); // 실행할거에요 -> 레이어 충돌 설정을
}
// ─────────────────────────────────────────────────────────
// ⭐ 경직 적용 함수 — ChargeMonster 무기에서 호출
//
// 사용법:
// hit.GetComponent<PlayerMovement>()?.ApplyStagger();
// 또는 duration 직접 지정:
// hit.GetComponent<PlayerMovement>()?.ApplyStagger(1.2f);
// ─────────────────────────────────────────────────────────
public void ApplyStagger(float duration = -1f) // 함수를 선언할거에요 -> 외부에서 경직을 걸 때 호출할
{
float actualDuration = duration > 0f ? duration : staggerDuration; // 결정할거에요 -> 인자가 있으면 그 값, 없으면 Inspector 기본값을
if (_staggerCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 경직 중이면
StopCoroutine(_staggerCoroutine); // 멈출거에요 -> 이전 경직 코루틴을 (덮어쓰기)
_staggerCoroutine = StartCoroutine(StaggerRoutine(actualDuration)); // 시작할거에요 -> 새 경직 코루틴을
if (staggerIndicator != null) staggerIndicator.NotifyStagger(actualDuration); // 알릴거에요 -> UI에 경직 시작을
}
private IEnumerator StaggerRoutine(float duration) // 코루틴을 정의할거에요 -> 경직 지속 로직을
{
_isStaggered = true; // 설정할거에요 -> 경직 상태를 true로
if (pAnim != null) pAnim.UpdateMove(0f); // 멈출거에요 -> 이동 애니메이션을
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 경직 시간만큼
_isStaggered = false; // 해제할거에요 -> 경직 상태를
_staggerCoroutine = null; // 초기화할거에요 -> 코루틴 핸들을
}
// ──────────────────────────────────────────────────────────────
// ⭐ 넉백 시스템 — BossIronBall 등 무거운 충격에서 호출
//
// 사용법 (BossIronBall 등):
// Vector3 dir = (playerRoot.position - ballPos).normalized;
// dir.y = 0f; // 수평 방향만 (y는 아래에서 따로 처리)
// playerMovement.ApplyKnockback(dir, 8f, 2f, 0.5f);
//
// 내부 동작:
// ① _knockbackVelocity 에 초기 수평 속도 저장
// ② _verticalVelocity 에 위로 튀기는 초기값 저장
// ③ _isStaggered = true → Update() 일반 이동 차단
// ④ ApplyGravityOnly() 가 매 프레임 _knockbackVelocity + 중력 합산
// ⑤ KnockbackRoutine 이 수평 속도를 duration 동안 감속 후 해제
// ──────────────────────────────────────────────────────────────
/// <summary>
/// 넉백을 적용해요. CharacterController 기반 플레이어 전용.
/// </summary>
/// <param name="direction">수평 방향 (Y=0 권장, Normalize 안 해도 내부에서 처리)</param>
/// <param name="horizontalForce">수평 날아가는 세기 (m/s). 추천: 6~10</param>
/// <param name="upwardForce">위로 튀기는 세기 (m/s). 추천: 2~4. 0이면 수평만</param>
/// <param name="duration">넉백 + 경직 지속 시간 (초). 추천: 0.4~0.7</param>
public void ApplyKnockback(Vector3 direction, float horizontalForce, float upwardForce = 2f, float duration = 0.5f) // 함수를 선언할거에요 -> 외부에서 넉백을 줄 때 호출할
{
if (_staggerCoroutine != null) // 조건이 맞으면 실행할거에요 -> 이미 경직/넉백 중이면
StopCoroutine(_staggerCoroutine); // 멈출거에요 -> 이전 코루틴을 (새 넉백으로 덮어쓰기)
Vector3 horizontal = new Vector3(direction.x, 0f, direction.z).normalized; // 계산할거에요 -> 수평 방향 벡터를 (Y성분 제거 후 정규화)
_knockbackVelocity = horizontal * horizontalForce; // 저장할거에요 -> 수평 넉백 초기 속도를
_verticalVelocity = upwardForce; // 저장할거에요 -> 위로 튀기는 초기 수직 속도를
_staggerCoroutine = StartCoroutine(KnockbackRoutine(horizontal, horizontalForce, duration)); // 시작할거에요 -> 넉백 감속 코루틴을
if (staggerIndicator != null) staggerIndicator.NotifyStagger(duration); // 알릴거에요 -> UI에 경직 시작을
}
private IEnumerator KnockbackRoutine(Vector3 dir, float initialForce, float duration) // 코루틴을 정의할거에요 -> 넉백 수평 속도 감속 로직을
{
_isStaggered = true; // 설정할거에요 -> 경직 상태를 (Update 일반 이동 차단)
if (pAnim != null) pAnim.UpdateMove(0f); // 멈출거에요 -> 이동 애니메이션을
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
while (elapsed < duration) // 반복할거에요 -> 넉백 지속 시간 동안
{
float t = elapsed / duration; // 계산할거에요 -> 0→1 진행 비율을
_knockbackVelocity = dir * Mathf.Lerp(initialForce, 0f, t); // 줄일거에요 -> 수평 속도를 (처음엔 빠름, 끝엔 0)
elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
yield return null; // 기다릴거에요 -> 다음 프레임까지
}
_knockbackVelocity = Vector3.zero; // 초기화할거에요 -> 넉백 속도를 완전히 0으로
_isStaggered = false; // 해제할거에요 -> 경직 상태를
_staggerCoroutine = null; // 초기화할거에요 -> 코루틴 핸들을
}
// ──────────────────────────────────────────────────────────────
// ⭐ 흡인 효과 — 보스 스매시 공격 시 플레이어를 공격 범위로 끌어당겨요
//
// 사용법 (보스 코루틴에서 매 프레임 호출):
// playerMovement.ApplySuction(smashCenter, 3f);
//
// 내부 동작:
// ① 공격 중심 → 플레이어 방향의 수평 벡터 계산
// ② pullSpeed * deltaTime 만큼 CharacterController.Move()로 이동
// ③ 플레이어 자신의 이동과 합산되어 "저항하며 끌려가는" 느낌 연출
// ④ 경직/넉백 중이면 흡인 무시 (넉백이 우선)
// ⑤ 이미 중심보다 가까우면 흡인하지 않음 (중심 통과 방지)
// ──────────────────────────────────────────────────────────────
/// <summary>
/// 흡인 효과를 적용해요. 매 프레임 보스 코루틴에서 호출하세요.
/// CharacterController.Move()를 추가 호출하므로 일반 이동과 합산됩니다.
/// </summary>
/// <param name="pullCenter">끌어당길 목표 지점 (보스 공격 판정 중심)</param>
/// <param name="pullSpeed">초당 끌어당기는 속도 (m/s). 추천: 2~5</param>
/// <param name="minDistance">이 거리 이내면 흡인 중단 (중심 통과 방지). 추천: 0.5</param>
public void ApplySuction(Vector3 pullCenter, float pullSpeed, float minDistance = 0.5f) // 함수를 선언할거에요 -> 외부에서 흡인을 줄 때 호출할
{
if (_controller == null) return; // 중단할거에요 -> 컨트롤러 없으면
if (_isStaggered) return; // 중단할거에요 -> 넉백/경직 중이면 (넉백이 흡인보다 우선)
Vector3 toCenter = pullCenter - transform.position; // 계산할거에요 -> 공격 중심까지의 벡터를
toCenter.y = 0f; // 제거할거에요 -> 수직 성분을 (수평 이동만)
float dist = toCenter.magnitude; // 계산할거에요 -> 공격 중심까지의 수평 거리를
if (dist < minDistance) return; // 중단할거에요 -> 이미 충분히 가까우면 (중심 통과 방지)
Vector3 pullDir = toCenter.normalized; // 정규화할거에요 -> 끌어당길 방향을 (중심 방향)
// 거리가 가까울수록 흡인력이 강해지는 비선형 보간 (역거리 가중)
// → 멀리 있으면 살짝 끌리고, 가까이 오면 강하게 빨려드는 느낌
float distFactor = Mathf.Clamp01(1f - (dist / 10f)); // 계산할거에요 -> 거리 비율을 (10m 기준, 가까울수록 1에 가까움)
float adjustedSpeed = pullSpeed * (0.3f + 0.7f * distFactor); // 계산할거에요 -> 최종 흡인 속도를 (기본 30% + 거리 보정 70%)
Vector3 suctionMotion = pullDir * adjustedSpeed * Time.deltaTime; // 계산할거에요 -> 이번 프레임 흡인 이동량을
_controller.Move(suctionMotion); // 이동할거에요 -> 공격 중심 방향으로 끌어당기기
}
private void SetupLayerCollision() // 함수를 선언할거에요 -> 레이어 충돌 설정을
{
int playerLayer = gameObject.layer; // 가져올거에요 -> 내 레이어 번호를
if (weaponLayer != 0) // 조건이 맞으면 실행할거에요 -> 무기 레이어가 설정됐으면
{
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; // 초기화할거에요 -> 레이어 번호를 0으로
int layer = mask.value; // 가져올거에요 -> 마스크 정수값을
while (layer > 1) // 반복할거에요 -> 1보다 클 동안
{
layer = layer >> 1; // 밀거에요 -> 비트를 오른쪽으로
layerNumber++; // 증가할거에요 -> 레이어 번호를
}
return layerNumber; // 반환할거에요 -> 계산된 레이어 번호를
}
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; // 반환할거에요 -> 모두 만족할 때만 true를
}
private void Update() // 함수를 실행할거에요 -> 매 프레임마다
{
// 1. 행동 불가 상태 체크
// ⭐ _isStaggered 추가 — 경직 중에도 이동 차단
if (health != null && (health.IsDead || health.isHit || _isDashing || _isStaggered)) // 조건이 맞으면 실행할거에요 -> 죽었거나, 피격이거나, 대시 중이거나, 경직 중이면
{
// ⭐ 경직 중 공격 강제 취소 — 차징 포함
if (_isStaggered && attackScript != null) // 조건이 맞으면 실행할거에요 -> 경직 상태이고 공격 스크립트가 있으면
{
attackScript.CancelCharging(); // 취소할거에요 -> 차징을
attackScript.IsAttacking = false; // 해제할거에요 -> 공격 상태를
}
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. 최종 이동 벡터 계산
Vector3 motion = _moveDir * speed * Time.deltaTime; // 계산할거에요 -> 이번 프레임 이동량을
ApplyGravity(); // 계산할거에요 -> 수직 속도를
motion.y = _verticalVelocity * Time.deltaTime; // 넣을거에요 -> 수직 이동량을
_controller.Move(motion); // 이동할거에요 -> 캐릭터 컨트롤러를
UpdateAnimation(); // 갱신할거에요 -> 이동 애니메이션을
}
private void ApplyGravity() // 함수를 선언할거에요 -> 중력을 계산할
{
if (_controller.isGrounded) // 조건이 맞으면 실행할거에요 -> 땅에 닿아있으면
_verticalVelocity = -2f; // 설정할거에요 -> 바닥 고정 속도를
else // 조건이 틀리면 실행할거에요 -> 공중이면
_verticalVelocity += gravity * Time.deltaTime; // 더할거에요 -> 중력 가속도를
}
private void ApplyGravityOnly() // 함수를 선언할거에요 -> 중력(+넉백)을 적용할
{
ApplyGravity(); // 계산할거에요 -> 수직 속도를 (중력 누적)
// ⭐ 넉백 수평 속도도 합산해요.
// _knockbackVelocity 가 Vector3.zero 이면 기존 동작과 완전히 동일해요.
Vector3 motion = new Vector3( // 만들거에요 -> 합산 이동 벡터를
_knockbackVelocity.x, // 수평 X: 넉백 속도
_verticalVelocity, // 수직 Y: 중력 속도
_knockbackVelocity.z // 수평 Z: 넉백 속도
) * Time.deltaTime; // 프레임 속도 보정
_controller.Move(motion); // 이동할거에요 -> 넉백 + 중력 합산 벡터로
}
private void UpdateAnimation() // 함수를 선언할거에요 -> 이동 애니메이션을 갱신할
{
if (pAnim == null || stats == null) return; // 중단할거에요 -> 필수 참조 없으면
Vector3 horizontalVel = _controller.velocity; // 가져올거에요 -> 현재 속도를
horizontalVel.y = 0f; // 무시할거에요 -> 수직 속도를
float actualSpeed = horizontalVel.magnitude; // 계산할거에요 -> 수평 속도 크기를
float walkSpeed = stats.CurrentMoveSpeed; // 가져올거에요 -> 걷기 속도를
float runSpeed = stats.CurrentRunSpeed; // 가져올거에요 -> 달리기 속도를
float targetAnimVal = 0f; // 초기화할거에요 -> 목표 애니메이션 값을
if (actualSpeed > 0.1f) // 조건이 맞으면 실행할거에요 -> 움직이고 있으면
{
if (actualSpeed <= walkSpeed) // 조건이 맞으면 실행할거에요 -> 걷기 이하면
targetAnimVal = Mathf.Lerp(0f, 0.5f, actualSpeed / walkSpeed); // 보간할거에요 -> 0~0.5로
else // 조건이 틀리면 실행할거에요 -> 달리기면
targetAnimVal = Mathf.Lerp(0.5f, 1.0f, (actualSpeed - walkSpeed) / (runSpeed - walkSpeed)); // 보간할거에요 -> 0.5~1.0으로
}
if (attackScript != null && attackScript.IsCharging) // 조건이 맞으면 실행할거에요 -> 차징 중이면
targetAnimVal *= 0.5f; // 줄일거에요 -> 절반으로
_currentAnimVal = Mathf.Lerp(_currentAnimVal, targetAnimVal, Time.deltaTime * animSmoothSpeed); // 보간할거에요 -> 부드럽게
pAnim.UpdateMove(_currentAnimVal); // 전달할거에요 -> 보간된 애니메이션 값을
}
private IEnumerator DashRoutine() // 코루틴을 정의할거에요 -> 대시 로직을
{
_isDashing = true; // 설정할거에요 -> 대시 상태를
_lastDashTime = Time.time; // 저장할거에요 -> 대시 시작 시간을
if (soundFX != null) soundFX.PlayDash(); // 재생할거에요 -> 대시 소리를
if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 트래커가 있으면
PlayerBehaviorTracker.Instance.RecordDodge(); // 기록할거에요 -> 회피를
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; // 계산할거에요 -> 이동량을
_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; // 반환할거에요 -> 현재 이동 속도를
}