1편에서 Node와 Scene에 대해 많은 얘기를 했지만, 결국 막대기 하나 만들었다. 처음이라서 하나하나 짚고 넘어가서 그랬는데, 중복된 얘기를 안한다고 해도 아직 할얘기가 많긴하다. 1편에서 설명이 조금 아쉬웠던 Kinematic body와 Rigid body의 차이만 간단하게 얘기하고 다음 얘기로 넘어가자.
KinematicBody2D vs RigidBody2D
Rigid body는 물리 시뮬레이션이 가능한 물체라고 생각하면 된다. 질량도 있고, 중력의 영향도 받으며, 회전 토크도 받는다. 마찰력의 영향을 받을 수도 있다. Kinematic body는 상호작용은 하지만 물리적인 시뮬레이션은 적용되지 않는다. 벽처럼 움직임이 없을 수도 있고, 개발자가 물리법칙과는 상관없는 단순한 움직임을 만들수도 있다. 당연히, Rigid body가 많은 속성들을 가지게 되고 계산도 복잡해진다. 그래서 물리 시뮬레이션이 필요없다면 대부분 Kinematic body가 효율적이며 간단하다. 여기서도 플레이어의 막대기, 튕겨내는 공도 전부 Kinematic body로 구현할 것이다. 회전 토크 없이 충돌시 면에서 튕겨내는건 Kinematic body에서도 충분하며, Godot에서 관련 API도 지원한다. 시각적으로 설명한 매우 적절한 영상(Pushable Objects in Godot Using KinematicBody2D vs RigidBody2D )이 있어 일부분을 따와봤다. 백마디 말보다 쉽게 이해될 것이다.

출처 : https://www.youtube.com/watch?v=XuH18H4hC0U
다시 1편에서 만들던 Pong게임으로 돌아가자. 플레이어 막대기를 만들었는데, 이를 움직이려면 유저 입력을 받아 이동시키는 스크립트를 만들어야 한다. 스크립트 설명이 장황해질 것 같으니, 화면 구성 요소를 먼저 끝내보자.
Ball과 Opponent 만들기
플레이어의 상대편을 만들려면 생각해볼게 있다. 이걸 둘이서 즐기는 PVP 게임으로 만들것인가, 알고리즘으로 동작하는 컴퓨터로 만들것인가? 나는 같이할 친구도 없고, 레퍼런스로 사용하는 영상에서 간단한 알고리즘을 구현해주고 있기 때문에 이걸 따라가 보겠다. 이걸 기반으로 키보드와 조이패드, 또는 키보드의 양쪽에 다른 키를 사용해서 PVP게임으로 확장해보는 것도 잼있을 것이다.
플레이어와 상대편이 동일하게 생기고 동작하기 때문에 재활용이 가능할 것 같다. 하지만 노드트리는 그대로 사용가능해도, 아직 다루지 않은 스크립트의 구현이 한쪽은 사용자 입력을 받아 움직이고, 한쪽은 알고리즘으로 움직여야해서 전혀 달라지기 때문에, 상대편을 구현하는데 PlayerStick 씬의 인스턴스는 사용이 어렵다. PlayerStick 씬을 참고하여 OpponentStick을 만들어보자.

1편에서 만들었던 PlayerStick과 동일한 방법이기 때문에 자세한 설명은 안하겠다. 만들게 되면 위와같이 보일 것이다. 루트노드는 복사가 안되니 KinematicBody2D로 만들고, 자식 노드들은 PlayerStick에서 복사해서 사용하면 하나하나 설정하지 않고 바로 만들 수 있다.
이제 Pong Level에 PlayerStick처럼 인스턴스를 추가해보자. 노트 트리에서 체인 아이콘을 클릭해도 되고, Ctrl+Shift+A 단축키를 이용해도 된다. 그럼 Pong Level의 노드트리가 다음과 같이 보일 것이다.

두 막대기를 화면에 배치해볼건데, 그전에 화면 해상도를 바꾸고 고정해보자. 메뉴의 Project>Project Settings… 를 선택한다.

다음 화면과 같은 다양한 내용을 설정하는 다이얼로그가 뜰 것이다.

