Week 04 · Period 02 · Lecture + Demonstration

물리의 시계로
움직입니다

입력은 화면이 갱신될 때 읽고, 물리 몸체의 속도는 일정한 물리 시간에 적용합니다. 두 시계를 분리하면 움직임과 충돌이 예측 가능해집니다.

  • 운영교수자 설명 · 코드 시연
  • 입력 시점Update
  • 물리 시점FixedUpdate
어두운 격자형 테스트 공간에서 주황색 플레이어가 상하좌우로 이동하고 대각선 방향의 벽과 충돌하는 모습
INPUT은 방향을 만들고 PHYSICS는 속도와 충돌의 실제 결과를 계산합니다.

00 / ARRIVAL

2교시의 도착점

코드를 복사하는 것이 아니라, 각 줄이 어느 시간에 어떤 책임을 맡는지 설명합니다.

  • 01

    UpdateFixedUpdate가 서로 다른 시계인 이유를 설명합니다.

  • 02

    Rigidbody2D는 움직이는 몸체, Collider2D는 충돌 경계라고 구분합니다.

  • 03

    moveInput × moveSpeedlinearVelocity가 되는 과정을 읽습니다.

  • 04

    Transform 직접 이동, 2D·3D 혼용, 대각선 과속이 왜 문제인지 진단합니다.

01 / RUN OF CLASS

60분의 설명 흐름

입력값이 물리 엔진에 안전하게 도착하는 순서를 먼저 설명하고, 마지막에 전체 코드를 하나로 연결합니다.

00–60 min

의도에서 충돌까지

Lecture + live demonstration

  1. 1교시 회수

    Move가 만드는 값과 입력이 없을 때의 값을 말합니다.

  2. 두 개의 시계

    가변 프레임과 고정 물리 간격을 타임라인으로 비교합니다.

  3. 2D 물리 몸체

    Rigidbody2D, Collider2D와 Body Type의 책임을 구분합니다.

  4. Controller 해설

    초기화, 입력 읽기, 속도 적용을 코드 한 줄씩 연결합니다.

  5. 충돌 시연

    정면·대각선 충돌과 잘못된 설정의 결과를 비교합니다.

  6. 오류 예측

    세 가지 잘못된 이동 방식의 원인을 말로 진단합니다.

02 / TWO CLOCKS

화면의 시계와 물리의 시계는 다릅니다

한 프레임을 그리는 시간은 기기와 장면 복잡도에 따라 달라집니다. 물리 계산은 가능한 한 일정한 간격으로 실행됩니다.

Variable

Update()

렌더링 프레임마다 한 번 호출됩니다. 키가 눌렸는지 읽고, UI나 일반 상태를 갱신하기에 알맞습니다.

입력 읽기 → moveInput 저장
Fixed interval

FixedUpdate()

고정된 물리 시간 간격에 맞춰 호출됩니다. Rigidbody2D의 속도와 힘을 적용하는 자리에 알맞습니다.

moveInput 사용 → 속도 적용
  1. 01
    Frame

    Update가 현재 Move 값을 읽습니다.

  2. 02
    Store

    값을 moveInput field에 잠시 보관합니다.

  3. 03
    Physics step

    FixedUpdate가 가장 최근 값을 가져옵니다.

  4. 04
    Velocity

    방향에 초당 속도를 곱합니다.

  5. 05
    Collision

    Physics 2D가 벽과의 실제 이동 결과를 계산합니다.

책임 분리 Update는 “무엇을 하려는가”를 읽고, FixedUpdate는 “물리 몸체를 어떻게 움직일까”를 적용합니다.

03 / PHYSICS BODY

몸체와 경계가 함께 있어야 충돌합니다

Sprite는 보이는 모양일 뿐입니다. 물리 엔진은 Rigidbody2D와 Collider2D를 통해 움직임과 경계를 이해합니다.

GameObjectPlayerComponent를 담는 하나의 대상
VisualSprite Renderer보이는 모양과 색
BodyRigidbody2D속도, 질량, 물리 계산
BoundaryCollider2D겹침과 충돌을 판단할 영역
BehaviorPlayerController입력값을 물리 속도로 전달
Rigidbody2D

움직임의 주체

위치, 회전, 속도와 물리 반응을 관리합니다. Player처럼 움직이는 대상에는 Dynamic 몸체를 사용합니다.

Collider2D

접촉의 경계

원, 상자, 캡슐 등의 모양으로 충돌 범위를 만듭니다. Sprite의 투명 영역까지 자동으로 정확히 읽는 것은 아닙니다.

