2D Godot 2D 게임 기초
P1 렌더와 프레임

5강좌표계와 변환

원점, Y축, 피벗, 로컬과 글로벌.

이 강의 목차 (6)
  1. 화면의 좌표는 수학 시간과 다르다
  2. 위치·회전·크기 — 변환의 3요소
  3. Vector2 — 2D의 기본 자료형
  4. 로컬과 글로벌
  5. 피벗 — 무엇을 중심으로 도는가
  6. 실습 — 궤도 도는 위성
이번 강의의 목표
  • 2D 좌표계의 원점과 Y축 방향을 안다.
  • position / rotation / scale 이 무엇을 바꾸는지 구분한다.
  • 로컬 좌표와 글로벌 좌표의 차이를 설명할 수 있다.
  • 피벗(원점)이 왜 디자인 단계에서 중요한지 안다.

화면의 좌표는 수학 시간과 다르다#

수학 시간에 배운 좌표평면은 원점이 가운데 있고 Y가 위로 갑니다. 컴퓨터 화면은 다릅니다.

text
(0,0) ────────────────▶ X 증가
  │
  │      화면
  │
  ▼
 Y 증가
  • 원점 (0, 0)은 왼쪽 위 모서리
  • X는 오른쪽으로 갈수록 증가 (같음)
  • Y는 아래로 갈수록 증가 (반대!)
Y가 아래로 증가한다

이것 하나 때문에 초보가 계속 헷갈립니다.

  • 위로 점프 = velocity.y = -400 (음수!)
  • 아래로 낙하 = velocity.y = +400
  • 중력은 velocity.y += 980 * delta (양수를 더하면 아래로)

"점프인데 왜 마이너스지?"가 나오면 이 그림을 다시 떠올리세요.

왜 화면 좌표는 Y가 아래로 갈까

브라운관 시절 화면은 왼쪽 위부터 오른쪽 아래로 한 줄씩 전자빔을 훑어 그렸습니다. 그 스캔 순서가 그대로 메모리 배열 순서가 되었고, 지금까지 표준으로 남았습니다. 글을 쓰는 방향(좌→우, 위→아래)과도 같아서 자연스럽기도 합니다.

참고로 3D 그래픽스와 물리 엔진 일부는 Y가 위입니다. Godot의 2D는 화면 관습을 따릅니다.

위치·회전·크기 — 변환의 3요소#

Node2D를 상속한 모든 노드는 이 세 가지를 가집니다. 인스펙터의 Transform 항목입니다.

속성 타입 의미 단위
position Vector2 어디에 있는가 픽셀
rotation float 얼마나 돌아있는가 라디안
rotation_degrees float 같은 값을 도(°)로
scale Vector2 얼마나 크게 배율 (1.0 = 원래 크기)
skew float 기울임 라디안
코드의 rotation은 라디안이다

인스펙터에는 도(degree) 로 보이지만 코드의 rotation라디안입니다.

gdscript
rotation = deg_to_rad(90)   # 이렇게 쓰거나
rotation_degrees = 90       # 이렇게 쓴다
rotation = PI / 2           # 같은 값(90도)

rotation = 90 이라고 쓰면 90라디안, 즉 약 5156도 회전합니다.

Vector2 — 2D의 기본 자료형#

position은 그냥 숫자가 아니라 Vector2(x와 y를 묶은 값) 입니다. 게임 코드의 절반은 이 타입을 다룹니다.

gdscript
var v := Vector2(100, 50)

v.x                    # 100
v.y                    # 50
v + Vector2(10, 10)    # (110, 60)
v * 2                  # (200, 100)
v.length()             # 원점에서의 거리 (약 111.8)
v.normalized()         # 길이가 1인 같은 방향 벡터
v.angle()              # 방향(라디안)
v.distance_to(other)   # 다른 점까지의 거리
벡터는 "위치"이자 "방향과 크기"다

같은 Vector2(100, 50) 이 문맥에 따라 두 가지로 읽힙니다.

  • position 에 쓰면 → : "화면의 (100, 50) 자리"
  • velocity 에 쓰면 → 화살표: "오른쪽으로 100, 아래로 50만큼의 속도"

방향만 필요할 땐 normalized() 로 길이를 1로 맞춥니다. 그래야 대각선으로 갈 때 더 빨라지는 문제가 안 생깁니다(7강에서 다시 나옵니다).

