Rigidbody 기반 이동을 CharacterController + New Input System으로 바꾸고,
달리기와 이단 점프까지 구현한 과정을 기록합니다.
1. Rigidbody 제거 → CharacterController 추가
왜 바꾸나?
- Rigidbody : 물리 엔진이 움직임을 알아서 제어 (중력, 충돌 반응 자동)
- CharacterController : 물리 없이 내가 직접 코드로 움직임을 제어
CharacterController는 예측 가능한 움직임을 만들 수 있다. 대신 중력 같은 건 직접 코딩해야 한다.
Inspector에서 한 작업
- Player 오브젝트에서 Rigidbody 컴포넌트 제거
- Capsule Collider 컴포넌트 제거 (CharacterController가 자체 콜라이더를 포함하므로)
- CharacterController 컴포넌트 추가

CharacterController 설정값
- Center: X 0, Y 1, Z 0
- Radius: 0.5
- Height: 2
Center Y = 1 인 이유
CharacterController의 캡슐은 Center를 중심으로 위아래로 펼쳐진다.
Height = 2, Center Y = 1 이면:
캡슐 꼭대기: Y = 2
캡슐 중심: Y = 1 ← Center
캡슐 바닥: Y = 0 ← 캐릭터 발 위치
Height = 2, Center Y = 0 이면:
캡슐 꼭대기: Y = 1
캡슐 중심: Y = 0
캡슐 바닥: Y = -1 ← 바닥 아래로 묻힘!
즉, 높이의 절반(2 ÷ 2 = 1)을 Y에 넣으면 캡슐 바닥이 캐릭터 발 위치에 맞는다.
2. MoveControl.cs 코드 수정
핵심 변경 사항
| 항목기준 | 변경 전 | 변경 후 |
| 이동 방식 | Rigidbody | CharacterController |
| 입력 읽기 | Input.GetKey(KeyCode.W) | _moveAction.ReadValue<Vector2>() |
| 이동 함수 | transform.Translate() | characterController.Move() |
| 중력 | Rigidbody가 자동 처리 | 직접 계산 |
| 착지 판정 | Physics.CheckSphere() | characterController.isGrounded |
코드 자체는 이 글에 기재하지는 않겠다.
대산 Unity 스크립트에서 코드를 작성할 때 사용하는 규칙이나 패턴을 정리해 보았다.
1) Unity 스크립트 생명주기
Unity 는 스크립트의 함수를 정해진 순서로 자동 호출한다.
Awake() ← 가장 먼저 1번 (재료 준비)
↓
OnEnable() ← 활성화될 때 (기계 전원 켜기)
↓
Start() ← 첫 Update 직전에 1번 (초기 설정)
↓
Update() ← 매 프레임 반복 (1초에 60~120번)
↓
OnDisable() ← 비활성화될 때 (기계 전원 끄기)
이번 과제를 코드에서 적용해 본다면,
Awake() → InputActions 객체 생성, Move/Jump 액션 가져오기
OnEnable() → 입력 감지 시작
Start() → CharacterController 연결, 상태 초기화
Update() → 매 프레임 입력 읽기, 이동, 점프, 중력 처리
OnDisable()→ 입력 감지 중지
2) 읽기 → 계산 → 적용
게임에서 움직임을 만드는 코드는 거의 항상 이 3단계다.
// [읽기] WASD 입력을 2D 벡터로 읽는다
Vector2 input = _moveAction.ReadValue<Vector2>();
// [계산] 3D 이동 방향을 만든다
Vector3 moveDirection = transform.forward * input.y + transform.right * input.x;
// [적용] 캐릭터를 실제로 움직인다
characterController.Move(finalMove * Time.deltaTime);
3) FSM (Finite State Machine, 유한 상태 기계)
"캐릭터가 지금 뭘 하고 있는지"를 상태(State)로 관리하는 패턴이다.
┌──────┐ Space 누름 ┌──────┐
│ Idle │ ──────────────→ │ Jump │
│(대기) │ │(점프) │
│ │ ←────────────── │ │
└──────┘ 착지(landed) └──────┘
FSM은 3단계로 동작한다.
- 상태 업데이트 (Update) - 변경 사항에 따라 상태를 바꾼다.
- 전환 조건 판단 (Transition Check) - 바뀐 상태가 조건을 충족하는지 판단한다.
- 상태 전환 처리 (Transition) - 조건을 충족하면 정의된 로직을 실행한다.
4) Time.deltaTime 보정
컴퓨터마다 프레임 속도가 다르기 때문에 이동량에 Time.deltaTime을 곱해서 보정한다.
좋은 컴퓨터 (120fps): 1 × 0.0083 × 120번 = 약 1.0 / 초
느린 컴퓨터 (30fps): 1 × 0.033 × 30번 = 약 1.0 / 초
→ 어떤 컴퓨터든 1초에 같은 거리를 이동!
Update() 안에서 이동이나 시간 경과를 다루면 Time.deltaTime을 곱한다.
반면, FixedUpdate() 를 사용하는 경우 Time.fixedDeltaTime을 적용해야 한다.
쉽게 말해, Update()는 “화면 그리는 타이밍” 이고,
FixedUpdate()는 “물리 계산 타이밍” 이라고 볼 수 있다.
5) 중력 직접 구현
CharacterController는 물리 엔진이 없어서 중력을 직접 코딩한다.
// 물리 공식: v = v + g × t
_verticalVelocity += gravity * Time.deltaTime; // gravity = -9.81
점프 + 중력의 흐름:
Space 누름 → _verticalVelocity = 5 (위로 솟음)
↓ 매 프레임 중력이 속도를 줄임
올라가는 중: 5 → 4.8 → 4.6 → ... → 0 (꼭대기)
↓ 속도가 음수로 바뀜
떨어지는 중: -0.2 → -0.4 → ... (점점 빨라짐)
↓
착지! → 리셋
3. 카메라 끊김 문제 해결
처음 테스트했을 때 마우스 카메라 회전이 느리고 끊겼다.
원인은 CameraControl.cs의 FixedUpdate()에서 마우스를 처리하고 있었다.
- FixedUpdate() = 물리 계산용, 0.02초마다 실행되는 물리 업데이트
- Update() = 매 프레임, 1초에 60~120번
카메라처럼 부드러워야 하는 건 Update()에서 해야 한다. → FixedUpdate를 Update로 한 줄만 바꿔서 해결했다!
더 알아보니 아래처럼 사용하는 것이 정석이라고 한다.
- 입력 & 카메라 → Update
- CharacterController 이동 → Update
- Rigidbody 물리 → FixedUpdate
Rigidbody 외에는 물리가 아니기 때문에 FixedUpdate 를 사용할 필요가 없는 것!
참고) FixedUpdate의 주기는 Edit > Project Settings > Time > Fixed Timestep 에서 바꿀 수 있다 (기본 0.02초 = 50fps).
4. 달리기 구현
.triggered vs .IsPressed()
_jumpAction.triggered // "이번 프레임에 눌렸나?" → 한 번만 감지
_sprintAction.IsPressed() // "지금 누르고 있나?" → 누르는 동안 계속 감지
- triggered = 눌렀을 때 한 번만 켜짐
- IsPressed() = 누르고 있는 동안만 켜짐
배수(multiplier) 패턴
float currentSpeed = moveSpeed; // 기본 속도 5
if (_sprintAction.IsPressed())
currentSpeed = moveSpeed * sprintMultiplier; // 5 × 1.5 = 7.5
핵심: 원본 값(moveSpeed)은 절대 건드리지 않고, 임시 변수에 계산한다.
직접 바꾸면 Shift 누를 때마다 5 → 7.5 → 11.25 → ... 무한 가속되기 때문...
5. 이단 점프 구현
카운터(counter) 패턴
"몇 번 했는지 세서 제한을 두는" 패턴
private int _jumpCount; // 현재 점프 횟수
int maxJumpCount = 2; // 최대 2번
// 점프할 때: _jumpCount++
// 가능 조건: _jumpCount < maxJumpCount
// 착지하면: _jumpCount = 0 (초기화)
구현 흐름
바닥 (_jumpCount = 0)
→ Space → 1번째 점프 (_jumpCount = 1)
→ 공중 Space → 2번째 점프 (_jumpCount = 2)
→ 공중 Space → 2 >= 2 → 점프 안 됨!
→ 착지 → _jumpCount = 0 → 다시 가능
FSM에서 같은 상태로 재진입
이단 점프는 Jump → Jump 전환이다. nextState = State.Jump을 설정하면
2단계 초기화가 다시 실행되면서 위로 다시 솟구친다.
6. 시작 시 플레이어가 떨어지는 문제
구현을 마치고 Play를 눌렀더니 시작하자마자 플레이어가 바닥 아래로 떨어졌다????
왜 그런가 살펴보니 Player의 Position Y가 1로 설정되어 있었다.
CharacterController의 Center Y=1, Height=2이므로 Position Y 는 0으로 설정해야 바닥 높이를 맞출 수 있다.
발이 지면에 닿는 지점을 수식으로 쓰면 [Position Y + Center Y − Height/2] 인데, 여기서 Center Y(=1) − Height/2(=1) = 0이므로 Position Y를 0으로 설정해야 지면에 닿는 지점(=0)이 나올 수 있다.
단, 위 수식은 “Center가 정확히 Height/2일 때만” Center 값을 Height의 중간 지점으로 지정했을 때만 성립한다.
다음에 계속...
'Unity 궁둥이 쪼물쪼물' 카테고리의 다른 글
| 006. 총알에 오브젝트 풀링 적용하기 (0) | 2026.04.06 |
|---|---|
| 005. 발사체(Projectile) 구현 (0) | 2026.04.06 |
| 004. Enemy(적) 애니메이션 구현 (0) | 2026.04.06 |
| 003. 적 AI 구현 & 맵 꾸미기 (0) | 2026.04.06 |
| 001. Unity 환경 설정 & New Input System (0) | 2026.04.02 |