PLAYER / Rigidbody2D
Body Type
Dynamic
Gravity Scale
0
Linear Damping
0
Interpolate
Interpolate
Collision Detection
Continuous
Constraints
Freeze Rotation Z
WALL / Collider2D
Component
Box Collider 2D
Is Trigger
Off
Rigidbody2D
없음 또는 Static
Count
네 개 이상
Purpose
충돌 경계 검증
Dimension
2D만 사용

04 / VELOCITY

속도는 ‘한 프레임의 거리’가 아닙니다

linearVelocity는 Rigidbody2D가 1초 동안 각 축으로 얼마나 이동하려는지를 나타내는 초당 속도입니다.

moveInputmoveSpeedlinearVelocity
입력moveSpeed계산 결과
(0, 0)5(0, 0)멈춤
(1, 0)5(5, 0)오른쪽으로 초당 5 unit
(0, -1)5(0, -5)아래로 초당 5 unit
제한한 대각선5길이 5직선과 같은 최대 속도
중요 body.linearVelocity = moveInput * moveSpeed;에는 Time.deltaTime을 곱하지 않습니다. 속도 자체가 이미 초당 단위이기 때문입니다.
Transform.position += ...

위치를 직접 바꾸면 Rigidbody2D의 물리 계산을 우회할 수 있습니다. 충돌 누락이나 떨림처럼 예측하기 어려운 결과가 생길 수 있습니다.

body.linearVelocity = ...

몸체의 속도를 물리 엔진에 전달합니다. 충돌 해결과 다음 위치 계산은 Physics 2D가 맡습니다.

05 / CONTROLLER

전체 코드를 네 책임으로 나눠 읽습니다

교수자가 위에서 아래로 입력합니다. 학생은 각 블록이 “참조 준비, Action 활성화, 입력 읽기, 물리 속도 적용” 중 무엇을 맡는지 추적합니다.

01References

Input Action과 Rigidbody2D를 기억합니다.

02Lifetime

활성화될 때 Action을 켜고 꺼질 때 끕니다.

03Frame

현재 입력을 읽고 길이를 제한합니다.

04Physics

저장한 입력을 초당 속도로 바꿉니다.

[RequireComponent(typeof(Rigidbody2D))]

이 Script가 Rigidbody2D를 필요로 한다는 계약입니다. 누락된 상태로 붙이는 실수를 줄입니다.

InputActionReference moveAction

Input Actions 에셋 속 Move를 Inspector에서 연결할 자리입니다. 장치별 키를 코드에 직접 적지 않습니다.

Awake()

같은 GameObject의 Rigidbody2D를 찾아 body에 보관합니다. 매번 다시 찾지 않습니다.

OnEnable() / OnDisable()

Component가 활성화된 동안에만 Move Action이 값을 만들도록 생명주기를 맞춥니다.

Update()

가장 최근의 입력을 읽어 moveInput에 보관하고, 대각선 벡터의 길이를 1 이하로 제한합니다.

FixedUpdate()

저장된 의도에 초당 속도를 곱해 Rigidbody2D의 linearVelocity에 적용합니다.

OnGUI()

수업 중 관찰을 위한 간단한 계기판입니다. 최종 게임 UI가 아니라 입력과 실제 속도를 비교하는 진단 도구입니다.

REFERENCE / COMPLETE SCRIPT전체 PlayerController 코드 펼쳐보기
PlayerController.cs · lecture reference
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    [Header("Input")]
    [SerializeField] private InputActionReference moveAction;

    [Header("Movement")]
    [SerializeField, Min(0f)] private float moveSpeed = 5f;
    [SerializeField] private bool showInputMonitor = true;

    private Rigidbody2D body;
    private Vector2 moveInput;

    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>();
        moveInput = Vector2.ClampMagnitude(moveInput, 1f);
    }

    private void FixedUpdate()
    {
        body.linearVelocity = moveInput * moveSpeed;
    }

    private void OnGUI()
    {
        if (!showInputMonitor || body == null) return;

        GUI.Box(new Rect(16, 16, 250, 82), "INPUT MONITOR");
        GUI.Label(new Rect(32, 46, 220, 20),
            $"Move: ({moveInput.x:0.00}, {moveInput.y:0.00})");
        GUI.Label(new Rect(32, 68, 220, 20),
            $"Speed: {body.linearVelocity.magnitude:0.00}");
    }
}

06 / INSPECTOR WIRING

