- 핵심 질문
- 입력 없이도 캐릭터가 낙하하고 바닥에 착지하는 이유는 무엇입니까?
- 학습 방식
- 개념 설명, 예측, 짧은 실험, 결과 해석
- 연결 개념
- Move Action, Vector2의 x와 y
- 새 개념
- FixedUpdate · Rigidbody2D · linearVelocity
- 관찰 기준
- 입력값, 속도, 벽 접촉
- 다음 교시
- 점프 구현과 발판 구간 구성
1교시에는 Move Action의 입력을 Vector2로 읽을 수 있도록 에셋을 구성했습니다. 1교시에서 저장한 PlayerControls와 GameEngine1을 이어서 사용합니다. 이번 교시에는 횡스크롤용 Ground 타일로 바닥을 준비하고 Player와 Ground에 물리 컴포넌트를 추가해 낙하와 착지를 먼저 확인합니다. 이어서 PlayerController.cs에서 Update로 입력을 읽고 FixedUpdate로 속도를 적용합니다. 좌우 이동이 작동하면 벽을 배치해 충돌 결과를 관찰합니다. 3교시에는 이 장면과 이동 코드에 점프를 추가하고 발판 구간을 구성합니다.
- 01물리가 필요한 이유
그림만 있는 장면에서 낙하와 착지가 생기는지 예측합니다.
- 02장면 준비와 낙하 실험
그림만 있는 상태 → Rigidbody2D → 양쪽 Collider2D 순서로 실행하고 차이를 설명합니다.
- 03속도와 실행 주기
입력값과 속도의 차이, 두 메서드가 필드를 공유하는 이유를 설명합니다.
- 04좌우 이동 적용
입력 설정을 마친 뒤 필드와 참조부터 이동 코드를 읽고, 입력값과 움직임을 비교합니다.
- 05벽 배치와 충돌 해석
양끝에 벽을 추가하고, 벽 접촉과 공중에서 키를 놓았을 때의 결과를 설명합니다.
기존 프로젝트와 1교시의 PlayerControls 에셋에 구성한 Move Action을 이어서 사용합니다. 이번 교시에는 핵심 코드를 해석한 뒤 완성 코드를 활용해 결과를 확인합니다. 완성한 PlayerController와 Scene은 저장해 두고, 3교시의 점프 입력·위쪽 속도 적용·발판 배치에 이어서 사용합니다.
캐릭터가 떨어지고 바닥에 서려면 무엇이 필요할까요?
3주차에는 코드로 위치를 바꿨고, 1교시에는 키 입력을 숫자로 읽었습니다. 이번 목표는 키를 누르지 않아도 떨어지고, 바닥을 만나면 멈추는 캐릭터입니다. 화면에 바닥 그림을 놓는 것만으로 이 동작이 생길까요?
먼저 이미지만 배치한 장면을 실행합니다. 이어서 중력으로 움직이게 하는 Rigidbody2D를 적용하고, 서로 닿을 경계를 만드는 Collider2D를 추가합니다. 각 단계에서 바뀌는 결과를 보고 필요한 기능을 설명합니다.
캐릭터와 잔디 그림만 있는 장면에서 Play하면 무엇이 달라질까요? 지금은 답을 외우지 말고 다음 장면에서 확인합니다.
낙하 실험에 사용할 Player와 바닥을 준비합니다
물리 컴포넌트를 추가하기 전에 같은 캐릭터와 횡스크롤용 바닥을 한 화면에 배치합니다. 이 준비를 마치면 Player·Ground의 물리 설정과 낙하 실험을 이어서 진행합니다.
1교시에서 입력값을 확인한 기존 Player를 이어서 사용합니다. 같은 Player의 Sprite Renderer와 자식 DirectionMarker를 유지합니다. Play 모드가 종료된 상태에서 InputValueDisplay와 이미 존재하는 PlayerController를 비활성화하고, 3주차 MotionController 등 Transform을 직접 움직이는 스크립트도 비활성화합니다. Player 오브젝트 자체는 활성 상태를 유지합니다.
이번 수업은 캐릭터를 옆에서 바라보는 횡스크롤 방식의 좌우 이동을 다룹니다. 2주차의 Background.png는 위에서 내려다본 잔디이므로 보존하고, 바닥에는 잔디층과 흙 단면이 보이는 Ground.png를 사용합니다. 캐릭터는 2주차의 Player를 계속 사용합니다.
실습용 Ground 타일 · 횡스크롤용 잔디·흙 단면 PNG
Ground.png 다운로드 ↓
Project에서 Ground.png를 선택하고 Texture Type을 Sprite (2D and UI), Sprite Mode를 Single, Mesh Type을 Full Rect, Filter Mode를 Point (no filter)로 설정한 뒤 Apply를 클릭합니다. 이미 같은 Ground.png가 있다면 기존 파일을 사용합니다.
-
Ground 스프라이트 배치
기존 Ground 오브젝트가 있다면 Sprite Renderer의 Sprite를 새 Ground로 교체합니다. 없다면 Ground.png를 Scene으로 드래그해 Ground 오브젝트를 만듭니다. 기존 탑뷰 Background 오브젝트는 비활성화합니다. Ground의 Draw Mode를 Tiled, Tile Mode를 Continuous로 설정합니다. 전체 크기는 Transform의 X·Y Scale을 같은 비율로 조절하고, 가로 길이는 Sprite Renderer의 Size X를 늘립니다. Size Y는 타일 한 줄의 높이를 유지합니다. 화면 아래에 배치하고 Main Camera의 Orthographic 화면에 Player와 바닥이 함께 보이도록 맞춥니다.
결과잔디·흙 단면이 가로로 반복되는 바닥이 보입니다. 반복 경계에 틈이 없는지 확인합니다.
DirectionMarker는 Player의 자식으로 유지하고 별도 Rigidbody2D를 추가하지 않습니다. ForegroundBush와 Collectible은 바닥 배치에 맞춰 옮겨 장식으로 둡니다. 캐릭터와 충돌 경계를 가리면 위치나 표시 순서를 조절합니다.
기존 Player와 새 Ground가 Game 뷰에 함께 보입니다. 이동 스크립트는 비활성화되어 있습니다. 먼저 아래의 그림만 있는 상태를 실행하고, 다음 절에서 Rigidbody2D를 켭니다. 좌우 Wall은 뒤의 충돌 실험에서 배치합니다.
실행 01 · 그림만 놓으면 떨어질까요?
Play를 종료한 상태에서 Player를 바닥보다 위에 배치합니다. 이동 스크립트는 모두 비활성화합니다. 복습용 Scene에 Rigidbody2D가 이미 있으면 Player의 Simulated를 잠시 해제합니다. Ground에도 몸체가 있다면 이 단계에서는 Simulated를 해제합니다. Player와 Ground에 기존 Collider2D가 있다면 컴포넌트 체크박스를 해제합니다. 새 장면이라면 아직 물리 컴포넌트를 추가하지 않습니다.
키를 누르지 않고 실행
Play하고 Game 화면을 봅니다. 캐릭터와 바닥은 보이지만 Player는 배치한 위치에 그대로 있습니다. 확인 후 Play를 종료합니다.
관찰그림과 위치만으로는 중력에 의한 낙하가 생기지 않습니다.
결과 해석: Sprite Renderer는 모습을 보여 주고 Transform은 배치한 위치를 보관합니다. 낙하를 계산할 기능을 아직 사용하지 않았습니다. 이제 Rigidbody2D를 추가해 차이를 봅니다.
Rigidbody2D를 적용해 낙하를 관찰합니다
시작 상태: 그림만 있는 장면에서는 Player가 정지했습니다. 이제 Player의 Rigidbody2D만 물리 시뮬레이션에 참여시킵니다. Player와 Ground의 Collider2D는 아직 비활성 상태로 둡니다.
Rigidbody2D는 오브젝트의 속도와 중력에 따른 움직임을 물리 엔진이 계산하도록 하는 컴포넌트입니다. Inspector의 Add Component에서 Rigidbody 2D를 추가합니다. 이미 있다면 새로 만들지 않고 설정을 확인합니다. 이번 수업의 Rigidbody와 Collider는 모두 이름에 2D가 붙은 컴포넌트입니다.
중력으로 움직이는 몸체 선택
Player의 Body Type을 Dynamic으로 선택하고 Simulated를 켭니다. Dynamic은 중력과 충돌 반응으로 움직일 몸체에 사용합니다. Kinematic은 코드로 움직임을 제어하는 유형이므로 이번 낙하 실험에서는 선택하지 않습니다.
설정 이유위치를 직접 옮기는 코드 없이 물리 엔진이 움직임을 계산하게 합니다.중력과 이동 축 확인
Gravity Scale은 1, Linear Damping은 0으로 설정합니다. Gravity Scale이 1이면 프로젝트의 2D 중력을 그대로 적용합니다. Project Settings → Physics 2D의 Gravity Y가 음수인지 확인합니다. 기본값은 -9.81입니다.
Constraints에서 Freeze Rotation Z만 선택하고 Freeze Position Y는 선택하지 않습니다. 가로 X·세로 Y 화면에서 Z축 회전은 캐릭터가 옆으로 넘어지는 회전입니다. 회전만 제한하고 낙하 방향은 열어 둡니다.
확인Player는 Dynamic, Simulated 켜짐, Gravity Scale 1이며 Y 위치가 고정되지 않았습니다.
실행 02 · Rigidbody2D만 켜면 어떻게 될까요?
Player를 바닥 위에 두고 Play합니다. 키를 누르지 않은 채 떨어지는지와 바닥에서 멈추는지를 따로 관찰합니다. 화면 아래로 내려가면 Play를 종료합니다. 종료하면 Scene의 시작 위치로 돌아옵니다.
Rigidbody2D가 중력에 따른 낙하를 계산했습니다. 그러나 바닥과 캐릭터가 접촉할 경계는 아직 준비하지 않았습니다. 다음 단계에서는 그림은 그대로 두고 충돌 경계를 추가합니다.
이동 코드와 키 입력 없이 Player가 떨어집니다. Collider2D는 아직 비활성 상태이며, Play를 종료한 뒤 다음 단계로 갑니다.
Collider2D를 추가해 바닥에 착지시킵니다
시작 상태: Player는 Rigidbody2D로 떨어지지만 바닥 그림을 통과합니다. 이제 바닥과 캐릭터가 서로 닿을 영역을 Collider2D로 지정합니다. 이동 스크립트는 계속 비활성화합니다.
Collider2D는 충돌에 사용할 영역입니다. 이번에는 수평인 발과 바닥 경계를 비교하기 쉬운 Box Collider 2D를 씁니다. 컴포넌트는 Play 모드를 종료한 상태에서 하나씩 추가하거나 켭니다.
먼저 Ground에 충돌 경계 추가
Ground에
Box Collider 2D를 추가하거나 기존 Box를 켭니다. Edit Collider로 위쪽 경계를 잔디 윗면에, 가로 길이를 바닥 폭에 맞춥니다. Is Trigger는 선택하지 않습니다. Trigger는 겹침을 감지할 때 사용하며 Player를 받치지 않습니다.고정된 Ground에는 Rigidbody2D를 추가하지 않습니다. 이미 있다면 Body Type을 Static으로 바꾸고 Simulated를 켭니다. Player의 Collider는 아직 꺼 둡니다.
예측바닥 쪽에만 경계가 있어도 Player를 받칠 수 있을까요?바닥 쪽 경계만으로 실행
Player를 바닥 위에서 시작해 Play합니다. 여전히 바닥을 통과하는지 보고 종료합니다. 접촉은 양쪽 영역 사이에서 계산되므로 Player 쪽 경계도 필요합니다.
관찰바닥에만 Collider가 있고 Player에 활성 Collider가 없으면 착지하지 않습니다.Player에도 충돌 경계 추가
Player에 Box Collider 2D를 추가하거나 기존 Box를 켭니다. Capsule 등 다른 Collider가 있다면 이번 비교에서는 하나의 Collider만 활성화합니다. Is Trigger를 해제하고, Edit Collider로 몸통과 발을 감쌉니다. 정확한 발 위치 조절은 착지 결과를 본 뒤 진행합니다.
Player를 바닥 위에 띄워 처음부터 두 Collider가 겹치지 않게 합니다. 처음에는 발 아래에 캐릭터 높이의 절반 정도 공간을 둡니다. 겹친 상태에서 시작하면 물리 계산이 겹침을 해소하면서 캐릭터가 밀려날 수 있습니다.
설정 확인Player와 Ground 양쪽의 Collider가 켜져 있고 Trigger가 아닙니다.실행 03 · 양쪽 경계가 있으면 착지할까요?
다시 Play하고 키를 누르지 않습니다. 캐릭터가 떨어진 뒤 바닥에서 멈추는지 확인합니다. 발이 떠 보이거나 잠겨 보이는지도 관찰하고 Play를 종료합니다.
관찰이동 코드를 추가하지 않아도 낙하한 Player가 Ground에 착지합니다.
착지한 뒤에도 중력은 작용합니다
바닥에 서 있어도 Gravity Scale이나 프로젝트의 중력 설정이 바뀌는 것은 아닙니다. 바닥과의 접촉이 아래 방향 이동을 제한하기 때문에 캐릭터가 바닥을 통과하지 않습니다. 착지를 구현하기 위해 중력을 0으로 변경할 필요는 없습니다.
장면 배치와 Collider 조절은 Play 모드를 종료한 뒤 진행합니다. 일반적인 Scene 오브젝트의 실행 중 변경은 Play 종료 시 되돌아갑니다. 유지할 설정은 종료 후 다시 적용하고 Scene을 저장합니다.
입력 없이 낙하하고 바닥에 착지합니다. 앞선 실행에서 무엇을 추가했는지 떠올리며 두 컴포넌트의 역할을 정리합니다.
실행 결과로 두 컴포넌트의 역할을 정리합니다
그림만 있을 때는 정지했고, Rigidbody2D를 적용하자 떨어졌으며, 양쪽 Collider2D를 준비하자 착지했습니다. 이제 관찰한 결과를 기준으로 움직임을 계산하는 역할과 접촉 영역을 만드는 역할을 구분합니다.
Rigidbody2D와 Collider2D는 서로 대신할 수 없습니다
왼쪽의 색칠된 모형은 화면에 보이는 모습을 뜻합니다.
가운데 계기와 추는 속도·질량 등 물리 상태를, 아래 방향 표시는 중력을 뜻합니다.
오른쪽 주황색 테두리는 다른 물체와 접촉할 영역을 뜻합니다.
이번 설정에서 Dynamic Rigidbody2D만 있는 Player는 중력으로 떨어지지만, 자신의 충돌 영역이 없어 바닥을 통과합니다. 바닥에 서려면 Player와 Ground 양쪽에 Collider2D가 있어야 하며, Trigger 해제 등 충돌 설정도 맞아야 합니다.
역할은 다르지만, 이번 Player의 낙하와 착지에는 Rigidbody2D와 Collider2D를 함께 사용합니다. 움직이지 않는 Ground는 Collider2D만 있어도 Player를 받칠 수 있습니다.
아래 표에서 컴포넌트 구성에 따른 낙하와 접촉의 차이를 비교합니다. Collider2D의 Box와 Capsule은 충돌 영역의 형태를 구분하는 이름입니다.
| 구성 | 중력에 의한 낙하 | 바닥과의 접촉 |
|---|---|---|
| Dynamic Rigidbody2D만 있음 | Gravity Scale이 1이면 낙하합니다. | 자신의 Collider가 없으므로 바닥이 받칠 영역이 없습니다. |
| Collider2D만 있음 | 중력으로 낙하하지 않습니다. | 고정된 경계로 사용하며, Dynamic 몸체에 연결된 Collider를 막을 수 있습니다. |
| Dynamic Rigidbody2D와 Collider2D가 함께 있음 | 중력으로 낙하합니다. | 바닥의 Collider와 접촉해 착지할 수 있습니다. |
위 비교는 Collider가 활성 상태이고 Trigger가 아니며, 해당 레이어 사이의 충돌이 허용된 경우입니다. Player와 Ground의 물리 동작은 오브젝트 이름이 아니라 추가한 컴포넌트와 설정에 따라 결정됩니다.
이번 수업에서 줄여 말하는 Rigidbody와 Collider는 모두 2D 컴포넌트를 뜻합니다. Inspector에서는 Rigidbody 2D와 Box Collider 2D처럼 이름의 2D까지 확인합니다.
| 요소 | 담당하는 역할 | 단독으로 수행할 수 없는 기능 |
|---|---|---|
| Transform | 위치·회전·크기를 나타냅니다. | 위치를 직접 바꾸는 명령만으로 충돌 규칙을 구성하지는 않습니다. |
| Sprite Renderer | 캐릭터와 바닥의 이미지를 표시합니다. | 이미지의 발이나 잔디 윗면이 자동으로 충돌 경계가 되지는 않습니다. |
| Rigidbody2D | 속도와 중력 등 물리 움직임을 계산하는 데 사용합니다. | 그 자체로 캐릭터 모양의 충돌 경계를 만들지는 않습니다. |
| Collider2D | 다른 물체와 접촉할 영역을 정의합니다. | 입력에 따라 이동하는 코드를 대신하지는 않습니다. |
실험에 사용한 설정 다시 확인하기
| 설정 | 선택과 이유 | 다르게 설정하면 |
|---|---|---|
| Body Type | Player의 Body Type은 Dynamic으로 설정합니다. 고정된 Ground에는 Collider2D만 추가합니다. 뒤에서 배치할 Wall도 같은 방식입니다. Ground나 Wall에 Rigidbody2D가 이미 있다면 Body Type을 Static으로 설정합니다. | Kinematic은 코드로 움직임을 제어하는 유형이며, 이번 낙하 실험의 대체 설정이 아닙니다. |
| Gravity Scale | 1로 설정해 프로젝트의 중력을 그대로 적용합니다. | 0이면 중력에 의한 낙하가 발생하지 않습니다. |
| Is Trigger | 선택을 해제해 바닥과 벽이 움직임을 막도록 합니다. | Trigger는 겹침 감지에 사용하며 물체를 받치거나 통과를 막지 않습니다. |
| Constraints | Freeze Rotation Z로 충돌 시 캐릭터가 넘어지는 회전을 제한합니다. Freeze Position Y는 선택하지 않습니다. | Y 위치를 고정하면 아래로 떨어질 수 없습니다. |
| Linear Damping | 0으로 설정해 감쇠에 의한 속도 감소가 발생하지 않도록 합니다. | 이동 키를 놓았을 때 가로 속도를 0으로 지정하는 코드와는 별개의 설정입니다. |
Rigidbody2D만 켰을 때와 양쪽 Collider2D까지 켰을 때 무엇이 달랐나요? Ground에는 왜 Dynamic 몸체를 추가하지 않았는지도 설명합니다. 다음에는 착지한 발의 위치에 맞춰 경계를 조절합니다.
착지 결과를 보고 충돌 경계를 조절합니다
방금 관찰한 발의 위치를 아래 세 가지 상태와 비교합니다. 낙하하고 바닥에 멈추는 물리 동작을 확인했다면, 이제 보이는 발과 실제 접촉 경계가 맞는지 살펴봅니다.
캐릭터의 발과 바닥에 충돌 경계를 맞춥니다
Play 모드를 종료한 뒤 Player를 선택하고 Box Collider 2D의 Edit Collider를 사용합니다. Scene 뷰에서 아래쪽 경계는 보이는 발바닥에, 좌우 경계는 몸통의 폭에 맞춥니다. 위쪽은 머리의 주된 형태를 포함하되, 긴 모자 끝이나 DirectionMarker까지 모두 감싸는 것은 이번 예제의 목표가 아닙니다. 충돌 영역은 모든 그림 픽셀을 따라가는 윤곽선이 아니라 게임에서 사용할 단순한 영역입니다.
Ground도 선택해 Collider의 위쪽 경계를 잔디 윗면에 맞춥니다. Offset은 경계의 중심 위치, Size는 경계의 크기를 조절합니다. 이미지의 투명 여백은 발이나 바닥의 일부로 판단하지 않습니다.