편리한 상수들:

gdscript
Vector2.ZERO    # (0, 0)
Vector2.ONE     # (1, 1)
Vector2.LEFT    # (-1, 0)
Vector2.RIGHT   # (1, 0)
Vector2.UP      # (0, -1)   ← Y가 위로 갈 때 음수!
Vector2.DOWN    # (0, 1)

로컬과 글로벌#

2강에서 "부모가 움직이면 자식도 따라온다"를 봤습니다. 그 원리가 여기 있습니다.

두 개의 위치
  • position (로컬)부모를 기준으로 한 상대 위치
  • global_position (글로벌)화면 세계 전체를 기준으로 한 절대 위치

부모가 원점에 있으면 둘이 같습니다. 부모가 움직이면 달라집니다.

예를 들어 이런 구조에서,

text
Player  (global_position = (500, 300))
└─ Gun  (position = (20, -5))

Gun.position(20, -5) 이지만 Gun.global_position(520, 295) 입니다.

접시 위의 컵, 다시
  • 로컬 = "컵은 접시 중심에서 오른쪽으로 3cm"
  • 글로벌 = "컵은 방 왼쪽 벽에서 2m, 앞 벽에서 1.5m"

접시를 옮기면 로컬은 그대로, 글로벌만 바뀝니다. 접시를 회전시키면 글로벌 위치가 원을 그리며 돕니다.

언제 무엇을 쓰나#

하려는 일 쓰는 것
캐릭터를 앞으로 움직인다 position
총알을 총구 위치에서 생성한다 global_position
적이 플레이어 쪽을 본다 global_position 끼리 계산
팔을 몸통 기준으로 흔든다 position
마우스 위치를 안다 get_global_mouse_position()
총알을 만들 때 가장 많이 하는 실수

총알을 만들어 Player의 자식으로 붙이면, 플레이어가 움직일 때 총알이 따라갑니다. 총알은 발사되는 순간 부모에게서 독립해야 합니다.

gdscript
var b := BULLET.instantiate()
# 총알은 씬 루트(월드)에 붙인다. 부모를 따라가면 안 되므로.
get_tree().current_scene.add_child(b)
# 위치는 총구의 "세계 좌표"로 지정한다.
b.global_position = muzzle.global_position
b.rotation = muzzle.global_rotation

피벗 — 무엇을 중심으로 도는가#

Sprite2D는 기본적으로 이미지의 정중앙을 원점으로 삼습니다. 그래서 rotation을 주면 이미지가 가운데를 축으로 돕니다.

문제는 이게 항상 원하는 축이 아니라는 겁니다.

원하는 동작 필요한 피벗
캐릭터가 제자리에서 돈다 몸 중앙
캐릭터가 바닥에 서 있다 발밑
총이 손잡이를 축으로 겨눈다 손잡이
문이 경첩을 축으로 열린다 경첩
대포가 포신 뒤를 축으로 든다 포신 뒤쪽

Godot에서 피벗을 바꾸는 방법은 두 가지입니다.

방법 1 — Sprite2D의 Offset

인스펙터의 Offset → Offset 값을 조정합니다. 발밑을 원점으로 두려면 이미지 높이의 절반만큼 위로 올립니다((0, -32) 같은 값). Centered 체크를 끄면 이미지 좌상단이 원점이 됩니다.

방법 2 — 부모 노드를 하나 더 두기 (권장)

text
Player      (Node2D)     ← 발밑 기준. 이게 캐릭터의 "위치"
└─ Sprite   (Sprite2D)   ← position (0, -32) 로 올려둔다

이 구조가 훨씬 유연합니다. Player를 회전시키면 발밑을 축으로 돌고, Sprite만 따로 흔들 수도 있습니다.

  • 개발자 — 캐릭터의 "위치"는 발밑으로 잡는 게 대부분 편합니다. 바닥 판정, 그림자 위치, Y-sort(6강) 정렬이 전부 발밑 기준이기 때문입니다.
  • 기획자 — "적이 바닥에 반쯤 묻혀 보여요"는 대개 버그가 아니라 피벗 규칙이 없는 것입니다. 프로젝트 시작 시 "모든 캐릭터의 원점은 발밑"처럼 규칙을 문서에 못 박으세요.
  • 디자이너 — 스프라이트를 그릴 때 캔버스 안에서 캐릭터의 위치를 통일하세요. 같은 캐릭터의 대기/걷기/공격 이미지에서 발 위치가 3픽셀씩 다르면 캐릭터가 떨립니다. 이건 코드로 못 고칩니다.

