티스토리 뷰

728x90

개발 과정에서는 무엇을 배우든 간에 과정의 처음부터 끝까지 길을 한번 가보는 것이 매우 중요하다. 결과적으로 거칠고 다듬어지지 않은 것이라 해도 완성품을 보는 것이다. 이 과정을 통해서 할 수 있다는 자신감뿐만 아니라,  각 과정에서 필요한 것이 무엇인지, 한 단계 발전하기 위해서 내게 필요한 것이 무엇인지도 파악할 수 있다. 이번 포스팅에서는 고도 튜토리얼에서 제시하고 있는 키보드를 이용한 "피하기 게임"을 제작하는 과정을 다루어 보고자 한다.

 

게임은 키보드의 방향키로 캐릭터를 움직여서 사방에서 날아오는 괴물을 피하는 것이다. 방향키를 따라 움직이는 캐릭터로 표시하는 플레이어, 사방에서 날아오는 괴물들, 그리고 플레이어가 버틴 시간을 표시하는 정보 창 등이 이 게임의 주요 요소라 하겠다. 2D 게임으로 시작한다.

 

■ 프로젝트 생성 및 설정

일단 새로운 프로젝트를 생성한다.

 

프로젝트를 생성하면 프로젝트>프로젝트 설정 메뉴를 선택하여 위의 그림과 같이 Display>Window 에서 창의 크기와 창 크기가 바뀔 때 어떻게 처리할지를 선택한다. 예제에서는 모드를 2d로 Aspect를 keep로 선택하여 가로, 세로 비율을 유지하도록 했다.

 

 

■ 프로젝트 자원 준비하기