다뤄야 할게 많아 보이지만, 여기서는 건너 뛰겠다. 화면 해상도만 볼텐데, General 탭에서 Display>Window를 선택한다. 그러면 위 화면처럼 오른쪽에 디스플레이 관련 설정들이 보이는데, Width/Height값을 자신에게 맞게 변경한다. 이 해상도가 게임 실행 해상도가 된다. 여기서는 1280×720으로 주었다. 그리고, 화면 리사이즈가 안되게 하기위해서 Resizable은 체크를 해제한다. 프로젝트 셋팅은 따로 저장하지 않아도 변경시 바로 적용되므로 Close 버튼을 누르고 나온다.
이제 막대기를 배치해보자. 눈대중으로 옮겨놔도 되겠지만, 해상도를 정했으니, 수치로 위치를 설정해보자.

노드트리에서 PlayerStick을 선택한 후, Inspector화면의 Node2D에 있는 Transform>Position 값을 위와 같이 (35, 360)으로 입력했다. Position속성이 보이지 않는다면, Transform항목을 클릭하면 열릴 겻이다. OpponentStick도 동일하게 선택한 후, Inspector에서 Position을 (1245, 360)으로 입력했다. 이 값들은 이전에 설정한 1280×720 해상도에 맞춘 것이다. F5를 눌러 실행해보고 화면에 제대로 보이는지 확인해 본다.
이제 Ball을 추가해보자. 이 포스트의 처음에 RigidBody와 KinematicBody를 다시한번 짚고 넘어갔다. 튜토리얼을 안보고 직접 만들었다면 이걸 RigidBody로할지 KinematicBody로 할지 고민이 되었을 것이다. 게임을 보다 흥미롭게 만들고 싶다면, RigidBody로 시도를 해봐도 잼있을 것 같지만, 여기서는 단순하게 튕기기만 할 것이므로 KinematicBody로도 충분하다.
Pong게임이 매우 단순한 게임이긴 하지만, 튜토리얼 포스팅을 작성하면서 뜯어보다보니 변주할만한 흥미로운 부분이 많이 보이는거 같다. 방금 얘기한 것처럼 RigidBody를 이용하여 물리 시뮬레이션을 넣어 변수를 만들 수도 있겠지만, 다른 방법도 있다. 예를들면, 벽돌깨기 게임인 알카노이드를 해봤다면 알텐데, 물리법칙이 적용되는건 아니지만, 공이 플레이어의 어느 부분에 맞느냐에 따라서 반사각도가 달라진다. 입사각에 따른 반사가 아니라 충돌 위치가 중요한 것이다. 이경우, 물리법칙을 따르지는 않지만, 유저가 공을 원하는데로 보낼 수가 있다.
사고의 확장은 잠시 접어두고, 만들려는 것에 다시 집중해보자. PlayerStick처럼 새로운 씬을 만든다. 루트 노드는 동일하게 KinematicBody2D이다. 노드 이름을 PongBall 로 바꾸자.

PlayerStick과 마찬가지로 Sprite와 CollisionShape2D 노드를 추가한다.

트리구조가 그림과 같이 되지 않는다면, 각 노드를 드래그해서 위치를 변경할 수 있다.
먼저, Sprite의 속성을 설정하자. PlayerStick과 비슷하게, Texture속성을 설정해야 한다. 이전에 받은 Asset를 보면, Ball.png가 있을 것이다. 이걸 Texture로 드래그 하거나, Texture>Load를 선택하여 이미지를 직접 선택해준다. 이제 이미지가 보일텐데, 여기에 맞게 CollisionShape을 설정해주자.
노드트리에서 CollisionShape2D를 선택해주고, Inspector에서 Shape항목을 New CircleShape2D로 선택하자. 노드트리에서 선택한 후, 뷰포트에서 컨트롤 포인트를 조정하여 이미지 크기에 맞게 조절하자.

노드를 선택하고 F를 누르면 Focus가 잡혀서 해당 노드를 뷰포트의 중심으로 가져온다. 마우스 휠을 스크롤하면, 줌을해서 편집이 용이해진다.
이제 만든 볼 씬을 Pong Level로 가져오자. Pong Level을 열고 Pong Ball을 인스턴스로 추가한다. 추가한 후, 노드트리의 모습은 다음과 같다.

Ball의 위치도 조정하자. PongBall을 선택한 후, Inspector에서 Position을 (640, 360)으로 조정하여 화면 중앙에 위치시킨다.

완료한 화면은 뷰포트로 보면 다음과 같이 보일 것이다.

