저번에 남은일이 폴리싱이라고 얘기했었는데, 사실 생각보다 할게 많다. 개발자는 항상 남은일을 추정할 때 이렇게 막연하게 긍정적일 때가 많다. 나처럼… 일정을 산출할 때 항상 경계하자. 어쨌든, 이번에 할 일은 스코어를 기록하고 게임을 재시작 하는 일이다.
스코어 기록하기: Area2D 배치
이걸 하기 위해선 먼저 공이 화면을 벗어났는지 알아야 한다. 여기에 사용되는 노드가 Area2D 이다. Area2D는 영역에 오브젝트가 들어왔는지, 나갔는지등을 체크할 수 있다. 문서를 보면 더 다양한 기능이 있긴 하지만, 여기서는 일종의 트리거 박스라고 생각하면 되겠다. 이 Area2D를 화면 양옆에 배치해서 공이 영역에 들어오면, 상대편이 득점 한 것으로 판단이 가능하다.
Pong Level 씬을 열고, 노드트리에서 Area2D를 추가한다. 이름은 LeftArea로 바꿔준다. 노란 경고 마크가 뜰텐데, Area2D도 자식노드로 CollisionShape2D를 필요로 한다. 이 노드를 추가해주고, RectangleShape2D로 Shape을 설정해준다. 이렇게 만들어준 노드는 노드트리에서 다음과 같이 보일 것이다.

이제 뷰포트에서 적당히 왼쪽 공간을 채워준다. 채우는 영역도 스크린 밖 영역이지만, 논리적인 공간일 뿐이므로 화면에 보여지는 것은 없다. 다음 이미지처럼 화면을 채워주자. 공이 이 영역에 들어오면 득점한 것으로 처리할 것이다.

배치는 Inspector에서 수치를 직접 입력하여 적용했다. 먼저, LeftArea의 Position을 다음과 같이 맞춰준다.

그리고, CollisionShape2D의 크기를 다음과 같이 정해준다.

왜 이렇게 이중으로 설정하냐면, 부모인 Area2D의 중심과 CollisionShape2D의 중심을 맞춰주기 위함이다. CollisionShape2D는 항상 Area2D의 중심에 붙어 있으므로, 이렇게 하는게 추가작업이 필요할 때 편할 것이다.
OpponentStick의 컴퓨터 알고리즘 작성 때, 공이 화면을 벗어난 후에도 공을 따라 움직이는걸 보고 눈치 챘을지 모르겠지만, 특별히 처리를 해주지 않는이상, 화면을 벗어났다고 무시하거나 처리하지 않는건 아니다. Area2D의 영역 역시, 스크린 밖에 위치하지만 처리가 가능하다.
오른쪽에도 마찬가지로 배치해준다. 위 아래 Wall을 만들 때 했던 것처럼, 이미 만들어놓은 LeftArea를 duplicate해서 사용하면 편하다. LeftArea를 선택한 후, Ctrl+D를 눌러 duplicate한다. 복제된 LeftArea2의 이름을 RightArea로 바꿔주고, 위치만 오른쪽으로 이동시켜준다. 노드트리는 다음과 같이 보일 것이다.

RightArea를 선택하고 Inspector에서 position을 다음과 같이 설정하여 오른쪽으로 이동시켰다.

이 작업을 해보면, Left Area 작업시, Area2D 노드에서 위치를 지정하고, 자식인 CollisionShape2D에서 크기만 지정했는지 알 것이다. 여기서 RightArea의 위치계산이 1280 + 60 으로 간단했는데, Area2D와 CollisionShape2D가 따로 논다면 이 계산이 복잡해진다.
이제 이 Area2D에 공이 들어왔는지 체크를 해야한다. 어떻게 해야할까? 이전에 알고있는 내용을 쥐어 짜내보면, _process() 내에서 매 프레임마다 공의 위치를 얻어오고 그 위치가 이 박스 내부인지 체크할 수 있을 것이다. 유효한 방법이지만, 이럴거면 Area2D의 존재가 필요없다. 여기서 Godot의 핵심 개념중 하나인 시그널(Signal)을 알아야 한다.
시그널(Signal)
윈도우즈 API 프로그래밍같은 이벤트 드리븐 프로그래밍을 해봤다면, 배후에서 일어나는 계산들은 시스템에 맡기고 onKeyDown, onResize등 해당 이벤트 발생시 이에대한 처리만 해주는 작업에 익숙할 것이다. 이런 작업은 원하는 값에 변경이 있는 경우에만 처리하는 옵저버 패턴(Observer Pattern)으로도 구현된다.
시그널(Signal)은 이벤트 발생시, 노드간에 주고받는 메세지다. 객체지향적인 디자인으로 다음 다이어그램처럼 도식화가 가능하다.