게임 프로젝트에 필요한 자원은 이미지, 사운드, 동영상, 폰트 등 게임 성격에 따라 다양하다. 기획자, 디자이너 등과 협업이 이루어지는 지점으로 조금 규모가 있는 프로젝트라면 스크립트 파일, 씬 파일, 이미지, 사운드, 기타 자원 등으로 폴더를 나누어서 체계적으로 관리하겠지만 이 게임은 단순하므로 고도 튜토리얼에서 제공하고 있는 이미지와 사운드만 art 폴더에 저장하고 나머지 자원과 씬 파일 등은 프로젝트의 루트 폴더(res://)에 저장한다. 게임 프로젝트의 규모에 따라 폴더를 분리해서 관리하는 전략은 협업의 여지를 높여주고 개발 및 유지 보수의 효율성을 제고하므로 개발 초기부터 적절하게 설정하는 것이 중요하다 하겠다. 게임에서 사용할 폰트는 "고도에서 한글 출력하기"를 참조해서 위의 그림처럼 복사하고(NotoSansKR-Black.otf) 애니메이션에 사용할 이미지와 사운드 파일은 아래의 링크를 다운로드하여 압축을 해제한다. 압축을 해제한 결과는 우측의 그림과 같다.

 

art.zip
1.62MB

 

■ 플레이어 씬

게임은 키보드의 방향키를 따라 움직이는 캐릭터를 표현할 플레이어(Player), 사방에서 랜덤으로 날아와 플레이어와 부딪히면 게임을 종료시키는 괴물들(Mob), 현재 버틴 시간을 표시할 정보 표시창(HUD)을 각각의 씬으로 제작한 다음에 이 씬들을 메인 씬(Main)에 통합시키는 방식으로 제작한다. 이렇게 하나의 게임을 여러 가지 독립적인 요소를 나누고 통합하는 개발 방식은 협업의 가능성, 개발 속도 향상, 코드 재사용 등 다양한 측면의 이점을 개발자에게 제공하므로 게임 기획 및 설계 단계부터 이러한 점을 감안해서 설계하고, 반대로 다른 개발자의 게임을 참조하거나 분석하는 시각에서도 먼저 게임의 씬 구조를 분석하는 것이 효과적인 분석 방법이라 하겠다.

 

플레이어 씬 제작은 씬 창에서 +다른 노드를 클릭하고 새 노드 만들기에서 Node2D>CollisionObject2D>Area2D 노드 유형을 선택하고 [만들기]로 시작한다. Area2D 노드 유형을 사용하면 노드에 대한 설명처럼 캐릭터끼리의 충돌이나 겹침 등을 인식할 수 있다. 

 

노드를 생성했으면 씬 창에서 노드의 이름을 더블클릭하여 이름을 수정하고 작업 공간 상단의 아이콘 중에서 자물쇠 모양의 잠금 버튼 우측에 있는 "객체의 자식을 선택하지 않도록 합니다"를 클릭한다. 이렇게 하면 루트 노드를 선택한 상태에서는 작업 공간에서 하위 노드의 객체를 클릭하더라도 선택되지 않으므로 작업의 혼란을 예방할 수 있다. 이 상태에서 하위 노드를 선택하려면 작업 공간이 아니라 좌측의 씬 창에서 해당 노드를 선택하고 작업하면 된다. 이 상태에서 일단 씬>씬 저장 메뉴Ctrl+S 단축키로 씬을 파일로 저장한다.

 

플레이어가 키보드의 방향키를 누를 때 해당 방향으로 움직일 캐릭터를 생성하는 단계로 Player 노드를 선택한 상태에서 새 노드 만들기를 클릭하여 Node2D>AnimatedSprite 노드 유형으로 자식 노드를 추가한다. 새 자식 노드가 추가되면 중앙의 그림처럼 인스펙터 창에서 Frames 항목의 [비었음]을 클릭하고 "새 SpriteFrames"를 눌러서 캐릭터 애니메이션을 위한 준비를 한다. 우측의 그림처럼 생성된 SpriteFrames를 클릭하면 작업 공간 하단에서 각 프레임들에 대한 이미지를 설정하는 방법으로 캐릭터 애니메이션을 제작할 수 있다.

 

작업 공간 하단의 "스프라이트 프레임" 편집 창의 좌측이 애니메이션 목록인데 좌측의 그림처럼 기본으로 되어 있는 "default"를 walk로 변경한다. 캐릭터를 좌우로 움직일 때 표시할 애니메이션이다. 다음에는 우측의 그림처럼 상단에 있는 "새 애니메이션" 버튼으로 애니메이션을 추가하고 "up"으로 이름을 변경한다.  이 애니메이션은 상하로 움직이는 캐릭터를 표현할 것이다.

 

walk와 up 애니메이션을 표시할 각 프레임의 이미지를 위의 그림처럼 좌측의 파일 시스템 창에서 애니메이션 프레임으로 끌어다 놓기 방식으로 추가한다. 프레임의 개수가 많을수록 좀 더 부드러운 캐릭터의 움직임을 표현할 수 있을 것이다. 고도 튜토리얼에서 제공하는 이미지의 크기의 조정하려면 씬 창에서 AnimatedSprite 노드를 선택하고 우측의 인스펙터에서 Node2D>Transform의 Scale 항목을 조정하면 된다. 같은 방식으로 walk 애니메이션의 프레임들도 파일 시스템 창에서 끌어다 놓기로 추가한다.

 

끝으로 플레이어 캐릭터의 충돌을 감지하기 위해서 위의 그림처럼 플레이어 노드를 선택한 상태에서 "새 노드 만들기"를 클릭하여 Node2D>CollisionShape2D 유형의 노드를 자식 노드로 추가하고 영역의 모양을 인스펙터 창에서 Shape 항목의 [비었음]>새 CapsuleShape2D를 선택하여 캡슐 모양으로 선택한다. 충돌 영역은 작업 영역에 우측의 상단 그림처럼 표시되는데 주황색 원으로 표시되는 두 개의 핸들을 끌어다 놓기 방식으로 이동시키면 우측의 하단 그림처럼 캐릭터가 감싸 지도록 영역의 크기를 조정할 수 있다.

 

■ 플레이어 스크립트

 

플레이어 씬에서 루트 노드를 선택한 상태에서 우측 상단의 노드 스크립트 붙이기로 스크립트 작성을 시작한다. 경로나 스크립트 파일명을 수정할 수 있지만 기본값대로 진행해도 된다.

extends Area2D

export var speed = 400  # 플레이어의 움직임 속도(초당 픽셀수)
var screen_size  # 화면 크기(캐릭터의 움직임을 제한 하기 위한 정보)
signal hit

func _ready():
	screen_size = get_viewport_rect().size
	position.x = screen_size.x / 2
	position.y = screen_size.y / 2    
	#hide()	#플레이어 씬 테스트 완료후 주석 해제

func _process(delta):
	var velocity = Vector2()  # The player's movement vector.
	if Input.is_action_pressed("ui_right"):
		velocity.x += 1
	if Input.is_action_pressed("ui_left"):
		velocity.x -= 1
	if Input.is_action_pressed("ui_down"):
		velocity.y += 1
	if Input.is_action_pressed("ui_up"):
		velocity.y -= 1
	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite.play()
	else:
		$AnimatedSprite.stop()

	position += velocity * delta
	position.x = clamp(position.x, 0, screen_size.x)
	position.y = clamp(position.y, 0, screen_size.y)
	
	if velocity.x != 0:
		$AnimatedSprite.animation = "walk"
		$AnimatedSprite.flip_v = false
		$AnimatedSprite.flip_h = velocity.x < 0
	elif velocity.y != 0:
		$AnimatedSprite.animation = "up"
		$AnimatedSprite.flip_v = velocity.y > 0
		
func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false		

코드 처음에 선언한 speed 변수는 캐릭터가 움직이는 속도를 조정하기 위한 것으로 변수명 앞에 export를 붙이면 해당 값을 좌측의 그림처럼 스크립트 외부의 인스펙터에서도 수정할 수 있다. 인스펙터에서 값을 수정하면 스크립트에서 설정한 값을 대체해서 적용한다.

 

screen_size 변수는 캐릭터가 화면의 크기 내에서 움직이도록 제한하기 위한 공간 정보로 씬이 활성화되면 수행하는 _ready()에서 get_viewport_rect().size로 화면 크기 정보를 가져온다. 화면 크기가 정해지면 (x, y)를 2 등분한 좌표를 캐릭터의 시작 위치로 설정하고 움직임이 있을 때마다 캐릭터의 위치가 0 ~ screen_size 내에 위치하도록 clamp() 함수로 제한한다. 주석 처리한 hide()는 게임을 시작하기 전에는 캐릭터를 숨겼다가 srart(pos)로 게임을 시작하면 캐릭터가 표시되도록 하기 위한 것으로 다른 씬과 통합하기 전 테스트 과정에서는 주석 처리한다.

 

_process() 함수는 매 프레임마다 호출되는 함수로 이 곳에서 Input.is_action_pressed() 함수로 키가 눌러졌는지 확인하여 velocity라는 2차원 벡터 변수에 이동 방향과 이동량을 설정한다. 우선은 x, y의 +1 또는 -1로 수평 및 수직 방향을 결정하고, speed 변수와 곱해서 이동량을 결정한다. 그리고 프레임 간의 시간차인 delta를 곱해서 최종적으로 이동할 픽셀 값을 결정해서 캐릭터의 위치 값인 position에 반영한다. 이 과정에서 velocity.normalized()를 호출하는데 이것은 대각선 방향으로 움직이는 경우에도 수평 및 수직으로 이동하는 것도 같은 속도로 움직이게 하기 위한 것이다.

좌측 그림의 경우 velocity.normalized()를 수행하지 않을 경우의 움직임으로 수평, 수직 방향으로 동시에 움직이는 경우가 수평만 또는 수직으로만 움직이는 것보다 많은 거리를 이동한다. 그러나, velocity.normalized()를 수행하면 우측 그림처럼 대각선 방향의 움직임도 동일한 양으로 움직이게 된다.

 

_process() 함수에서는 캐릭터의 움직임에 따라 애니메이션을 처리하는 로직도 포함하는데 움직임이 있으면 .play()로 애니메이션이 동작하도록 하고 움직임이 없으면 .stop()으로 애니메이션을 멈춘다. $에 노드 이름을 붙여서 바로 참조하고 있음을 확인할 수 있다. 수평/수직 방향의 움직임에 따라 .animation 속성에 애니메이션의 이름을 지정하고 .flip_v 및 .flip_h 속성을 이용해서 캐릭터를 뒤집어서 표시할지를 지정한다. .flip_v 및 .flip_h 속성을 이용하여 두 가지 방향의 캐릭터만 준비하여 네 가지 방향으로의 캐릭터 이동을 모두 표시할 수 있는 것이다. 대각선 방향 이동 중의 캐릭터 표현은 수평 이동에 준해서 처리한다.

 

start(pos) 함수는 게임을 정식적으로 시작하도록 하는 함수로 지정한 위치에 플레이어를 보이고 이중 충돌 방지를 위해 설정되었을 수도 있는 $CollisionShape2D.disabled 속성을 해제한다.

 

signal hit 문장은 사용자 정의 시그널을 정의한 것으로 프레이어 노드의 노드>시그널에 해당 시그널을 처리하는 함수를 작성하기 위한 목록을 볼 수 있다. 캐릭터와의 충돌을 처리하는 함수는 노드>시그널에서 body_entered를 더블클릭하고 위의 그림처럼 Player 노드에 그 처리 루틴을 작성하도록 한다.

 

func _on_Player_body_entered(body):
	hide() 
	emit_signal("hit")
	$CollisionShape2D.set_deferred("disabled", true)

 

충돌이 일어나면 수행하는 작업은 일단 플레이어를 숨김 처리하고 앞서 정의한 hit 시그널을 발생시키고 이중 충돌을 방지하기 위한 작업을 수행하는 것이다. 그런데, start() 함수에서는 $CollisionShape2D.disabled = false로 속성에 대한 직접적인 접근 방법으로 처리했지만 충돌 발생 시점에서 set_deferred()를 통해서 처리하는 이유는 충돌에 대한 고도 엔진 자체의 처리와 겹치지 않기 위한 안전 통로라고 한다.

 

이 상태에서 씬 실행(F6)을 하면 키보드 움직임에 따라 캐릭터가 움직이는 것을 확인할 수 있다. 

 

■ 괴물 씬

플레이어가 움직이면서 피할 괴물들을 표현하기 위한 씬으로 동작은 화면 가장자리에서 랜덤 방향으로 일정하게 움직이는 단순한 형태이다. 다만 여러 개의 괴물을 표현하기 위해서 "고도의 씬 개체화(instance) 맛보기"에서 다룬 개체화를 적용할 것이다.

 

새로운 씬의 제작을 위해서 "씬>새 씬"을 선택하고 플레이어 씬을 만들 때처럼 좌측의 그림과 같이 [+ 다른 노드]를 클릭하고 Node2D>CollisonObject2D>PhysicsBody2D>RigidBody2D 노드 유형을 선택하여 괴물 씬의 루트 노드를 생성한다. 루트 노드를 생성하면 노드 이름을 Mob으로 변경하고 플레이어 씬에서 처럼 자물쇠 모양의 잠금 버튼 우측에 있는 "객체의 자식을 선택하지 않도록 합니다"를 클릭한 다음 씬을 일단 파일로 저장한다.

 

루트 노드의 유형인 RigidBody2D는 물리 엔진을 통해서 오브젝트의 회전, 중력의 영향이나 충격 등을 조정할 수 있는 것으로 인스펙터 창에서 Gravity scale을 0으로 해서 중력의 영향을 받지 않도록, 즉 아래로 떨어지지 않도록 하고 괴물들끼리는 충돌하지 않도록 하기 위해서 Collision>Mask 항목의 첫 칸을 클릭하여 Layer와 동일하게 설정되어 있던 박스를 지운다.

 

플레이어 씬에서 만들었던 애니메이션 제작 방법대로 괴물의 동작별 애니메이션을 제작한다. Mod 노드를 선택한 상태에서 새 노드 만들기를 클릭하여 Node2D>AnimatedSprite 노드 유형으로 자식 노드를 추가한다. 새 자식 노드가 추가되면 인스펙터 창에서 Frames 항목의 [비었음]을 클릭하고 "새 SpriteFrames"를 눌러서 애니메이션을 위한 준비를 한다. SpriteFrames를 클릭하여 작업 공간 하단에서 위의 그림들처럼 fly, swim, walk 애니메이션에 대한 이미지들을 설정한다. 플레이어와의 차이점은 초당 프레임 개수를 3으로 줄인 것이다. 괴물의 수가 많으므로 애니메이션으로 인한 성능 이슈가 없게 한다.

 

플레이어는 멈춤과 이동이 있지만 괴물을 항상 움직일 것이므로 AnimatedSprite 노드는 인스펙터 창에서 Playing를 체크한다. Playing를 체크하면 편집기의 작업 공간에서도 애니메이션이 동작하면서 프레임 번호 항목이 계속 바뀔 것이다. 고도 튜토리얼에서 배포하고 있는 있는 괴물 이미지를 보면 walk 애니메이션도 옆으로 누워 있는데 편집을 편리하게 하기 위해서 회전 각도를 -90도로 지정하고 크기도 조금 줄여 준다.

 

플레이어 씬에서 충돌 인식을 위해 추가한 것처럼 Mob 노드를 선택한 상태에서 "새 노드 만들기"를 클릭하여 Node2D>CollisionShape2D 유형의 노드를 자식 노드로 추가하고 인스펙터 창에서 Shape 항목의 [비었음]>새 CapsuleShape2D를 선택하여 캡슐 모양의 영역을 선택한다. 주황색 원으로 표시되는 두 개의 핸들을 끌어다 놓기 방식으로 캐릭터가 감싸 지도록 영역의 크기를 조정한다.

 

끝으로 추가할 노드는 괴물이 화면 밖으로 나가는 것을 인식하기 위한 노드로 Mob 노드를 선택한 상태에서 "새 노드 만들기"를 클릭하여 Node2D>VisibilityNotifier2D 유형의 노드를 자식 노드로 추가한다.

 

자식 노드 추가가 끝났으면 루트 노드를 선택한 상태에서 우측 노드 창의 그룹을 선택하고 괴물들을 한꺼번에 컨트롤하기 위한 그룹을 생성한다. 이 그룹은 플레이어와 괴물이 충돌하여 게임이 종료되면 화면에 남아 있는 괴물들을 모두 없앨 때 사용한다.

 

■ 괴물 스크립트

Mod 루트 노드를 선택한 상태에서 우측 상단의 노드 스크립트 붙이기로 스크립트 작성을 시작한다.

 

extends RigidBody2D

export var min_speed = 150  # 최소 속도 범위 
export var max_speed = 250  # 최대 속도 범위 

func _ready():
	var mob_types = $AnimatedSprite.frames.get_animation_names()
	$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]