실습 — 궤도 도는 위성#

부모-자식 변환을 눈으로 확인하는 가장 좋은 예제입니다.

씬 구성#

text
Main            (Node2D)
└─ Planet       (Sprite2D)   위치 (400, 300)
    └─ Pivot    (Node2D)     위치 (0, 0)
        └─ Moon (Sprite2D)   위치 (150, 0), scale (0.4, 0.4)

Pivot은 그림이 없는 빈 노드입니다. 회전축 역할만 합니다.

스크립트#

Main에 붙입니다.

gdscript
extends Node2D

@onready var planet: Sprite2D = $Planet
@onready var pivot: Node2D = $Planet/Pivot
@onready var moon: Sprite2D = $Planet/Pivot/Moon

func _process(delta: float) -> void:
	# 축을 돌리면 자식(달)이 원을 그린다.
	pivot.rotation += 1.2 * delta

	# 행성 자체도 좌우로 왕복시킨다.
	var t := Time.get_ticks_msec() / 1000.0
	planet.position.x = 400.0 + sin(t) * 120.0

	# 달의 두 좌표를 비교해 본다.
	if Engine.get_process_frames() % 60 == 0:
		print("로컬 ", moon.position,
			" 글로벌 ", moon.global_position)

실행하면 달이 행성 주위를 돌고, 행성 자체는 좌우로 흔들립니다. 출력 패널을 보면 moon.position은 항상 (150, 0)으로 고정인데 global_position은 계속 바뀝니다. 이게 로컬과 글로벌의 차이입니다.

직접 바꿔보기
  1. Moonposition.x를 150 → 300 으로: 궤도가 어떻게 되나요?
  2. Planetscale(2, 2) 로: 달의 궤도 반지름도 같이 커집니까? (부모의 scale이 자식에게 어떻게 전파되는지 확인)
  3. Moon 아래에 또 Node2D + Sprite2D를 넣어 달의 위성을 만들어보세요.
  4. moon.rotation 을 출력해 보세요. 부모가 돌면 자식의 rotation도 바뀝니까?
2번 실험이 알려주는 것 — scale은 상속된다

부모의 scale은 자식의 위치·크기에 모두 곱해집니다. 그래서 부모를 (2,2)로 키우면 달은 2배 커지고 궤도 반지름도 2배가 됩니다.

여기서 실무 함정이 하나 나옵니다. scale이 음수이거나 서로 다른 값(예: (2, 1))이면 자식의 회전이 찌그러집니다. 캐릭터를 좌우 반전할 때 scale.x = -1 을 쓰는 경우가 흔한데, 자식에 물리 콜라이더가 달려 있으면 물리 엔진이 경고를 내거나 이상하게 동작합니다.

권장 — 좌우 반전은 scale.x = -1 대신 Sprite2Dflip_h = true 를 쓰세요. 그림만 뒤집고 변환은 건드리지 않습니다.

자주 나는 오류

"회전시켰더니 캐릭터가 이상한 데로 날아간다" → 피벗이 이미지 밖에 있습니다. Offset 값을 확인하세요.

"부모를 옮겼는데 자식이 안 따라온다" → 자식을 코드로 global_position 에 매 프레임 대입하고 있진 않은지 보세요. 글로벌 대입은 부모의 변환을 덮어씁니다.

"Node2D를 추가했는데 화면에 안 보인다" → 정상입니다. Node2D위치만 가지고 아무것도 그리지 않습니다. 에디터에서 작은 십자 표시로만 보입니다.

여기까지 확인
  • 원점은 왼쪽 위, Y는 아래로 증가한다
  • rotation은 라디안, rotation_degrees는 도다
  • position은 부모 기준, global_position은 세계 기준이다
  • 피벗을 바꾸는 두 가지 방법을 안다
  • 궤도 예제에서 로컬은 고정, 글로벌은 변하는 걸 확인했다

다음 강의 — 이제 물체를 원하는 자리에 놓을 수 있습니다. 그런데 겹치면 누가 위에 오나요? 그리기 순서와 렌더링을 봅니다.