Godot에서 시그널은 옵저버 패턴을 이용하여 구현된다. 실제 예들은 다음과 같은 경우들이 있을 것이다.

Godot에서 Signal의 처리는 공식문서를 참고하자. 코드레벨에서 옵저버패턴처럼 이용이 가능하지만 이는 링크된 공식문서를 참고하도록 하고, 여기서는 에디터의 이용법을 알아보자.
Pong으로 돌아가서 LeftArea에 공이 들어오면, 이것을 감지하고 시그널을 발생시킬 주체가 LeftArea인건 명확하다. LeftArea를 선택한 후, Inspector 옆의 Node 탭을 클릭해보자. 다음과 같은 것이 보일 것이다.

Signals와 Groups가 보이고 Area2D가 보내는 시그널들이 나열된게 보인다. 헷갈릴수 있는데, area_enterd(), body_entered()등의 구분은 영역에 들어오는 오브젝트가 무엇인지로 구분되며, 인자로 넘어온다. 우리는 공이 영역에 들어왔는지 체크해야 하므로 body_entered()를 더블클릭한다. 다음과 같은 다이얼로그가 보일 것이다.

시그널을 받을 스크립트(Connect to Script)를 선택하는 리스트와 받을 메소드 이름(Receiver Method)가 보일 것이다 (메소드 이름은 실수로 area_entered()를 클릭한 화면을 캡쳐했다. body_entered로 보이는게 맞다). 이 시그널을 누가 받아야 할까?
Pong 게임을 만들면서 아직 고려를 안한 부분이 있다. 게임의 스코어나 전체 상태를 관리하는 매니저의 존재이다. 이에 관련해서 공식 문서에 Singtons(AutoLoad) 부분에 잘 설명되어 있다. 프로젝트 설정에서 자동으로 로드될 노드나 스크립트를 설정해 놓으면, 싱글톤 패턴을 이용하여 게임 시작시 씬에 포함되어 있지 않아도 자동으로 로딩이 된다.
이와같은 글로벌 매니저의 존재는 레벨 씬이 여러개일 경우 필수 일 것이다. 그러나 여기서는 단일 레벨 씬이 사용되고 있고, 굳이 오토로드를 사용하지 않더라도 레벨 씬에 스크립트를 작성하여 그 역할을 할 수 있다. 오랜 프로그래밍 경력에서 뛰어난 프로그래머가 되진 못했지만 확실히 배운 것이 있다면, 미리 예측해서 과잉 구현을 하지 말라는 것이다. 지금 요구사항에서 필요한 만큼만 구현하는게 합리적인 판단이다. 개발에서 시간은 가장 중요한 리소스중 하나이며, 미래는 예측대로 흘러가지 않는다. 장황한 얘기를 해버렸지만, 여기서는 단일 레벨 씬이므로 오토로드를 사용하지 않는게 맞다는 얘기를 하고 싶은 거다. Pong Level에 attach script를 사용해서 스크립트 파일을 생성하자.

이제 다시 시그널로 돌아가서 LeftArea를 선택하고, Inspector 옆의 Node 탭에서 body_entered()를 더블클릭해보자. 다음과 같이 생성한 Pong Level 스크립트가 보일 것이다.

다이얼로그에서 Pong Level 스크립트를 선택하고 Connect를 누른다. Pong Level 스크립트에 해당 메소드인 _on_LeftArea_area_entered()가 생성된걸 확인 할 수 있다.

특이한점은, 에디터 왼편에 동그라미 친 부분처럼 connect 표시 아이콘이 보인다. 해당 아이콘을 클릭해보면, 다음과 같이 시그널에 대한 정보가 표시된다.

Source에 시그널을 보내는 노드가 보이고, 시그널 이름이 뭔지, 그리고 Target에 시그널을 받는 대상이 표시된다.
노드트리에서도 다음과 같이 변경사항을 확인 할 수 있다.

LeftArea에 마치 와이파이를 닮은 아이콘이 보일 것이다. 해당 아이콘을 클릭하면, Inspector옆 Node탭이 표시될 것이다.