func _on_VisibilityNotifier2D_screen_exited():
	queue_free()

 

괴물의 스크립트는 아주 단순한데, min_speed와 max_speed는 추후 개체화 과정에서 개별 괴물이 움직일 속도를 이 범위 내에서 랜덤으로 설정하게 하는 용도로 사용한다. 괴물 씬이 활성화되면 수행되는 _ready()에서는 앞서 제작한 3가지 애니메이션 중에 하나를 랜덤으로 선택하는 작업만 하면 된다. *.frames.get_animation_names()를 수행하면 배열로 애니메이션 이름들이 전달되는데 랜덤 함수(randi)로 이 배열중에 있는 애니메이션 이름을 선택하여 설정하는 것이다. 위의 중앙에 있는 그림과 우측 그림처럼 VisibilityNotifier2D 노드를 선택한 상태에서 노드 창에서 시그널>screen_exit()를 더블클릭하여 캐릭터가 화면 바깥으로 나가면 수행될 함수에서 queue_free()로 오브젝트를 스스로 제거하도록 하면 괴물에 대한 씬과 스크립트 제작은 모두 끝난다.

 

■ 메인 씬

이제 앞서 제작한 씬들을 메인 씬으로 통합하는 과정으로 새로운 씬의 제작을 위해서 "씬>새 씬"을 선택하고 플레이어 씬을 만들때 처럼 위의 그림과 같이 [+ 다른 노드]를 클릭하고 모든 씬의 기본 노드 유형인 Node 노드 유형을 선택하여 메인 씬의 루트 노드를 생성한다. 루트 노드를 생성하면 노드 이름을 Main으로 변경한다.

 