F5를 눌러 게임을 실행하여 제대로 보이는지 확인해보자.
Godot의 GDScript
스크립트를 어디서부터 어디까지 설명해야할지 난감하긴하다. 간단히 얘기하면 게임 엔진이 제공하는 API를 사용해서 게임을 작동하게 만드는 프로그래밍이다. 매 프레임마다 뭘 해야할지, 사용자 입력을 받으면 뭘 해야할지, 충돌과 같은 처리는 어떻게 해야할지 등등.
Godot에서 스크립트 언어로는 여러가지가 제공되지만, 에디터와 통합되어 있고 사용이 쉬우며, 별다른 설정없이 사용할 수 있는건 GDScript라는 자체 스크립트 언어이다. 이외에 C#등도 사용가능하다. GDScript는 90%가까이 파이썬을 닮아있다. 파이썬을 안다면, 새로배울 내용은 거의 없을 것이다. 이럴거면 그냥 파이썬을 쓰는게 낫지 않았나 싶은데, 초기에 파이썬과 루아등 기존에 존재하는 스크립트 언어를 쓰려는 시도가 있었으나, 게임과의 통합과 최적화에 어려움이 있었던 것으로 보인다. 결국 이들을 참고하여 새로운 언어를 만든게 GDScript이다. 링크를 들어가보면 이와 관련된 내용을 볼 수 있다.
GDScript의 튜토리얼을 만들 생각은 없다. 프로그래밍은 가능하다고 가정할 것인데, 스크립트를 익히는데에도 시간이 꽤 필요할 것이므로, 여기서는 그냥 따라하기만 해도 된다. 필요하다면 학습자료는 많다.
- GDScript 공식 문서 : https://docs.godotengine.org/ko/stable/tutorials/scripting/gdscript/index.html
- gdquest 가 만든 튜토리얼 사이트 : https://gdquest.github.io/learn-gdscript/
- Godot Tutorials 채널의 GDScript 기초 튜토리얼 : https://www.youtube.com/watch?v=itKLmCwGeNs&list=PLJ690cxlZTgL4i3sjTPRQTyrJ5TTkYJ2_
대표적인 튜토리얼들을 나열해봤는데, 검색해보면 공부할 자료가 부족하지는 않을 것이다.
PlayerStick 움직이기위한 스크립트 작성
게임 엔진의 플로우를 다시 돌아보자. 게임 엔진은 무한루프를 돌고 있다고 얘기했었다.

게임 시작시, 시작 씬으로 설정된 레벨 씬을 로드하게 되는데, 이 때 레벨 씬에 있는 트리노드의 노드들이 인스턴스로 생성되서 큐나 트리등의 자료구조안에 저장된다. Pong에서는 사용하지 않지만, 적을 생성하거나 총알을 쏘는등 동적으로 노드를 생성하는 경우에도 이 자료구조에 저장될 것이다.
30fps의 경우라면 초당 30번, 60fps라면 초당 60번 루프를 돌며 계산을 하고 프레임을 그려낼텐데, 자료구조에 저장해놓은 오브젝트들을 어떻게 움직일지, 충돌시에 무슨 작업이 필요한지 게임엔진은 알 수 없다. 이 부분을 우리가 스크립트로 작성해서 알려줘야 한다.
다른 엔진도 비슷하지만, Godot은 Callback을 이용하여 이 부분을 처리한다. 루프를 돌면서 모든 노드 인스턴스들의 특정 Callback들을 호출한다. 다음은 Godot이 Callback을 부르는 일부분의 시퀀스 다이어그램을 그려본 것이다.