시그널을 연결하기 전과 다르게, body_entered() 시그널이 어디에 커넥트 되어 있는지 표시된걸 확인 할 수 있다. 이 커넥트된 항목을 더블클릭하면, 해당 스크립트의 메소드로 바로 이동이 된다.
이제 LeftArea에서 했던 것처럼 RightArea도 body_entered() 시그널을 연결해보자. 똑같으므로 알아서 해보기. 시그널을 연결하고 나면, Pong Level 스크립트가 다음과 같이 변경되어 있을 것이다.

스코어 기록하기: Pong Level에 스코어 저장
제일 먼저 할 일은 Pong Level 스크립트에 스코어를 기록하는 일이다. 다음과 같이 변수 두개를 만들고 공이 어느쪽으로 나가느냐에 따라 상대방의 스코어를 올리도록 해주자. 코드는 다음과 같다.(여기에선 오류가 있으니 끝까지 봐주길)
extends Node
var PlayerScore := 0
var OpponentScore := 0
func _on_LeftArea_body_entered(body: PhysicsBody2D):
OpponentScore += 1
print("Opponent Score = %d" %OpponentScore)
func _on_RightArea_body_entered(body: PhysicsBody2D):
PlayerScore += 1
print("Player Score = %d" %PlayerScore)
코드를 살펴보자. 에디터에서 생성한 코드에선 넘겨받는 인자가 그냥 “body”일 것이다. GDScript가 기본적으로 phthon과 같이 동적 타입 언어라서 그렇다. 동적 타입 언어는 장점도 있지만, 가독성을 매우 떨어트린다. 그 예로, “body.na” 까지 입력해보면, 에디터에서 body가 어떤 타입인지 모르기 때문에, 속성을 참조하려고 해도 자동완성이 되지 않는다. 동적 타입 언어지만 위와같이 body의 타입을 명시해주면, 정적 타입처럼 사용이 가능하고 자동완성도 동작하는걸 확인 할 수 있다.
print()문은 디버깅용으로 아래쪽 Output 콘솔창에 출력된다. print문은 여러가지로 출력이 가능한데, 문자열 출력에 대해선 format string을 참조하자. 어? 그런데 실행해보면 이상하다. 점수는 1점만 올라가야 하는데, 다음과 같이 출력된다.

body_entered 시그널을 여러개 받고 있는게 보인다. 왜이럴까? 다음과 같이 넘겨받는 인자인 body의 name을 찍어보자.
func _on_LeftArea_body_entered(body: PhysicsBody2D):
print("body name = %s" %body.name)
OpponentScore += 1
print("Opponent Score = %d" %OpponentScore)
func _on_RightArea_body_entered(body: PhysicsBody2D):
print("body name = %s" %body.name)
PlayerScore += 1
print("Player Score = %d" %PlayerScore)
다시 실행해보면, 출력이 다음과 같이 보인다.

아하, Area2D를 만들 때, WallTop과 WallBottom이 다음과 같이 겹치도록 했었다.

충돌 영역을 조정해서 해결해도 되겠지만, 코드상에서 PongBall 인지 체크하는 코드를 추가하자.
func _on_LeftArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
OpponentScore += 1
print("Opponent Score = %d" %OpponentScore)
func _on_RightArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
PlayerScore += 1
print("Player Score = %d" %PlayerScore)
실행해보면, 정상 동작함을 확인 할 수 있다.
스코어를 획득하면, 볼을 다시 초기화해서 게임을 계속하도록 해야한다. PongBall.gd 스크립트 파일을 열고 reset() 함수를 추가해보자.
func reset():
position.x = 640
position.y = 360
init_random_direction()
이제 이 초기화 메소드를 Pong Level.gd에서 스코어 득점을 했을 때 불러줄 것이다.
func _on_LeftArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
OpponentScore += 1
print("Opponent Score = %d" %OpponentScore)
$PongBall.reset()
func _on_RightArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
PlayerScore += 1
print("Player Score = %d" %PlayerScore)
$PongBall.reset()
get_node()를 사용하는 대신, 자식 노드이므로 ‘$’ 표현식을 이용해 “$PongBall”로 노드의 참조를 얻어왔다. 그리고 방금 추가한 reset() 메소드를 호출해주면 된다. 실행해보면, 공이 나갔을 때, 너무 바로 실행되긴 하지만, 게임이 계속 진행되는걸 확인 할 수 있다. 또한, Output 콘솔창을 통해, 스코어도 정상적으로 올라가는걸 확인할 수 있다.
스코어 기록하기: UI를 이용해 화면에 표시
이제 이 스코어를 화면에 표시해보자. UI요소를 사용할 것이다. UI요소는 모두 control 노드 하위에 존재한다. Pong Level을 열고 Ctrl+A를 눌러 새 노드 추가 다이얼로그를 열면 다음과 같이 확인가능하다.