루트 노드를 선택한 상태에서 상단의 씬 파일을 노드로 인스턴스 하기를 클릭하고 플레이어 씬을 선택하여 플레이어 씬을 개체화한다. 씬 파일을 저장하고 다음으로 진행한다.

 

메인 씬에는 타이머 3개가 필요한데 위의 그림처럼 루트 노드를 선택한 상태에서 자식 노드 추가 버튼을 클릭하고 Timer 노드 유형을 선택하는 것으로 타이머 노드 3개를 추가한다. 괴물을 주기적으로 발생시킬 MobTimer(0.5 초 간격), 괴물을 피한 시간을 점수로 계산할 ScoreTimer(1초 간격), 게임을 시작하면 처음에 잠시 대기 시간을 주기 위한 StartTimer(3초)를 생성하며 각 노드를 생성하고 인스펙터의 "Wait Time"에 시간을 초 단위로 설정한다. 다른 타이머들은 한 주기가 끝나고 다음 주기가 이어지지만 게임 시작 타이머의 경우에는 한 번만 수행할 것이므로 "One Shot" 속성을 체크해 준다. 타이머 노드를 여러 개 생성할 때는 노드 유형이 같으므로 노드 복제(Ctrl+D 단축키) 기능을 사용하면 편리하다. 플레이어를 개체화하고 타이머를 추가한 이후의 메인 씬은 우측의 그림과 같다.

 