일단, 노드를 처음 생성할 때 일회성으로 _init() 함수가 호출된다. 이것은 클래스를 인스턴스로 생성할 때 생성자같은 역할을 한다. 노드가 준비가 되면 역시 일회성으로 _ready()함수가 호출된다. 노드가 준비 되었다는 얘기는 하위 노드트리가 모두 인스턴스로 생성이 되고, 하위 노드가 모두 _ready()가 호출된 상태를 말한다. 그러니까 _ready()는 트리의 잎사귀 노드부터 위로 올라가며 호출된다. 일반적인 초기화 작업을 여기서 해준다.
몇가지 callback이 더 있지만, 코드를 작성하는데 있어서 꼭 알아야 하는건 _process(delta)와 _physics_process(delta) 두개의 callback 함수이다. _process(delta)는 루프를 돌며 매번 호출이 된다. 이 얘기는 프레임 레이트에 따라 매 프레임마다 불린다는 것이다. delta는 이전 프레임과 시간 간격을 게임엔진에서 인자로 넘겨준다. 대부분의 그래픽적인 처리를 여기서 하게된다.
_physics_process(delta)는 프레임 레이트와 무관하게 일정한 시간간격으로 호출된다. 타이머에 더 가깝게 생각하면 된다. 여기서는 보통 물리 시뮬레이션이나 기타 복잡한 계산을 해준다. 프레임 레이트와 무관하게 만든 이유는, 프레임 레이트는 가변적이고, 60fps이상으로도 올라갈 수 있는데, 물리 계산등을 프레임 레이트와 동일하게 해주면 프레임 레이트에 따라 계산량이 증가하기 때문이다. 고정된 시간간격이라고 했지만, 실제 실행시 조금씩 오차가 존재한다. 그래서 게임엔진이 실제 소요된 시간을 delta값으로 인자로 넘겨준다.
물리 시뮬레이션에서도 고정된 값의 시간 간격으로 계산하며 이 시간 간격을 줄일수록 더 정확한 계산이 된다. 게임에서는 실험수준의 정확도를 요구하지도 않고 성능과의 밸런스도 고려해야 하므로 이상동작이 보이지 않을 정도의 시간 간격이면 충분하다. 이 간격을 맞추는게 최적화의 하나일 것이다. 이 시간 간격은 프로젝트 셋팅에서 변경 가능하다. 메뉴의 Project>Project Settings… 를 선택하면 나오는 다이얼로그에서 아래로 죽 내려가보면 Physics 항목이 있다.

여기에서 FPS단위로 시간단위를 설정할 수 있다. 성능에 문제가 없다면, 디폴트 값인 60fps가 적절할 것이다. 이 시간단위대로 _physics_process(delta)가 호출된다.
바로 아래 항목에 있는 Jitter에 대한 호기심이 생기는데, Jitter라는 것은 _physics_process() fps와 모니터의 리프레쉬 fps가 안맞을 때 발생한다고 한다.


또한, _physics_process()에서 움직이는 물체와 _process()에서 움직이는 물체의 시간차이 때문에 발생한다고 한다. ( Fixing Jitter and stutter 참조) 내가 만든 게임에서 이런 현상이 생긴다면 해결을 시도해야 할 것이다. 여기서 자세히 다룰 내용은 아니고 관심 있다면 링크를 따라가보길 바란다. 후자의 해결책은 문서에도 나와있긴 하지만, Fixing Jittery Movement In Godot 영상도 참고할만 하다.
서론이 너무 길었다. 구현해보자. 노드트리에서 씬 옆에 슬레이트(씬이라서 영화촬영처럼 슬레이트 아이콘) 아이콘을 클릭해서 열 수도 있다.

PlayerStick의 루트 노드를 선택하고 스크립트 파일을 추가하자. 노드트리 검색창 오른쪽에 문서표시 아이콘을 클릭하거나, 마우스 우클릭의 컨텍스트 메뉴에서 ‘Attach Script’를 선택한다.

다음과 같은 다이얼로그가 뜬다.

노드 이름과 동일하게 자동으로 스크립트 이름이 생성되어 있다. 바로 Create버튼을 눌러 스크립트를 생성한다. 뷰포트가 스크립트 에디터로 바뀔 것이다.

좌측에는 열려있는 문서 목록이 뜨고, 오른쪽에는 현재 문서가 보인다. 문서가 여러개 열려있을 경우, 좌측에서 선택할 수 있다. 또한, 최상단에 보이는 아이콘을 클릭하여 뷰포트를 스크립트에디터에서 그래픽 뷰포트로 전환이 가능하다.