UI만으로도 꽤 많은 얘기를 해야한다. 노드 하나하나 설명을 하진 않겠지만, 기본적인 앵커와 마진에 대한 설명이 필요하다. 보다 자세한 내용을 알고 싶다면 공식 문서의 User Interface를 참조하기 바란다.
고정된 해상도의 스크린이라면 문제가 매우 간단하나, 현실에서는 각종 모니터부터 핸드폰까지 여러 형태의 스크린 사이즈가 존재한다. 앵커 시스템은 가변적인 스크린 또는 부모 컨트롤 사이즈에 대응하기 위한 방법이다. 유니티에서도 이걸 사용하고 있다. 다음 그림을 보자.

그림이 복잡해 보일지 모르겠는데, 간단한 개념이다. 가장 큰 사각형이 뷰포트, 스크린 사이즈라고 생각한다면, 먼저 기준점이 되는 앵커를 박는다. 닻을 의미하는 앵커라고 하는 이유는 스크린 사이즈에서 해당 위치에 고정되기 때문이다. 다만, 고정되는 위치가 절대적인 좌표가 아닌 스크린의 비율에 맞는 위치가 된다. 비율로 따지기 때문에 Anchor Left/Right의 경우, 0~1 사이 값을 가지며, 0은 왼쪽 끝, 1은 오른쪽 끝이된다. Anchor Top/Bottom의 경우에도 0~1사이 값이며 0이 최상단, 1은 최하단을 의미한다. 이 값은 Control을 선택한 후, Inspector에서 Control>Anchor 부분에서 확인가능하다. 아래 이미지는 Godot에서 임의로 앵커위치를 시키고 버튼 컨트롤을 위치시킨 모습이다.

위 이미지에서 앵커는 녹색 핀셋모양으로 표시된다. 이 경우, Inspector에서 Control>Anchor를 확인해 보면 다음과 같다.

Anchor의 Left, Top, Right, Bottom이 0~1 사이값으로 표시됨을 확인 할 수 있다. 조금 혼동할만한 부분은 Right라고해서 오른쪽이 기준점이 아니라는 점이다. 왼쪽이 0, 오른쪽을 1로 봤을 때, 그 중간 위치값이다.
그 다음, Margin값은 앵커로부터 거리를 나타낸다. 앵커는 화면 비율이었지만, Margin값은 실제 해상도(픽셀) 값이다. 또한, 각각 Margin Left는 Anchor Left로부터의 거리, Margin Right는 Margin Right로부터의 거리로 표시된다. 즉, 위 버튼처럼 앵커 사각형 내부에 존재하는 경우, Margin Right는 마이너스의 값을 갖는다. 위 버튼의 경우, Inspector에서 Margin값을 살펴보면 다음과 같다.

Margin값이 픽셀 단위로 표시되고, Right값은 마이너스인걸 확인 할 수 있다.
앵커는 스크린 사이즈에 따라 변경이 된다. 또한, 컨트롤의 각 모서리가 각 앵커에 고정되어 있는 형태이므로 컨트롤도 앵커를 따라 움직이거나 크기가 변형될 수 있다. 위의 버튼 예제에서 Inspector의 Anchor값을 움직여 보면, 버튼이 늘어나고 줄어든걸 확인 가능하다.

이걸 이용하면, 다양한 사이즈의 화면 크기에 맞게 UI 크기도 맞출 수 있는 것이다.
그림에선 마치 앵커 사각형내에 컨트롤이 위치하는 것처럼 보이지만, 앵커의 위치는 화면 어디에나 가능하기 때문에, 얼마든지 임의로 배치가 가능하다. Unity에서도 앵커 시스템을 사용한다고 했는데, 유니티 문서에 보다 이해가 쉬운 직관적인 이미지가 있어 가져와봤다.

위 이미지는 앵커가 모두 중앙에 모여있는 경우이다. 이렇게 앵커를 모아놓으면 컨트롤을 화면에 상대위치에 배치하면서 컨트롤 자체의 크기 변경은 일어나지 않는다.

앞에서와 비슷하지만, 오늘쪽 아래에 앵커를 모두 모아놓은 경우이다. 높이 변경에는 비율대로 움직이지만, 폭의 변경에는 오른쪽을 따라가서 왼쪽이 넓어지는걸 볼 수 있다.