플레이어의 시작 위치를 지정하기 위해서 좌측의 그림처럼 Position2D 노드 유형으로 자식 노드를 추가하고 우측의 그림처럼 인스펙터 창에서 Position에 플레이어의 시작 위치를 앞서 설정한 프로젝트의 화면 크기의 중앙으로 설정한다.

 

다음으로 필요한 작업은 괴물 타이머에 따라 괴물들을 주기적으로 만들어 주어야 하는데 화면의 네 방향에서 랜덤 하게 나오도록 해야 하는데 그 시작 지점을 화면의 좌상단 꼭짓점에서 시작하여 화면 둘레를 시계 방향으로 도는 경로 도상에서 랜덤 위치로 하는 것이다. 경로 정의는 루트 노드를 선택한 상태에서 자식 노드로 Node2D>Path2D 노드 유형을 추가하면 된다. 경로를 정의하는 노드를 추가했으면 경로를 따라 움직이며 캐릭터를 자동 회전시켜주는 Path2D>PathFollow2D 노드 유형을 우측의 그림과 같이 Path2D의 자식 노드로 추가한다. 노드의 이름은 각각 MobPath와 MobSpawnLocation으로 변경한다.

 

경로를 정의하는 방법은 우선 경로 노드를 선택한다. 경로 노드를 선택하면 위의 그림처럼 상단에 경로를 그리기 위한 도구들이 나타나는데 이때 "점 추가" 버튼을 누르고, 작업 공간에 가는 실선으로 표시된 화면 크기를 따라서 좌상단 꼭짓점부터 시계방향으로 각 꼭짓점을 차례대로 클릭하고 마지막으로 "곡선 닫기" 버튼을 누르면 된다. 이 경로의 자식 노드로 추가한 MobSpawnLocation 노드를 활용하면 경로 상의 위치에 따라 캐릭터를 자동으로 회전시켜 주는데 이렇게 경로를 따라 회전된 상태에서 가는 방향으로 우측 90도로 회전하면 각 캐릭터는 화면의 중심을 향하게 되고 이 상태에서 괴물이 움직일 방향과 속도로 랜덤으로 설정해서 움직이도록 하는 것이다.

 

