- 수업 방식
- 교수자 설명과 Live Demo
- 핵심 질문
- 같은 속도가 기기마다 같으려면?
- 연결 개념
- field · method · if
- 새 개념
- lifecycle · serialization · deltaTime
- 학생 활동
- 실행 결과 예측과 코드 추적
- 다음 교시
- 독립 구현·검증 미션
학생이 메뉴를 따라 누르는 시간이 아닙니다. 교수자는 코드를 한 덩어리씩 공개하고 Inspector 값을 바꾸어 결과를 시연합니다. 학생은 각 변화 전에 결과를 먼저 말합니다.
코드의 줄과 화면의 변화가 연결되어야 합니다. 단순히 ‘deltaTime을 곱한다’가 아니라 왜 필요한지까지 이해하는 것이 목표입니다.
- 1교시 회수
field, method, if의 역할을 완성 코드에서 다시 찾습니다.
- 생명주기
Awake, Start, Update의 호출 시점과 책임을 구분합니다.
- Inspector 직렬화
코드 기본값과 instance별 저장값의 차이를 설명합니다.
- 시간 기반 변화
프레임 이동량과 초당 속도의 차이를 계산합니다.
- 완성 코드 시연
이동·회전·경계 처리를 화면 결과와 한 줄씩 연결합니다.
- 예측 점검
Inspector 변화 세 가지의 결과를 코드로 설명합니다.
MonoBehaviour는 Unity와 맺은 호출 약속입니다
Awake(), Start(), Update()는 우리가 직접 호출하지 않아도 Unity가 정해진 시점에 호출하는 event function입니다.
Awake()로드될 때 한 번자기 Component 참조나 내부 초기 상태처럼 가장 이른 준비를 둡니다.
OnEnable()활성화될 때마다Component가 사용 가능한 상태가 될 때 호출됩니다. 이번 주에는 존재만 알아둡니다.
Start()첫 Update 전에 한 번활성화된 Component가 본격적으로 동작하기 전 시작 준비를 마칩니다.
Update()Play 중 매 프레임입력 확인, Transform 변화처럼 시간에 따라 계속 달라지는 일을 둡니다.
시작 위치 기억, 첫 상태 설정, 시작 로그처럼 반복할 이유가 없는 작업입니다.
위치 이동, 회전, 조건 검사처럼 프레임마다 새 결과가 필요한 작업입니다.
이름과 대소문자도 약속의 일부입니다
update()나 START()는 문법 오류가 없어도 Unity의 event function으로 호출되지 않습니다. “오류는 없는데 움직이지 않는다”면 함수 이름, 대소문자, 괄호 형태를 먼저 확인합니다.
시작 위치 저장과 계속되는 이동은 어디에 둘까요?
실행 횟수를 기준으로 선택합니다.
Inspector는 instance의 설정을 저장합니다
Unity의 직렬화는 field 값을 Scene이나 Prefab 데이터로 저장하고 Inspector에 표시할 수 있게 합니다. 같은 Script라도 instance마다 다른 값이 가능한 이유입니다.
[SerializeField] private float moveSpeed = 2f;
[SerializeField] private bool shouldRotate = true;
코드가 새 Component의 출발값을 제안합니다.
Unity가 지원하는 field 값을 Scene 또는 Prefab에 저장합니다.
개발자가 코드를 고치지 않고 값을 조절합니다.
각 Component가 서로 다른 저장값으로 같은 행동을 실행합니다.
public float moveSpeed;Inspector에 보이지만 다른 Script도 직접 접근할 수 있어 값의 변경 범위가 넓습니다.
[SerializeField] private float moveSpeed;Inspector에는 보이되, class 밖에는 내부 구현으로 숨깁니다.
코드 기본값보다 저장된 Inspector 값이 우선할 수 있습니다
Component를 붙인 뒤 Inspector에서 moveSpeed를 5로 저장했다면 그 instance는 5를 사용합니다. 이후 코드의 기본값을 3으로 바꾸어도 이미 직렬화된 값이 자동으로 3으로 바뀌지 않을 수 있습니다. “코드를 고쳤는데 값이 그대로다”라는 상황의 흔한 원인입니다.
같은 Script를 쓰는 두 오브젝트가 서로 다른 속도로 움직일 수 있는 이유는?
코드와 데이터 중 무엇이 달라지는지 고릅니다.
속도는 ‘프레임당’이 아니라 ‘초당’으로 정의합니다
컴퓨터마다 한 초에 그리는 프레임 수가 다릅니다. 프레임마다 같은 거리를 더하면 빠른 기기에서 더 멀리 움직이므로, 직전 프레임부터 흐른 시간을 곱합니다.
distance = speed속도 2를 매 프레임 더하면 30 FPS에서는 1초에 60, 60 FPS에서는 120만큼 움직입니다.
distance = speed × deltaTime각 프레임의 이동량은 달라도 1초 동안 더한 총거리는 속도와 거의 같아집니다.
| 환경 | 한 프레임 시간 | 프레임 이동량 | 1초 총거리 |
|---|---|---|---|
| 30 FPS | 약 0.033초 | 2 × 0.033 ≈ 0.066 | 약 2 unit |
| 60 FPS | 약 0.0167초 | 2 × 0.0167 ≈ 0.033 | 약 2 unit |
이번 프레임의 변화량 = 초당 변화량 × Time.deltaTime
Time.deltaTime은 ‘시간 그 자체’가 아닙니다
게임 시작 후 누적된 전체 시간이 아니라, 바로 앞 프레임에서 현재 프레임까지 걸린 초 단위 간격입니다. 프레임마다 조금씩 달라질 수 있습니다.
속도 4, deltaTime = 0.25라면 이번 프레임 이동량은?
speed × deltaTime을 계산합니다.
Translate와 Rotate는 Transform의 상태를 바꿉니다
이번 주에는 물리 힘을 사용하지 않고 Transform을 직접 바꿉니다. 다음 주 Rigidbody2D를 배우면 물리 기반 이동과 책임을 구분합니다.
Vector3.right방향
월드의 오른쪽을 나타내는 (1, 0, 0) 방향 벡터입니다. 속도와 시간을 곱하면 이번 프레임의 이동 벡터가 됩니다.
Space.World기준 좌표계
오브젝트가 회전하더라도 화면의 오른쪽, 즉 월드 x축 방향으로 이동하게 합니다.
Vector3.forward2D 회전축
화면을 뚫고 나오는 z축 방향입니다. 2D Sprite는 이 축을 중심으로 돌아갑니다.
rotationSpeed부호와 방향
양수와 음수는 반대 회전 방향을 만듭니다. 크기의 절댓값은 초당 각속도를 나타냅니다.
transform.Translate(
Vector3.right * moveSpeed * Time.deltaTime,
Space.World
);
transform.Rotate(
Vector3.forward,
rotationSpeed * Time.deltaTime,
Space.Self
);
방향 × 속도 × 시간어느 방향으로, 초당 얼마나, 이번 프레임 동안 얼마만큼 변할지를 하나의 벡터로 만듭니다.Space.World이동 기준을 월드로 고정합니다. 회전 중인 Sprite도 계속 화면 오른쪽으로 갑니다.Space.Self회전을 현재 오브젝트의 축을 기준으로 적용합니다. 2D의 z축 회전에서는 결과가 직관적입니다.
완성 코드를 실행 흐름으로 읽습니다
아래 코드는 교수자 Live Demo의 완성본입니다. 학생은 복사해 따라 치지 않고, field가 어느 method에서 사용되는지 색인하듯 추적합니다.
using UnityEngine;
public class MotionController : MonoBehaviour
{
[SerializeField, Min(0f)]
private float moveSpeed = 2f;
[SerializeField]
private bool shouldRotate = true;
[SerializeField]
private float rotationSpeed = 90f;
[SerializeField]
private float resetAtX = 8f;
[SerializeField]
private float restartAtX = -8f;
private void Start()
{
Debug.Log(name + "의 움직임을 시작합니다.");
}
private void Update()
{
Move();
RotateWhenEnabled();
WrapAtBoundary();
}
private void Move()
{
Vector3 movement =
Vector3.right * moveSpeed * Time.deltaTime;
transform.Translate(movement, Space.World);
}
private void RotateWhenEnabled()
{
if (!shouldRotate)
{
return;
}
float angle =
rotationSpeed * Time.deltaTime;
transform.Rotate(
Vector3.forward,
angle,
Space.Self
);
}
private void WrapAtBoundary()
{
if (transform.position.x <= resetAtX)
{
return;
}
Vector3 nextPosition = transform.position;
nextPosition.x = restartAtX;
transform.position = nextPosition;
}
}
Start()Play 시작 뒤 한 번 이름이 포함된 로그를 남깁니다. 반복 동작과 초기 동작의 차이를 눈으로 확인하기 위한 시연입니다.Update()매 프레임 세 method를 같은 순서로 호출합니다. 전체 흐름을 읽는 목차 역할을 합니다.Move()오른쪽 방향, 초당 속도, 프레임 시간을 곱해 월드 좌표로 이동합니다.RotateWhenEnabled()shouldRotate가 false이면 즉시 돌아가고, true일 때만 시간 기반 각도를 적용합니다.WrapAtBoundary()x가 오른쪽 경계를 넘었을 때 y와 z는 유지하고 x만 왼쪽 시작값으로 바꿉니다.
- 코드 실행 전
Inspector의 다섯 값을 짚고, 각 값이 사용되는 method를 학생에게 묻습니다.
- Play 직후
Console에는 한 번만 로그가 생기고, Transform 값은 계속 바뀌는 차이를 보여 줍니다.
- 값 변경 전
moveSpeed,shouldRotate,rotationSpeed중 하나를 바꾸기 전에 결과를 예측하게 합니다. - 경계 시연
resetAtX를 화면 안쪽으로 옮겨 조건 통과 시점을 빠르게 관찰합니다.
값을 바꾸기 전에 화면을 예측합니다
아래 시뮬레이터는 교수자 설명용입니다. 실제 Unity의 Inspector를 단순화했으며, 코드가 아니라 field 값만 바뀌어도 결과가 달라진다는 점을 보여 줍니다.
MotionController 값 시연
같은 시간 동안 이동하는 거리가 약 두 배가 됩니다. 회전 속도에는 영향을 주지 않습니다.
회전만 멈춥니다. Move()와 WrapAtBoundary()는 계속 호출됩니다.
회전 방향이 반대로 바뀝니다. 이동 방향은 Space.World의 오른쪽으로 유지됩니다.
한 화면의 움직임을 네 연결고리로 설명합니다
아래 순서를 끊기지 않게 말할 수 있으면 3교시 독립 미션을 시작할 준비가 된 것입니다.
instance별 field 값을 저장합니다.
Unity가 매 프레임 호출합니다.
초당 속도를 프레임 변화량으로 바꿉니다.
위치와 회전 상태가 실제로 달라집니다.
교수자 확인 질문
“회전은 멈추되 이동은 유지하려면 어느 값을 바꾸고, 어느 if가 어떤 method 호출을 건너뛰는가?”에 코드의 field 이름과 method 이름을 사용해 답합니다.
설명과 시연의 공식 근거
현재 수업 기준인 Unity 6.6 문서에서 실행 순서, 직렬화, 시간과 Transform API의 정확한 의미를 확인합니다.
Order of execution ↗
Awake, OnEnable, Start와 프레임 update 함수의 호출 순서
Serialization rules ↗
Unity가 field를 저장하고 Inspector에 표시할 수 있는 조건
Time.deltaTime ↗
이전 프레임에서 현재 프레임까지 흐른 초 단위 시간
Transform.Translate ↗
이동 벡터와 좌표계를 지정해 Transform 위치를 바꾸는 API
Transform.Rotate ↗
축, 각도, 기준 좌표계를 지정해 Transform 회전을 바꾸는 API
Debug.Log ↗
실행 시점과 상태를 Console에서 관찰하기 위한 기본 출력
질문이 남았다면
“shouldRotate를 false로 바꿨을 때 이동은 남고 회전만 사라졌습니다. 이 결과를 만드는 return의 범위가 어디까지인가요?”처럼 바꾼 값, 관찰한 결과, 궁금한 코드 범위를 함께 제시합니다.