발바닥과 Player 경계의 아래쪽이 일치합니다.

발보다 아래로 나온 Collider가 먼저 바닥에 닿습니다.

Collider가 닿았을 때 그림의 발은 경계 아래에 있습니다.
비교 그림의 주황색 테두리는 Player의 충돌 영역이고 초록색 선은 바닥 경계입니다. 이해를 위한 도식이며 실제 Unity 화면 캡처는 아닙니다. 실제 장면에서는 Player와 Ground 양쪽 경계를 확인합니다. Capsule은 끝이 둥근 충돌 영역이며, 형태에 따른 접촉 차이는 수업 후 비교합니다.
Sorting Layer에서 뒤에 그려져도 Collider는 충돌에 참여합니다. 캐릭터가 공중에 떠 보이거나 발이 잠기면 이미지의 투명 여백과 Collider의 경계를 함께 확인합니다.
경계를 조절했다면 다시 Play해 착지 위치를 확인하고, 종료한 뒤 Scene을 저장합니다. 바닥에 서는 동작을 확인했으므로 다음에는 입력에 따라 가로로 움직이는 방법을 살펴봅니다.
속도는 위치가 변하는 방향과 빠르기를 나타냅니다
linearVelocity는 X·Y 방향의 속도를 Unity 거리 단위/초로 나타냅니다. 이번 코드에서는 입력으로 가로 속도를 지정하고, 세로 속도는 중력과 충돌에 따라 변하도록 유지합니다.
| 개념 | 질문 | 예 |
|---|---|---|
| 위치 | 현재 위치는 어디입니까? | 바닥보다 위에 있는 캐릭터의 좌표 |
| 속도 | 이동 방향과 빠르기는 어떻습니까? | 가로 속도 5는 오른쪽으로 초당 5 Unity 단위 |
| 가속도 | 시간에 따라 속도가 어떻게 변합니까? | 중력은 낙하하는 동안 세로 속도를 변화시킴 |
이동 입력 x는 방향과 세기를 나타내며 거리 단위가 없습니다. moveSpeed와 곱해야 가로 속도가 됩니다. 화면 픽셀 수와 Unity의 거리 단위도 구분합니다. 중력의 방향이 아래라면 낙하 중 y속도는 음수입니다.
가로 속도가 5로 유지되고 장애물이 없다면 1초 동안 오른쪽으로 5단위 이동합니다. 도중에 속도가 달라지거나 벽에 닿는다면 같은 이동 거리를 보장하지 않습니다.
이번 코드는 가로 속도를 직접 지정합니다
D키의 입력 x가 1이면 가로 속도를 moveSpeed로 지정하고, 키를 놓으면 다음 FixedUpdate에서 0으로 지정합니다. 매 물리 업데이트마다 현재 입력에 맞는 가로 속도를 다시 설정하므로, 이전 입력으로 설정한 속도가 계속 유지되지 않습니다. 키를 놓았을 때 멈추는 이유는 마찰이나 감쇠가 아니라 가로 속도를 0으로 지정하는 코드입니다.
힘을 가하는 AddForce는 힘과 질량에 따라 속도가 변하도록 하는 다른 제어 방법입니다. Rigidbody2D를 사용하더라도 이번 코드가 서서히 가속하거나 관성으로 미끄러지는 동작을 자동으로 구현하지는 않습니다. 가속·감속이나 외부 충격을 유지하는 이동은 이후에 별도로 설계합니다.
가로 속도만 새로 정하고 세로 속도는 보존합니다
vx = moveInput.x × moveSpeed / vy = body.linearVelocity.y
세로 속도 보존은 이동 코드가 현재 y속도를 다른 값으로 덮어쓰지 않는다는 뜻입니다. y속도를 특정 숫자로 고정하는 것이 아닙니다. 이후 물리 계산에서는 중력과 충돌에 의해 y속도가 달라집니다. 매번 y를 0으로 덮어쓰면 누적된 낙하 속도가 지워지므로, 정상적인 낙하를 방해합니다.
주황색은 오른쪽 이동입니다. D키를 누르면 코드가 x속도를 지정하고, 키를 놓으면 x속도를 0으로 지정합니다.
초록색은 낙하 방향입니다. 코드는 현재 y속도를 유지하며, 중력과 착지 시의 충돌이 이후 y속도에 영향을 줍니다.
입력이 (0, 0)이어도 공중의 캐릭터는 계속 떨어져야 합니다. 아래 표의 -3은 그 순간의 예시 값입니다. 다음 물리 계산 이후에도 계속 -3이라는 뜻은 아닙니다.
| 상황 | Move | 현재 y속도 | 새 속도 (vx, vy) |
|---|---|---|---|
| 착지 후 키를 놓은 상태 | (0, 0) | 0 | (0, 0) |
| 낙하 중 D | (1, 0) | -3 | (5, -3) |
| 낙하 중 A | (-1, 0) | -3 | (-5, -3) |
| 낙하 중 키를 놓은 상태 | (0, 0) | -3 | (0, -3) |
| W만 누름 | (0, 1) | -3 | (0, -3) · 입력 y는 사용하지 않음 |
이후 물리 계산에서 중력은 y속도를 바꾸고, Ground나 Wall과의 접촉은 움직임을 제한합니다. 낙하 중 전체 속력은 5보다 클 수 있습니다. 비교할 값은 전체 벡터 길이가 아니라 가로 속도 vx입니다.
body.linearVelocity = new Vector2(moveInput.x * moveSpeed, body.linearVelocity.y);linearVelocity는 초당 속도이므로 Time.deltaTime을 곱하지 않습니다. 물리 엔진이 시간에 따른 위치 변화를 계산합니다.
3주차의 Transform 직접 이동 스크립트는 비활성화합니다. 같은 Player를 물리 속도와 Transform 변경으로 동시에 움직이면 충돌 결과를 해석하기 어렵습니다.
3주차에는 속도에 경과 시간을 곱해 위치에 더할 이동량을 계산했습니다. 이번에는 이동량이 아니라 속도 자체를 Rigidbody2D에 지정하므로 deltaTime이나 fixedDeltaTime을 다시 곱하지 않습니다.
낙하 중 y속도가 -3일 때 D키를 놓으면 새 속도는 무엇인가요? 전체 속도를 (0, 0)으로 지정하는 코드와 비교합니다.
입력을 읽는 주기와 물리를 계산하는 주기를 구분합니다
화면을 갱신하는 주기와 물리를 계산하는 시간 간격은 다릅니다. Update는 프레임마다 호출되고, FixedUpdate는 고정된 시뮬레이션 시간 간격에 대응해 호출됩니다.
두 메서드가 항상 한 번씩 번갈아 호출되는 것은 아닙니다. 렌더링 프레임 하나에 FixedUpdate가 없거나 여러 번 호출될 수 있습니다. ‘고정’은 호출 사이에 실제로 경과한 시간이 항상 같다는 뜻이 아니라, 물리를 계산하는 시간 단위가 일정하다는 뜻입니다.
| 실행 주기 | 프레임 A | 프레임 B | 프레임 C |
|---|---|---|---|
| Update | 1회 | 1회 | 1회 |
| FixedUpdate | 0회일 수 있음 | 1회일 수 있음 | 여러 회일 수 있음 |
moveInput은 두 메서드가 공유하는 최근 입력값입니다
이번 수업은 Input System의 기본적인 동적 입력 업데이트 설정을 전제로 합니다. Update에서 Move를 읽어 필드 moveInput에 저장합니다. FixedUpdate는 그 필드에 저장된 가장 최근 값을 사용해 가로 속도를 설정합니다. 새 입력을 읽기 전까지는 같은 값을 다시 사용할 수 있습니다.
주황색 칸의 폭은 프레임마다 달라질 수 있는 시간 길이를 뜻합니다. 이번 코드는 프레임마다 Move를 읽어 저장합니다.
가운데 카드는 가장 최근에 읽은 입력값입니다. 새 입력을 저장하기 전까지 FixedUpdate가 같은 값을 다시 사용할 수 있습니다.
초록색 칸의 폭은 일정한 물리 시뮬레이션 시간 단위를 뜻합니다. 이번 코드는 저장된 입력으로 몸체의 가로 속도를 지정합니다.
필드와 지역 변수는 사용할 수 있는 범위가 다릅니다
| 선언 위치 | 코드 예 | 사용 범위 |
|---|---|---|
| 클래스 안, 메서드 밖 | private Vector2 moveInput; | 해당 컴포넌트의 필드입니다. Update와 FixedUpdate가 같은 필드를 읽고 쓸 수 있습니다. |
| Update 메서드 안 | Vector2 currentInput = moveAction.action.ReadValue<Vector2>(); | 지역 변수입니다. Update 안에서 선언한 currentInput을 FixedUpdate에서 이름으로 직접 사용할 수 없습니다. |
실제 코드의 Update에는 moveInput = ...;처럼 대입만 작성합니다. 그 앞에 Vector2를 다시 적으면 같은 이름의 지역 변수를 선언하게 되어, 다른 메서드가 사용하는 필드가 갱신되지 않습니다.
이동 방향처럼 유지되는 값에는 이 구조를 사용할 수 있습니다. 한 번 누른 순간을 놓치지 않아야 하는 점프 등의 처리 방법은 이후 수업에서 별도로 다룹니다.
입력값을 Update 안의 임시 변수에만 저장하면 FixedUpdate가 그 값을 바로 사용할 수 있을까요? 클래스 필드에 저장하는 이유를 설명합니다.
입력을 어디에 저장하고 언제 사용할지 정했습니다. 다음에는 가로 이동에 맞게 입력 에셋을 조정한 뒤, 방금 설명한 두 메서드를 코드에서 확인합니다.
가로 이동에 맞게 Move 입력을 준비합니다
이동 코드로 넘어가기 전에 1교시의 입력 에셋을 한 번 조정합니다. 이번에는 Move의 x만 이동에 사용하므로, 위아래 키를 함께 눌러도 가로 입력이 줄지 않도록 설정합니다.
좌우 이동만 하는 이번 장면에서도 1교시에서 만든 Move를 재사용하기 위해 Vector2를 유지합니다. 처음부터 가로 이동 전용 입력을 설계한다면 하나의 축 값을 읽는 방식도 가능합니다. Vector2가 횡스크롤 이동의 필수 조건인 것은 아닙니다.
1교시의 Value / Vector2 Move를 유지하고 x만 이동에 사용합니다. Play 모드를 종료한 뒤 WASD와 방향키 Composite의 Mode를 모두 Digital로 바꾸고 Save Asset을 클릭합니다. 기본 Digital Normalized에서는 W+D의 x가 약 0.71이지만, Digital에서는 x가 1이므로 W를 함께 눌러도 가로 속도가 줄지 않습니다. 게임패드 스틱 Binding은 그대로 둡니다.
Vector2.ClampMagnitude로 전체 입력을 보정하면 x도 줄어들 수 있으므로 이번 횡스크롤 코드에는 넣지 않습니다. W/S의 y값은 읽히지만 점프나 위아래 이동에 사용하지 않습니다.
Digital로 변경하는 대상은 키보드 Composite입니다. 게임패드 스틱은 기울인 정도에 따라 중간 입력값을 전달합니다. 예를 들어 Action에서 읽은 x가 0.5이고 moveSpeed가 5이면 코드가 지정하는 가로 속도는 2.5입니다. 같은 코드를 사용해도 키보드와 스틱의 입력 세기가 항상 같은 것은 아닙니다.
Play 모드를 종료한 상태에서 WASD와 방향키 Composite의 Mode를 모두 Digital로 설정하고 에셋을 저장했습니다. 이제 이 입력을 Update에서 읽고 FixedUpdate에서 사용하는 코드를 살펴봅니다.
입력 읽기와 속도 적용을 나눕니다
앞에서 배운 속도와 실행 주기를 PlayerController.cs에 연결합니다. 필드에 무엇을 보관하는지 확인하고, Awake에서 몸체 참조를 준비합니다. 이어서 Action 활성화, Update의 입력 읽기, FixedUpdate의 속도 적용 순서로 코드를 읽습니다.
이 흐름에 필요한 필드와 참조를 먼저 준비한 뒤 각 메서드를 연결합니다.
필드로 설정값·참조·관찰값을 준비합니다
Project의 Assets/Scripts에 MonoBehaviour Script를 생성하고 이름을 PlayerController로 맞춥니다. 이미 같은 스크립트가 있다면 기존 파일을 사용합니다. 아래 기본 코드에서 참조와 필드의 역할을 확인합니다. 단계별로 작성할 때는 기본 코드를 붙여 넣고, 이어서 나오는 Awake·OnEnable·OnDisable·Update·FixedUpdate를 클래스의 마지막 중괄호 앞에 추가합니다. 수업 중 실행 확인에는 이 절 마지막의 완성 코드를 사용할 수 있습니다.
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
[SerializeField] private InputActionReference moveAction;
[SerializeField, Min(0f)] private float moveSpeed = 5f;
private Rigidbody2D body;
[SerializeField] private Vector2 moveInput;
[SerializeField] private Vector2 observedVelocity;
}
[RequireComponent(typeof(Rigidbody2D))]는 이 스크립트를 추가할 때 필요한 Rigidbody2D가 없으면 함께 추가하도록 지정합니다. typeof는 필요한 자료형을 전달합니다. [Min(0f)]는 Inspector에서 moveSpeed에 음수를 입력하지 않도록 돕는 설정이며, 이동 계산이나 실행 중 검증을 대신하지 않습니다.
RequireComponent가 추가하는 컴포넌트와 사용자가 설정할 항목을 구분합니다. Collider2D는 별도로 추가하고, Body Type·Gravity Scale·Constraints는 앞에서 설명한 값으로 설정합니다.
| 필드 | 역할 | 지정 또는 확인 방법 |
|---|---|---|
| moveAction | 입력을 읽을 Action의 참조 | Inspector에서 Player/Move를 지정합니다. |
| moveSpeed | 가로 이동 속도의 설정값 | Inspector에서 값을 정하고 움직임을 비교합니다. |
| moveInput | 실행 중 읽은 입력값 | Play 중 Inspector에서 관찰합니다. Update가 갱신하므로 직접 편집하지 않습니다. |
| observedVelocity | 관찰 시점에 읽은 물리 속도 | Update에서 body.linearVelocity를 읽어 표시합니다. 이동을 제어하는 설정값이 아닙니다. |
| body | 같은 Player의 Rigidbody2D 참조 | Awake에서 GetComponent로 찾습니다. |
InputActionReference moveAction에는 Inspector에서 Input Actions 에셋 속 Move를 지정합니다. 장치별 키를 코드에 직접 적지 않고, 연결한 Action에서 입력값을 읽습니다.
[SerializeField]가 붙었다고 모두 사용자가 조절하는 설정값은 아닙니다. 이번 moveInput에는 학습 중 값을 관찰하기 위해 이 특성을 붙였습니다. 필드의 역할은 실제로 값을 지정하고 사용하는 코드와 함께 판단합니다.
Awake에서 Rigidbody2D 참조를 준비합니다
1교시의 moveAction은 Inspector에서 에셋 속 대상을 지정했습니다. 이번 body는 같은 Player에 이미 추가한 Rigidbody2D를 코드로 찾아 참조합니다. GetComponent<Rigidbody2D>()는 새 몸체를 만드는 명령이 아닙니다.
여기서 ‘같은 Player’는 PlayerController가 추가된 바로 그 게임오브젝트입니다. GetComponent는 이 호출만으로 자식의 Rigidbody2D까지 검색하지 않습니다. 이번 구성에서는 PlayerController, Rigidbody2D, Box Collider 2D를 모두 Player에 추가합니다.
Awake()에서 실행에 필요한 참조를 준비합니다. 이 컴포넌트의 Awake는 OnEnable과 첫 Update보다 먼저 실행되며, 여기서 구한 참조를 이후 메서드에서 사용합니다. 다른 오브젝트의 Awake 순서까지 보장한다는 뜻은 아닙니다.
private void Awake()
{
body = GetComponent<Rigidbody2D>();
}
찾은 Rigidbody2D를 필드 body에 보관하면, 이후 Update와 FixedUpdate에서 같은 몸체를 사용할 수 있습니다. 매번 다시 찾지 않습니다. 다음에는 필드 moveAction이 참조하는 Move Action을 활성화합니다.
Action 활성화와 비활성화
클래스 안에 아래 두 메서드를 추가합니다. PlayerController의 활성화 상태에 따라 Move Action을 활성화하거나 비활성화합니다.
private void OnEnable()
{
if (moveAction == null)
{
Debug.LogError("PlayerController: Move Action을 연결하세요.", this);
enabled = false;
return;
}
moveAction.action.Enable();
}
private void OnDisable()
{
if (moveAction != null)
{
moveAction.action.Disable();
}
}
입력 읽기
Update에서는 Move의 입력을 읽어 moveInput에 저장합니다. 앞에서 준비한 Digital 키보드 입력은 D와 W+D 모두 x가 1입니다. 함께 읽는 현재 속도는 관찰용 필드에 저장합니다.
private void Update()
{
moveInput = moveAction.action.ReadValue<Vector2>();
observedVelocity = body.linearVelocity;
}
observedVelocity = body.linearVelocity;는 몸체의 현재 속도를 학습용 필드에 복사합니다. 이 대입은 Rigidbody2D의 속도를 변경하지 않습니다. Update에서 읽는 순간의 관찰값이며, FixedUpdate에서 지정한 값은 그 사이 물리 계산의 영향을 받을 수 있습니다.
속도 적용
FixedUpdate에서 입력의 x 성분에 moveSpeed를 곱해 가로 속도를 지정합니다. 세로 속도에는 입력의 y 성분 대신 Rigidbody2D의 현재 y속도를 사용합니다.
private void FixedUpdate()
{
body.linearVelocity = new Vector2(moveInput.x * moveSpeed, body.linearVelocity.y);
}
새 Vector2로 가로·세로 속도를 구성합니다
new Vector2(x, y)는 두 성분으로 새 값을 구성합니다. moveInput.x * moveSpeed는 새 가로 속도이고, body.linearVelocity.y는 현재 물리 몸체의 세로 속도입니다. 입력의 y인 moveInput.y와 구분합니다. 대입문 오른쪽에서 가로·세로 속도를 계산해 Vector2를 구성한 뒤, 이 값을 linearVelocity에 대입합니다.
| 코드의 부분 | 예시 값 | 의미 |
|---|---|---|
| moveInput.x | -1 | 왼쪽 입력이며, 거리나 속도 단위가 없습니다. |
| moveInput.x * moveSpeed | -1 × 5 = -5 | 새로 설정할 가로 속도입니다. |
| body.linearVelocity.y | -3 | 현재 몸체의 낙하 속도를 읽습니다. |
| new Vector2(-5, -3) | (-5, -3) | 두 성분을 합쳐 몸체에 지정할 속도를 구성합니다. |
moveInput.x는 사용자의 가로 입력이고, body.linearVelocity.x는 물리 몸체의 가로 속도입니다. 같은 x축을 나타내지만 값의 의미와 단위가 다릅니다. 위 예에서 입력은 -1이고 코드가 지정하는 가로 속도는 -5입니다.
완성 코드 · PlayerController.cs 전체 복사
앞에서 설명한 필드와 메서드를 포함한 전체 코드는 다음과 같습니다. 완성 코드를 활용할 때는 PlayerController.cs의 내용을 아래 코드로 교체합니다. 저장한 뒤 Console 오류가 0개인지 확인합니다.
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
[SerializeField] private InputActionReference moveAction;
[SerializeField, Min(0f)] private float moveSpeed = 5f;
private Rigidbody2D body;
[SerializeField] private Vector2 moveInput;
[SerializeField] private Vector2 observedVelocity;
private void Awake()
{
body = GetComponent<Rigidbody2D>();
}
private void OnEnable()
{
if (moveAction == null)
{
Debug.LogError("PlayerController: Move Action을 연결하세요.", this);
enabled = false;
return;
}
moveAction.action.Enable();
}
private void OnDisable()
{
if (moveAction != null)
{
moveAction.action.Disable();
}
}
private void Update()
{
moveInput = moveAction.action.ReadValue<Vector2>();
observedVelocity = body.linearVelocity;
}
private void FixedUpdate()
{
body.linearVelocity = new Vector2(moveInput.x * moveSpeed, body.linearVelocity.y);
}
}
입력은 Update에서 읽고, 가로 속도는 FixedUpdate에서 설정합니다. 세로 속도를 보존하는 줄을 찾고 Console의 컴파일 오류가 없는지 확인합니다.
Inspector에서 Move Action을 연결합니다
코드가 정확해도 참조가 비어 있으면 Move 값을 읽을 수 없습니다. Play 전에 아래 연결을 확인합니다.
-
컴포넌트 추가
기존 스프라이트 Player에 PlayerController를 추가합니다. 이미 있다면 중복으로 추가하지 않고, 낙하 실험에서 비활성화했던 PlayerController를 다시 활성화합니다. Rigidbody2D와 Collider2D가 같은 오브젝트에 있는지 확인합니다. InputValueDisplay와 MotionController는 비활성화되어 있고 PlayerController만 이동을 담당해야 합니다. Player와 Sprite Renderer는 활성 상태로 유지합니다.
결과Inspector에 Move Action 슬롯이 보입니다. -
Move 참조 연결
Move Action 슬롯에서 PlayerControls / Player / Move를 선택합니다. Move Speed를 5로 설정합니다. 목록에서 Move를 찾을 수 없다면 1교시의 Input Actions 에셋을 저장했는지 확인합니다.
완료 기준Play 후 착지하고 A/D 또는 좌우 방향키로 이동합니다. 키를 놓으면 가로 이동만 멈춥니다.
Inspector의 입력값과 실제 움직임을 비교합니다
아직 좌우 Wall을 배치하기 전이므로 바닥 가운데에서 짧게 이동하고, 끝에 닿기 전에 키를 놓습니다. Play 모드를 시작한 뒤 Player를 선택해 Inspector의 PlayerController를 표시합니다. Game 뷰를 클릭하고 A/D를 누르면서 Move Input의 X가 -1/1로 바뀌는지 확인합니다. 키를 놓으면 0입니다. W/S는 Y만 바꾸며 이번 코드에서는 세로 이동에 사용하지 않습니다. 실행 중 Move Input을 직접 편집하지 않습니다.
D와 W+D의 X가 모두 1인지 비교합니다. 다르면 WASD Composite가 Digital인지 확인합니다. 1교시 InputValueDisplay를 동시에 활성화할 필요는 없습니다.
Move Input과 Observed Velocity를 함께 관찰합니다
같은 PlayerController의 Observed Velocity를 펼칩니다. 장애물이 없는 곳에서 D를 누르면 Move Input X는 1이고, 코드가 지정하는 가로 속도는 5입니다. Move Input은 입력의 방향과 세기를, Observed Velocity는 관찰 시점의 실제 속도를 나타냅니다. 두 값은 의미와 단위가 다르며, 실제 속도에는 중력과 충돌의 영향도 반영됩니다.
낙하 중 키를 놓았을 때 Move Input은 (0, 0)이지만 Observed Velocity의 Y는 음수인지 확인합니다. 바닥에서 안정되면 Y는 0에 가까워집니다. 값이 빠르게 변하면 Play 모드를 일시 정지해 확인합니다. 표시된 값은 마지막 Update에서 읽어 저장한 입력과 속도입니다. 이 관찰값은 직접 수정하지 않습니다.
입력값의 X와 가로 이동 방향이 일치합니다. 이동 키를 놓으면 가로 이동만 멈추며, 낙하 중에는 세로 이동이 이어집니다.
바닥에 착지하고 벽을 통과하지 않는 결과를 관찰합니다
입력은 가로 속도를 정하고, 중력과 Collider2D 접촉은 세로 움직임과 이동 가능한 범위를 결정합니다.
시작 상태: 입력으로 좌우 이동하고, 키를 놓아도 낙하가 이어집니다. Play 모드를 종료한 뒤 Ground의 양끝에 벽을 추가합니다. 바닥이 세로 움직임을 막았듯, 벽이 가로 움직임을 막는지 확인합니다.
-
바닥 양끝에 Wall 배치
좌우 끝에
Wall_Left와Wall_Right를 세웁니다. 기존 소품 Sprite를 사용하거나 사각 Sprite로 경계를 표시하고 Box Collider 2D를 추가합니다. Is Trigger의 선택을 해제합니다. Rigidbody2D는 추가하지 않으며, 이미 추가했다면 Body Type을 Static으로 설정합니다. 바닥과 벽 사이에 Player가 빠질 틈이 없게 배치합니다.결과Ground와 좌우 Wall이 캐릭터의 이동 범위를 제한합니다.
| 대상 | 몸체 | 경계와 역할 |
|---|---|---|
| Player | Dynamic / Gravity Scale 1 / Freeze Rotation Z | Collider2D, Is Trigger Off · 낙하와 좌우 이동 |
| Ground | Rigidbody2D 없음 또는 Static | Box Collider 2D, Is Trigger Off · 발을 받치는 윗면 |
| Wall_Left · Wall_Right | Rigidbody2D 없음 또는 Static | Box Collider 2D, Is Trigger Off · 좌우 경계 |
선택 설정 · 충돌 검출과 화면 보간
Collision Detection의 Continuous는 빠른 이동에서 충돌을 놓치는 현상을 줄이기 위한 설정이며 계산 비용이 증가할 수 있습니다. Interpolate는 물리 계산 사이의 표시 위치를 보간해 움직임을 부드럽게 보이게 합니다. 충돌 경계를 추가하거나 속도를 바꾸는 기능은 아닙니다.
빠른 이동의 충돌 검출과 화면 보간을 비교하려면 Continuous와 Interpolate를 선택합니다. 필수 개념은 Rigidbody2D와 Collider2D의 역할이며, 이 두 옵션의 비교 실험은 수업 후 진행합니다.
Player·Ground·Wall의 Collider2D는 Trigger가 아닙니다. 두 벽이 바닥과 이어져 있고 Player의 시작 위치가 벽이나 바닥과 겹치지 않습니다.
D를 계속 눌러 벽에 닿으면 Move Input의 X도 0이 될까요? 낙하 중 D를 놓으면 가로 이동과 낙하가 모두 멈출까요?
입력 없이 낙하와 착지
Edit Mode에서 Player를 바닥 위에 띄우고 Play 모드를 시작합니다. 키를 누르지 않아도 떨어지고, 발 쪽 Collider가 Ground의 윗면에 닿으면 내려가지 않는지 관찰합니다. 확인 후 Play 모드를 종료합니다.
예상입력이 (0, 0)이어도 낙하합니다. 착지 후에는 위치가 안정됩니다.좌우 이동과 벽 접촉
다시 Play 모드를 시작하고 Game 탭을 클릭합니다. 착지 후 A/D 또는 좌우 방향키로 이동합니다. 좌우 Wall을 향해 계속 입력해도 통과하지 않는지 확인합니다.
예상바닥 위에서 좌우로 이동하고 벽 앞에서 멈춥니다. 키를 놓으면 가로 이동이 멈춥니다.공중에서 키를 놓기
Play 모드를 종료한 뒤 Edit Mode에서 Player를 조금 더 높게 놓고 다시 Play 모드를 시작합니다. 낙하 중 D를 잠깐 누른 뒤 놓습니다. 가로 이동은 멈추지만 바닥까지 낙하는 계속되어야 합니다.
해석코드가 가로 속도를 0으로 바꾸면서 현재 세로 속도는 보존합니다.
Physics 2D는 Player의 Rigidbody2D 상태와 양쪽 Collider2D의 접촉을 바탕으로 충돌 반응을 계산합니다. Ground의 그림만으로는 발을 받칠 수 없습니다.
| 상황 | 예측 | Move Input X | 실제 움직임 |
|---|---|---|---|
| D를 누른 채 벽 접촉 | 기록 | 기록 | 기록 |
| 낙하 중 D를 놓음 | 기록 | 기록 | 기록 |
관찰 결과 해석
벽에 닿아도 D를 누르고 있으면 입력 X는 1입니다. 코드가 설정하는 가로 속도와 충돌 처리 뒤의 움직임은 구분해야 합니다. 벽이 이동을 막는다고 사용자의 입력이 사라지는 것은 아닙니다.
낙하 중 D를 놓으면 입력 X가 0이 되어 가로 이동은 멈추지만, 현재 y속도를 보존하므로 낙하는 계속됩니다. 충돌로 멈추는 것은 물리 엔진의 기본 반응이며, 이번 범위에서는 OnCollisionEnter2D 같은 충돌 이벤트 코드를 작성할 필요가 없습니다.
선택 심화 · 충돌 경계와 마찰·반발은 다릅니다
Collider의 모양은 어디에서 접촉하는지를 정합니다. Physics Material 2D의 마찰은 접촉면을 따라 미끄러지는 성질에, 반발은 충돌 후 튀는 성질에 영향을 줍니다. 그림에 사용하는 머티리얼과 구분합니다.
Linear Damping은 접촉면의 마찰과 별개로 속도를 감쇠하는 설정입니다. 또한 이번 코드는 가로 속도를 매번 지정하므로, 마찰만으로 가로 이동의 가속·감속을 설명해서는 안 됩니다. 재질 비교 실험은 이동 코드의 영향을 분리해 수업 후 진행합니다.
입력·설정한 속도·충돌 후 움직임을 구분해 설명합니다. 확인 후 Play 모드를 종료하고 Scene을 저장합니다.
관찰한 현상에 따라 입력·이동 코드·충돌 설정을 점검합니다
Console에 오류가 없어도 참조나 물리 설정이 잘못되면 캐릭터가 이동하지 않거나 바닥을 통과할 수 있습니다. 아래에서 관찰한 현상에 해당하는 항목을 점검합니다.
WASD를 눌러도 값이 (0, 0)입니다
PlayerController의 Move Input을 관찰합니다. 먼저 Game 뷰에 키보드 포커스가 있는지, Move Action 참조가 연결되었는지, Action이 활성화되었는지, Input Actions 에셋을 저장했는지 확인합니다. 이 경우에는 물리 설정보다 입력 처리 과정을 먼저 점검합니다.
값은 바뀌는데 Player가 움직이지 않습니다
W/S만 눌렀다면 가로 이동이 없는 것이 정상입니다. A/D에서도 움직이지 않으면 PlayerController가 활성화되어 있는지, Rigidbody2D Body Type이 Dynamic인지, moveSpeed가 0보다 큰지 확인합니다.
공중에 멈추거나 낙하가 지나치게 느립니다
Gravity Scale이 1인지, Physics 2D의 Gravity Y가 음수인지, Freeze Position Y가 선택되지 않았는지 확인합니다. FixedUpdate가 y속도를 0이나 moveInput.y로 덮어쓰지 않고 body.linearVelocity.y를 유지하는지 확인합니다.
벽을 그대로 통과합니다
Player와 Ground·Wall에 2D Collider가 있는지, Is Trigger의 선택이 해제되어 있는지, Rigidbody2D를 Transform으로 직접 움직이는 다른 스크립트가 없는지 확인합니다.
학습 내용 점검
먼저 코드를 보지 않고 답을 작성합니다. 작성 후 설명과 비교하고, 설명이 어려운 항목은 관련 개념과 코드를 다시 읽습니다.
Update와 FixedUpdate는 각각 어떤 처리를 담당합니까?
Update에서는 Move 값을 읽어 저장하고, FixedUpdate에서는 x로 가로 속도를 설정합니다. 현재 y속도를 유지하여 중력과 충돌로 계산된 세로 속도를 이동 코드가 덮어쓰지 않도록 합니다.
linearVelocity 계산에 Time.deltaTime을 곱하지 않는 이유는 무엇입니까?
linearVelocity는 초당 속도이므로 계산할 때 Time.deltaTime을 따로 곱하지 않습니다. 물리 엔진이 고정 간격으로 그 속도를 사용합니다.
Player와 Wall의 충돌에는 어떤 물리 컴포넌트가 필요합니까?
Player에는 Rigidbody2D와 Collider2D를, 고정된 Wall에는 Collider2D를 사용합니다. 2D와 3D 물리 컴포넌트 사이에서는 충돌이 처리되지 않습니다.
Move Action을 Inspector에서 연결하지 않으면 어떤 일이 일어납니까?
OnEnable이 오류를 출력하고 컴포넌트를 비활성화합니다. 입력에 의한 가로 이동은 실행되지 않습니다. Rigidbody2D는 별도 컴포넌트이므로 중력에 의한 낙하는 계속될 수 있습니다.
Sprite Renderer, Rigidbody2D, Collider2D는 각각 무엇을 담당합니까?
Sprite Renderer는 모습을 표시하고, Rigidbody2D는 속도와 중력 등 물리 상태를 담당하며, Collider2D는 접촉 경계를 정의합니다. Dynamic Rigidbody2D만 있으면 낙하할 수 있지만 바닥이 받칠 충돌 영역은 없습니다. Collider2D만 있는 Ground는 고정된 경계로 Player를 받칠 수 있습니다. 그림과 충돌 영역은 별개이므로 발과 바닥의 경계도 직접 확인합니다.
낙하 중 이동 키를 놓을 때 y속도를 보존해야 하는 이유는 무엇입니까?
입력은 가로 이동만 담당합니다. 중력과 충돌로 계산된 세로 속도를 유지해야 이동 키를 놓아도 낙하가 이어집니다.
Unity 공식 문서
시간 흐름, Rigidbody2D와 Collider2D의 설명은 Unity 6.6 문서를 기준으로 정리했습니다.
- SpriteRenderer.drawMode ↗Sprite의 반복 표시와 크기 지정
- Physics Material 2D ↗접촉면의 마찰과 반발 설정
- GetComponent ↗같은 게임오브젝트에서 컴포넌트 참조 찾기
- Per-frame updates ↗가변 프레임과 Update 계열의 시간 개념
- Fixed updates ↗고정된 시뮬레이션 시간 간격과 물리 계산 주기
- Rigidbody2D.linearVelocity ↗2D 몸체의 초당 X·Y 속도
- Introduction to Rigidbody 2D ↗Transform, Rigidbody2D와 Collider2D의 관계
- Collider 2D ↗2D 충돌 영역과 Collider 종류
질문이 남았다면
변경한 값, 관찰한 결과, 추가 설명이 필요한 코드 범위를 함께 제시합니다. 예: “Update의 moveInput은 (1, 0)인데 Player가 움직이지 않습니다. FixedUpdate와 Rigidbody2D Body Type을 확인했습니다.”