이 작업은 게임의 배경색을 조정하기 위한 것으로 위의 그림처럼 루트 노드의 자식 노드로 Control>ColorRect 노드 유형을 추가한다. 색이 아니라 이미지 배경으로 하고 싶다면 TextureRect 노드 유형을 적용하면 된다.

 

배경이 되기 위해서는 맨 아래 레이어로 가야 하므로 메인 씬에서 첫 번째 자식 노드가 되어야 한다. 추가한 배경 노드를 선택한 상태에서 Ctrl+↑ 단축키나  콘텍스트 메뉴>위로 이동을 선택하면 노드를 위로 움직일 수 있다. 배경이 화면 가득 차게 하려면 작업 공간 상단 메뉴의 레이아웃>사각형 전체를 선택하거나 인스펙터 창에서 Rect>Size로 직접 화면 크기를 지정하면 된다. 그리고 Color 항목을 클릭하여 배경색을 선택한다.

 

게임의 질을 한층 높여주는 상당히 중요한 게임 자원인 사운드를 입히는 과정이다. 고도 튜토리얼에서 제공하는 두 가지 사운드를 적용하기 위해서 위의 그림처럼 루트 노드에 AudioStreamPlayer 노드 유형의 자식 노드를 2개 추가하고 이름을 각각 부여한다.

 

추가한 노드의 인스펙터 창에서 위의 그림처럼 Stream 항목의 [비었음]>불러오기를 선택하여 플레이할 동안의 배경 음악으로는 art 폴더에 있는 House In a Forest Loop.ogg를 선택하고 괴물과 부딪혔을 때 나는 소리로는 gameover.wav를 선택한다.

 

■ 메인 스크립트

메인 노드를 선택한 상태에서 노드 스크립트 붙이기를 수행하고 파일을 저장한다. 또한 프로젝트 설정에서 Application>Run에 메인 씬을 시작 씬으로 설정한다.

 

extends Node

onready var mob_scn = preload("res://Mob.tscn")
var score

func _ready():
	randomize()
	new_game()

func new_game():
	score = 0
	$Player.start($StartPosition.position)
	$StartTimer.start()
	$BackMusic.play()

func _on_Player_hit(): #게임 종료 처리
	$BackMusic.stop()
	$DeathSound.play()
	$ScoreTimer.stop()
	$MobTimer.stop()
	get_tree().call_group("mob_group", "queue_free")

func _on_MobTimer_timeout():
	# 테두리 경로 상의 특정 위치를 랜덤으로 설정
	$MobPath/MobSpawnLocation.offset = randi() 
	# 괴물을 개체화하여 자식 노드로 추가
	var mob = mob_scn.instance()
	add_child(mob)
	mob.position = $MobPath/MobSpawnLocation.position
	# 경로 이동 방향으로 우측으로90도 회전
	var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
	# 화면 중심에서 좌우 45도 범위 내의 랜덤 각도로 회전
	direction += rand_range(-PI / 4, PI / 4)
	mob.rotation = direction
	# 속도와 방향을 설정해 놓으면 물리 엔진이 알아서 이동 시켜줌.
	mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
	mob.linear_velocity = mob.linear_velocity.rotated(direction)

func _on_ScoreTimer_timeout():
	score += 1