이 작업은 단축키로도 가능하며, 2D에디터는 Ctrl+F1, 3D는 Ctrl+F2, Script는 Ctrl+F3를 이용하면 된다. 단축키에 대해선 공식 문서를 참고하자.
앞서 말했지만, 여기서 GDScript 튜토리얼을 작성할 마음은 없다. 간단히 몇가지만 짚고 넘어가자. 일단, 생성된 스크립트 gd파일은 명시되어 있지 않지만 하나의 클래스이다. 제일 상단에 “extends KinematicBody2D”가 보이는데, KinematicBody2D를 상속받은 클래스란 얘기다. 즉, PlayerStick노드로 생성한 KinematicBody2D를 상속받아 오버라이드해서 사용하는게 되는 것이다.
보면 알겠지만, 문법이 거의 파이썬이다. 다만, 함수를 파이썬에선 ‘def’로 썼다면, 여기선 ‘func’로 쓴다. 안드로이드 코틀린에서 ‘fun’을 쓰다가 넘어왔더니 매우 혼란스럽긴 하다 ㅋㅋ
내용이 없는 함수인 _ready()가 정의되어 있는데, 콜백 함수들은 언더바(‘_’)로 이름을 시작하고 있다.
이제 사용자 입력을 받아 위아래로만 움직이는 코드를 작성할건데, _process()에 작성해야할지 _physics_process()에서 작성해야할지 혼란스러울 수 있다. 기본 가이드라인을 준다면, RigidBody나 KinematicBody는 _physics_process()에서 작성하자. 작성할 코드는 다음과 같다.
extends KinematicBody2D
export var speed = 400
func _physics_process(delta):
var direction = Vector2.ZERO
if Input.is_action_pressed("ui_up"):
direction.y = -1
if Input.is_action_pressed("ui_down"):
direction.y = 1
move_and_slide(direction * speed)
아쉽게도 현재 사용하는 워드프레스가 GDScript 문법 하이라이팅이 지원이 안되서 파이썬으로 설정했는데, 파이썬에 없는 키워드들은 하이라이팅이 제대로 표시가 안되는거 같다.
코드를 살펴보면, 먼저 speed 변수에 스틱의 속력을 주고있다. 하나의 gd스크립트가 하나의 클래스라고 했으므로, 이렇게 사용하면 멤버변수가 된다. 앞에 export라는 키워드가 붙어있는데, 이렇게하면 에디터에서 직접 값을 변경하며 테스트 해볼 수 있다. 2D 에디터로 돌아간 후, 노드트리에서 PlayerStick을 선택하면, 오른쪽 Inspector에 다음 항목이 추가된걸 볼 수 있다.

이렇게 사용하면, 디버그 모드로 실행해서 실행중에 값을 변경해보며 테스트가 가능해진다.
_physics_process(delta)를 살펴보자. 위로 움직일지, 아래로 움직일지 결정하는 방향벡터 direction을 정의하고 초기화 하고 있다. 그 다음에 게임엔진의 Input을 이용하여 up방향키가 눌렸는지, down방향키가 눌렸는지 체크하고 방향값을 단위벡터로 설정해준다. 키를 체크할 때, 문자열로 “ui_up”, “ui_down”을 넘겨주고 있는데, 이 값은 메뉴의 Project>Project Settings… -> Input Map 탭에서 확인 및 수정이 가능하다.

여기에 있는 것들은 디폴트로 정의되어 있는 값들이다. “ui_up”은 방향키 up과 게임패드의 D-pad Up이 정의되어 있다. 이 키와 버튼이 눌리면, 스크립트에서 Action 이름인”ui_up”으로 체크할 수 있는 것이다. 여기서 새로운 Action을 추가할 수도 있고, Action에 키를 할당하거나 제거할 수 있다.
마지막으로 move_and_slide() 를 호출하고 있다. 이는 게임엔진에서 KinematicBody2D를 위해 제공하는 API로, 인자로 넘기는 주어진 벡터 방향으로 이동하다가 충돌하게되면 충돌면을 따라 미끄러지는 움직임을 만들어주는 함수이다. Godot 공식문서에서 KinematicBody2D 설명에 잘 나와있다. 문서를 읽어보면 주의할 점은, delta를 알아서 계산해준다는 것이다. 그래서 인자를 넘길 때, delta를 곱해주지 않고 초당 속력값을 그대로 넘겨준다.

이처럼 모르는게 있을 때는 직접 검색을 해봐도 되지만, 에디터의 도움말을 이용도 가능하다. move_and_slide()위에서 Ctrl키를누른채로 클릭을 하면, 에디터 내에서 관련 문서를 열어준다. 또는 F1을 눌러 직접 API검색도 가능하다. 스크립트 에디터에서 F1을 누르면 다음 화면이 뜬다.

여기서 move_and_slide를 검색해서 열어주면, 다음과 같이 도움말 파일이 열린다.

도움말 파일은 gd스크립트와 같이 왼쪽 열린 파일 리스트에 나열된다. 문서를 완전히 닫고 싶으면 여기서 우클릭으로 close를 선택하면 된다.
이제 실행해서 움직여 보자. 위아래로 잘 움직이지만 화면을 넘어가서 안보이는걸 알 수 있다. 이를 수정하기 위해선 화면의 위 아래벽 부분을 막아줘야한다. Pong Level로 돌아가자.
… 너무 길어지고 피곤하다. 다음편에서 만들게.