코드의 빈칸은 Inspector에서 연결합니다

Script가 정확해도 참조가 비어 있으면 Move 값을 읽을 수 없습니다. 교수자 시연에서는 아래 연결을 눈으로 확인합니다.

  1. 01

    Player에 Rigidbody2D와 Collider2D가 있는지 확인

  2. 02

    PlayerController를 Player에 부착

  3. 03

    Move Action 슬롯에서 Player / Move 선택

  4. 04

    Move Speed5로 설정

  5. 05

    Play 후 계기판의 Move와 Speed 관찰

입력 없음Move (0, 0)
오른쪽Move (1, 0)
속도Speed 5.00
ConsoleError 0

07 / COLLISION DEMO

벽 앞에서 멈추는 것은 코드 한 줄의 결과가 아닙니다

Controller는 속도를 제안합니다. Rigidbody2D와 두 Collider2D, Physics 2D가 함께 실제 접촉 결과를 만듭니다.

LIVE DEMO / 관찰 순서

① 입력 계기판의 값이 바뀐다 → ② Player의 속도가 바뀐다 → ③ Collider 경계에 닿는다 → ④ 벽을 통과하지 않고 접촉면을 따라 움직이거나 멈춘다.

DEMO 01

정면 충돌

벽을 향해 계속 눌러도 Player 중심이 벽 안으로 통과하지 않는지 봅니다.

DEMO 02

대각선 충돌

두 키를 함께 눌러 모서리에서 튀거나 빠져나가는지 관찰합니다.

DEMO 03

Trigger 비교

Wall의 Is Trigger를 잠시 켰을 때 통과가 허용되는 차이를 봅니다.

DEMO 04

Transform 비교

물리 몸체를 Transform으로 직접 옮길 때 왜 결과가 불안정할 수 있는지 설명합니다.

08 / PREDICT ERRORS

결과를 보고 책임이 어긋난 곳을 찾습니다

오류 문구가 없어도 구조가 잘못되면 움직임이 이상할 수 있습니다. 현상에서 원인으로 거슬러 올라갑니다.

WASD를 눌러도 계기판이 (0, 0)입니다

먼저 Move Action 참조가 연결되었는지, Action이 활성화되었는지, Input Actions 에셋을 저장했는지 확인합니다. 물리 설정보다 입력 단계의 문제입니다.

계기판 값은 바뀌는데 Player가 움직이지 않습니다

입력은 정상입니다. PlayerController가 활성화되어 있는지, Rigidbody2D Body Type이 Dynamic인지, moveSpeed가 0보다 큰지 확인합니다.

대각선으로 갈 때 Speed가 약 7.07입니다

moveSpeed = 5일 때 (1, 1)의 길이 때문에 나타나는 값입니다. ClampMagnitude가 빠졌거나 결과를 다시 moveInput에 저장하지 않았는지 봅니다.

벽을 그대로 통과합니다

Player와 Wall에 2D Collider가 모두 있는지, Wall의 Is Trigger가 꺼져 있는지, Rigidbody2D를 Transform으로 직접 움직이는 다른 Script가 없는지 확인합니다.

Player가 멈춘 뒤 천천히 미끄러집니다

Update에서 입력 없음이 (0, 0)으로 갱신되는지 확인합니다. 관성 기반 설계가 아니라면 매 FixedUpdate에 속도 0을 명시적으로 적용해야 합니다.

모서리에 닿을 때 Player가 회전합니다

Rigidbody2D의 Constraints에서 Freeze Rotation Z가 켜져 있는지 확인합니다. 회전이 필요한 게임이라면 잠그지 않지만, 이번 4방향 이동 기준에서는 고정합니다.

09 / RETRIEVAL

세 문장으로 2교시를 닫습니다

아래 문장을 자신의 말로 완성하면 3교시에서 무엇을 만들어야 하는지 흔들리지 않습니다.

01

Update에서는 ______을 읽어 저장하고, FixedUpdate에서는 ______을 적용합니다.

02

linearVelocity는 초당 속도이므로 계산할 때 ______을 따로 곱하지 않습니다.

03

Player와 Wall이 충돌하려면 같은 ______ 물리 체계의 Rigidbody와 Collider를 사용해야 합니다.

정답의 뼈대 입력값 · Rigidbody2D 속도 · Time.deltaTime · 2D

10 / OFFICIAL SOURCES

Unity 공식 자료

시간 흐름, Rigidbody2D와 Collider2D의 설명은 Unity 6.6 문서를 기준으로 정리했습니다.