func _on_StartTimer_timeout():
	$MobTimer.start()
	$ScoreTimer.start()

mod_scn은 괴물 씬을 타이머 시그널에 따라 개체화하기 위한 준비로 위의 방법으로 해도 되고, 고도 튜토리얼에 있는 대로 export하여 인스펙터 창에서 씬을 설정해도 된다. 메인 씬이 활성화되면 수행되는 _ready()에 있는 randomize()는 난수 시스템을 초기화시켜 이후 난수 함수들을 사용할 때 규칙적인 난수가 나오지 않도록 해준다. 현재는 게임을 시작할 때 새로운 게임이 바로 시작하는데 나중에 게임을 다시 시작하는 기능을 위해 new_game() 함수를 작성하고 점수 초기화, 플레이어 위치 초기화, 한 번만 수행하는 시작 타이머를 가동하는 코드를 작성한다.

 

나머지 코드들은 모두 시그널 기반으로 동작하는 함수로 일단, 좌측의 그림처럼 플레이어 노드를 선택한 상태에서 hit() 시그널을 더블클릭하여 게임을 종료하는 루틴을 작성한다. 괴물과 충돌하면 배경 음악을 중단시키고, 죽는 사운드를 플레이하고 점수 타이머와 괴물 타이머를 멈춘다. 그리고 아직 화면에 남아 있는 괴물들을 그룹으로 한꺼번에 종료시킨다.

 

다음으로는 3개의 타이머 노드를 선택하고 우측의 그림처럼 timeout()을 더블클릭하여 타이머 시그널 처리 루틴을 작성한다. 점수 타이머에서는 점수를 1을 증가시키고, 시작 타이머는 점수 타이머와 괴물 타이머를 동작시키는 간단한 동작이다. 괴물 타이머 루틴에서는 괴물을 개체화하여 자식 노드로 추가하고 테두리 경로 상의 특정 위치를 랜덤으로 가져와 설정합니다. 선택한 위치에서 이동 방향의 우측으로 90도 회전을 하면 화면의 중심을 바라보게 되는데 이때 좌우 45도 범위 내의 랜덤 각도를 방향으로 속도도 랜덤으로 받아 설정하면, 플레이어는 키보드 조작에 따라 위치를 직접 설정하지만 괴물들은 물리 엔진이 알아서 위치를 움직여 준다. 

 

이 상태에서 실행(F5 단축키) 하면 메시지도 없고, 점수도 없어서 조금 그렇지만 플레이어도 움직이고 괴물들도 각 방향으로 날아다니며 부딪히면 게임도 종료된다. 물론 배경 음악도 들릴 것이다.

 

■ 정보 표시(HUD) 씬

HUD는 헤드 업 디스플레이(Head Up Display)의 약자로 각종 게임에서 점수나 게임에 관련된 메시지를 표시하는 레이어를 의미한다. 당연히 게임의 최상단 레이어에 위치한다. 

 

씬>새 씬 메뉴를 선택하고 [+ 다른 노드]>CanvasLayer 노드 유형으로 새 씬의 루트 노드를 생성한 다음 우측의 그림처럼 씬을 파일로 저장한다. HUD 씬은 점수 표시, 2초간 보이는 메시지 표시, 게임 시작 버튼, 그리고 2초 후에 메시지를 숨기기 위한 타이머로 구성된다.

 

HUD 루트 노드의 자식 노드로 Control>BaseButton>Button 노드 유형과 Control>Label 노드 유형으로 점수, 메시지, 시작 버튼을 위한 노드를 생성하고 "고도에서 한글 출력하기"를 참조하여 폰트를 한글로 설정하고 폰트의 크기를 적절하게 조정한다. 레이블과 버튼의 위치는 우측의 그림처럼 작업 공간 상단에 있는 [레이아웃] 버튼을 이용하면 편리하게 설정할 수 있는데 점수는 "위쪽 넓게", 버튼은 "아래쪽 중앙"으로 메시지는 "수평선 중앙 넓게"를 선택하고 모두 우측 인스펙터 창에서 배열을 중앙 배열로 설정한다. 추가로 설정할 것은 메시지는 Autowrap을 사용으로 체크하고 버튼은 Margin을 조정해 주어야 버튼이 정상적으로 표시된다. 필자는 Top과 Bottom을 모두 -100으로 조정했다.

 

HUD 씬에서 메시지를 2초 후에 자동적으로 사라지게 하기 위해서 루트 노드에 타이머 노드 유형의 자식 노드를 추가하고 시간은 2초로 설정하고, 반복수행이 아니므로 "One Shot"을 사용으로 체크한다.

 

■ 정보 표시(HUD) 스크립트