이 경우는 앵커가 좌우 아래에 모여있다. 높이의 변경은 이전과 같게 동작하지만, 폭의 변경에는 좌우가 고정되어 있으므로 컨트롤의 폭도 같이 변경되고 있다.
예제로 살펴봤듯이 대부분의 앵커들은 임의의 위치보다는 화면의 중앙 또는 사방 끝부분에 모여있거나 위치하게 된다. 그래서 미리 자주쓰는 위치를 목록으로 만들어서 유저가 쉽게 사용하도록 제공한다. 바로 뷰포트 위에 보이는 Layout이 그것이다.

Layout을 눌러보면, 다양한 프리셋들을 확인 할 수 있다.

UI 배치의 가장 기본적인 앵커와 마진에 대해 알아봤다. 이를 이용해서 점수를 표시할 Label을 위치시켜보자. 먼저, Pong Level 아래에 Label 노드를 추가하자.

노드트리에서 이름을 PlayerScore로 변경한다.

배치하는 방법은 앞에서 배운 앵커를 이용해 자유롭게 해도 된다. 만들고 있는 Pong 게임은 프로젝트 세팅에 따라 고정된 스크린 사이즈이기 때문에 영향을 받지 않는다. 여기서는 Center Top 레이아웃을 이용해 배치했다.

위 이미지에 있는 Layout의 Center Top을 선택했다.

Label의 Inspector로 속성 부분에서 Text에 임시로 테스트를 위해 “012”를 입력해놨다. 게임 실행시, 스코어가 대체될 텍스트이다. Align과 V Align은 둘 다 Center로 설정하여 중앙에 위치시켰다.

Center Top Layout을 선택해서 Anchor값이 위와같이 0.5/0/0.5/0 인 것을 확인할 수 있다. Margin값을 지정해주면, 앵커 하나로부터 거리이기 때문에 자동으로 사이즈가 정해진다. 뷰포트에서 적당한 위치와 크기를 잡은다음, Inspector에서 세부값을 조정하면 쉽게 설정이 가능하다. 여기서는 Margin값을 -200/50/-50/150 으로 지정했다. 이에 따라 Size도 150X100 으로 자동으로 정해진다.
문제가 하나 있을텐데, 글자가 너무 작게 보인다는 점이다. 글자 크기도 키울겸, 폰트를 다운받아 사용해보자. 잘 찾아보면 무료 폰트도 많이 구할 수 있겠지만, 저작권이 은근히 애매한 부분이다. Godot Asset Library에서 오픈 폰트 라이센스로 제공되는 폰트가 있으니 이를 받아보자. 먼저 제일 상단에 보이는 뷰포트 선택부분에서 AssetLib를 선택한다.

뷰포트가 애셋 라이브러리 리스트로 변경되는걸 확인할 수 있다. 상단의 검색창에 font를 입력해 검색해본다. 다음과 같이 폰트 애셋들만 리스트업된다.

위 이미지에서 빨간색으로 표시한 Open Font Package를 받자. 오픈 폰트 라이센스를 따르는 무료 폰트 셋을 받을 수 있다.

클릭하면 위와같이 애셋 라이브러리 정보가 표시된다. 다운로드를 클릭하자.

위와 같은 애셋 인스톨 화면이 나온다. 위 이미지는 이미 한번 설치를 했기 때문에, 체크가 해제되어 있지만 처음 설치시에는 다 체크가 되어 있을 것이다. Install을 클릭해서 설치를 하게되면, FileSystem Browser에 해당 파일들이 위치하는걸 확인할 수 있다.
이제 다운받은 폰트를 설정해보자. 노드트리에서 PlayerScore 레이블 노드를 선택한 후, Inspector 부분을 보면 Theme overrides 항목에서 설정이 가능하다. Theme overrides > Font 아래에 Font항목을 체크한다. 폰트를 오버라이드해서 사용자 폰트를 사용하겠다는 의미이다. 체크한 Font항목에서 NewDynamic Font를 선택한다. 선택한 Dynamic Font를 클릭하면, 하위 항목들이 나오는데, Font > Font Data에 다운받은 폰트중에서 맘에드는 폰트의 ttf파일을 설정한다. 여기서는 Xolomium-Bold.ttf를 설정했다. 폰트에 따라 크기가 다르게 적용되므로, 폰트를 설정한 후 Dynamic Font의 하위항목중에 Settings 항목을 열어서 폰트 크기를 설정한다. 여기서는 40으로 지정했다. 설정한 항목들은 다음과 같이 보일 것이다.