HUD 루트 노드에서 노드 스크립트 붙이기를 클릭하여 스크립트 작성을 시작합니다.

 

extends CanvasLayer

signal start_game
func _ready():
	$StartButton.hide()
	
func show_message(text):
	$MsgLabel.text = text
	$MsgLabel.show()
	$MsgTimer.start()

func show_game_over():
	show_message("게임 오버 !!!")
	yield($MsgTimer, "timeout")

	$MsgLabel.text = "괴물들을 요리조리 피해보세요!"
	$MsgLabel.show()
	yield(get_tree().create_timer(1), "timeout") # 1초짜리 임시 타이머
	$StartButton.show()

func update_score(score):
	$ScoreLabel.text = str(score)
	
func _on_StartButton_pressed():
	$MsgLabel.hide()
	$StartButton.hide()
	emit_signal("start_game")

func _on_MsgTimer_timeout():
	$MsgLabel.hide()

start_game은 [게임 시작] 버튼이 눌러졌을 때 메인 씬에 보낼 사용자 정의 시그널로 메인 씬에서는 시그널 처리 루틴으로 앞서 작성해 놓은 new_game()을 연결하면 된다. show_message() 함수는 HUD 씬 자체나 메인 씬에서 메시지를 출력하기 위해 호출하는 함수로 인수로 전달된 문자열을 출력하고 2초 후에 메시지를 숨길 타이머를 작동시킨다. HUD 씬이 활성화되면 수행되는 _ready() 함수에서는 게임 시작 버튼을 숨긴다. 버튼은 게임이 종료되었을 때만 보이도록 한다.

 

show_game_over()는 플레이어와 괴물이 충돌하면 메인 씬에서 호출할 함수로 두 가지 메시지를 대기 시간과 함께 출력하고 시작 버튼을 보이는 작업을 수행한다. 사용된 yield()함수는 지정한 타이머의 이벤트를 대기하는 역할을 수행한다. 앞서 추가한 타이머는 2초 대기인데 get_tree().create_timer(1)로 1초 타임아웃으로 동작하는 임시 타이머를 생성하는 과정도 볼 수 있다. 이 타이머는 시그널 처리 루틴도 지정되지 않으므로 메시지도 없어지지 않는다.

 

나머지 코드는 시그널에 반응하는 처리 루틴으로 버튼은 노드 창의 시그널 탭에서 pressed(),  타이머는 timeout()을 더블클릭하여 시그널 처리 루틴을 작성한다.

 

HUD 씬과 스크립트 작성이 끝났으면 씬을 저장하고, 위의 그림처럼 HUD 씬을 메인 씬의 자신 노드로 개체화한다.

 

우선 HUD 씬에서 게임 시작 버튼을 누르면 발송하는 start_game() 시그널을 받을 처리 루틴을 앞서 작성해 놓은 new_game() 함수로 연결한다. 

 

func new_game():
	score = 0
	$Player.start($StartPosition.position)
	$StartTimer.start()
	$BackMusic.play()
	$HUD.update_score(score)
	$HUD.show_message("준비하세요!")

HUD의 점수를 갱신하고 게임을 준비 메시지를 추가한 new_game() 함수의 내용은 위와 같다.

 

func _on_Player_hit(): #게임 종료 처리
	$HUD.show_game_over()
	$BackMusic.stop()
	$DeathSound.play()
	$ScoreTimer.stop()
	$MobTimer.stop()
	get_tree().call_group("mob_group", "queue_free")

플레이어와 괴물이 충돌하면 처리하는 함수에는 위와 같이 HUD의 게임 종료 함수 호출을 추가한다.

 

func _on_ScoreTimer_timeout():
	score += 1
	$HUD.update_score(score)

점수 타이머에도 위와 같이 점수를 갱신하는 로직을 추가한다.

 

이제 최종 결과를 테스트해 보면 위의 그림과 같다. 차례대로 게임 시작 화면, 게임 도중의 화면, 게임 종료 시 화면이다.

 

첫 게임을 만들어본 경험으로는 "아우 매력적인데!" 하는 총평이다. 게임 제작 도구를 처음으로 접하는 것이지만 잘 선택했다는 느낌이다. 파이썬을 경험한 개발자라면 더욱 친근감 있는 코딩을 할 수 있을 것으로 보인다. 프로젝트의 세분화와 최적의 분업 및 통합이라는 시각에서도 매력적이었다. 앞으로가 더욱 기대가 되는 고도 개발 도구이다.

 

 

728x90
댓글
글 보관함
최근에 올라온 글
최근에 달린 댓글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31