폰트까지 적용을 하고나면, 화면에 다음과 같이 보일 것이다.

PlayerScore를 duplicate하여 OpponentScore도 만들자. 노드트리에서 선택한 후, Ctrl+D를 누르면 복제가 된다. 여기에 Margin 값만 Left : 50, Right : 200으로 설정하면 오른쪽으로 이동이 될 것이다.

최종 결과물은 다음과 같다.

이제, 점수를 획득할 때마다 이 레이블을 업데이트 해야한다. 방법은 여러가지가 있다. Pong Level.gd 스크립트에서 업데이트시 레이블을 변경해도 되고, 레이블에서 매 프레임마다 스코어 값을 읽어올 수도 있을 것이다. 아마도 가장 이상적인건, Pong Level.gd에서 스코어가 변경되면 시그널을 발생시키고, 레이블에선 이 시그널을 받아서 값을 업데이트 하는 방법이다. 이렇게 시그널을 이용하는 방법이 객체간 의존도를 없애주기 때문이다.
시그널을 이용하려면 새로 시그널을 정의해야 한다. 커스텀 시그널의 사용법은 공식문서에 나와있다. 먼저 스크립트에서 시그널을 정의해야 한다. Pong Level.gd 스크립트 파일의 상단에 다음과 같이 커스텀 시그널을 정의하자.
extends Node
signal player_score_updated(score)
signal opponent_score_updated(score)
var PlayerScore := 0
var OpponentScore := 0
인자가 없는 시그널을 정의할 수도 있지만, 위와같이 인자를 표시해주면 시그널에 인자를 같이 넘겨줄 수 있다. 이렇게 정의하게 되면, 에디터 화면에서 시그널이 추가된걸 다음과 같이 확인할 수 있다.

코드상에서 시그널을 발생시키는 방법은 emit_signal() 함수를 쓰는 것이다. 인자로는 시그널 이름 스트링과 뒤에 인자를 추가할 수 있다. Pong Level.gd를 수정하여 시그널을 발생시켜보자.
func _on_LeftArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
OpponentScore += 1
print("Opponent Score = %d" %OpponentScore)
emit_signal("opponent_score_updated", OpponentScore) # <-- here
$PongBall.reset()
func _on_RightArea_body_entered(body: PhysicsBody2D):
if body.name == "PongBall":
PlayerScore += 1
print("Player Score = %d" %PlayerScore)
emit_signal("player_score_updated", PlayerScore) # <-- here
$PongBall.reset()
이제 시그널 부분에서 배웠듯이, 각 시그널을 더블클릭하여 해당하는 스코어 레이블에 연결하면 되는데, 그러려면 각 스코어 레이블도 스크립트 파일을 attach 해야한다. 각각 추가하자. 이제 시그널을 연결해주면된다. 시그널에 전달하는 인자가 int이기 때문에 str() 함수를 이용하여 스트링으로 변환해서 text 속성에 넣어준다. 완성된 스크립트 파일들은 다음과 같다.
extends Label
func _ready():
text = "0"
func _on_Pong_Level_player_score_updated(score: int):
text = str(score)
extends Label
func _ready():
text = "0"
func _on_Pong_Level_opponent_score_updated(score: int):
text = str(score)
레이블의 텍스트에는 더미값인 “012”를 넣어 놨었기 때문에, 시그널이 발생하기 전에는 “012”가 표시된다. 이를 해결하기 위해 _ready()에서 값을 초기화해 줬다. 시그널을 받는 부분에서는 인자에 타입 힌트로 int를 넣었으며, int를 String으로 변환하기위해 str() 함수를 사용하고 있다. 이제 실행해 테스트해보자. 정상적으로 스코어가 올라가는걸 확인 할 수 있다.
아니, 폴리싱만 남았다며… 더 해야할 일은?
분명한 착오였다. 이건 첫 게임을 만드는 튜토리얼이고 기본적인 항목들은 다 짚고 넘어가야 한다. 여기서는 시그널과 UI에 대해 얘기해야 했다. 이젠 진짜, 타이머랑 사운드 추가만 남았는데, 포스팅이 너무 길어져서 한번 더 미뤄야 겠다. 그럼 이만.
1 thought on “처음 접하는 Godot: Pong 게임을 만들어 보자 #4”