본문 바로가기

레퍼런스/고도엔진

고도엔진 튜토리얼 #6 간단한 2D 게임(Simple 2D game)


튜토리얼에서 간단한 게임인 퐁을 만들어 볼 겁니다. 많은 데모가 엔진에 포함되어 있습니다만, 2D 게임의 기본적인 기능성을 소개해줄 겁니다.


자 시작해보죠, 고도엔진을 켜서 프로젝트를 시작해주세요.



에셋


몇몇 에셋이 튜토리얼을 위해 포함되어 있습니다 : 


pong_assets.zip


여러분의 프로젝트 폴더에 내용물을 압축 해제하세요.



씬 설정


오랜 전통에 따라, 게임은 640x400 픽셀 해상도를 지닙니다. 해상도는 Project Setting (프로젝트 설정을 보세요.)의 Scene/Project 세팅 메뉴 하위에서 설정할 수 있습니다. 기본 배경 색은 검은색입니다 :


../../_images/clearcolor.png

프로젝트 루트를 위해서 Node2D를 생성하세요. Node2D는 2D 엔진의 기본 타입입니다. 이후, 왼쪽 혹은 오른쪽 패들에 몇몇 스프라이트(스프라이트 노드)를 더하세요, 구분하는 부분과 공을 추가합니다. 각 노드에 이름을 줄 수 있습니다. Inspector에서 각 스프라이트의 텍스쳐를 설정합니다.


../../_images/pong_nodes.png

노드 포지션을 설정하기:

  • "왼쪽" 노드: (67, 183)
  • "오른쪽" 노드: (577, 187)
  • "구분하는 부분" 노드: (320, 200)
  • "공" 노드: (320, 188)
최종 씬 레이아웃은 다음과 같은 모습입니다 (참조: 공은 중앙에 있습니다!):

../../_images/pong_layout.png

씬을 "pong.tscn"으로 저장하고 프로젝트 속성에서 메인 씬으로 설정합니다.



입력 행동 설정


비디오 게임은 다양한 입력 방법을 사용해서 플레이 가능합니다: 키보드, 조이패드, 마우스, 터치스크린(멀티터치)... 고도엔진은 이 모두를 사용할 수 있습니다. 그러나, 여러분이 별도로 관리하는 하드웨어 작업 대신에 "Input Actions"로 입력을 정의하는 것이 흥미로울 것입니다. 이 방법으로는, 어떤 입력 방법도 사용할 수 있습니다: 여러분이 직접 정의한 게임 행동이 여러분이 설정한 버튼에 연결되어 있게만 하면 됩니다.


이 게임은 퐁입니다. 유일한 입력은 패드가 위, 아래로만 움직이면 되겠죠.


프로젝트 설정 대화 상자를 열어주세요(Scene/Project 설정), 이번에는 "Input Map" 탭으로 가볼 시간입니다.


이 탭에서, 4개의 행동을 추가해주세요:


left_move_upleft_move_downright_move_upright_move_down


원하는 키에 할당하면 됩니다. A/Z(왼쪽 플레이어) 그리고 위/아래(오른쪽 플레이어)를 대부분의 경우에 사용합니다.


../../_images/inputmap.png

스크립트


씬의 루트 노드를 위한 스크립트를 하나 만들어주시고 열어주세요(스크립트 추가에서 말했듯이요). 이 스크립트는 Node2D를 상속받습니다 :

extends Node2D

func _ready():
    pass


우선 우리는 몇몇 유용한 변수를 저장할 수 있도록 우리의 스크립트를 위한 몇몇 멤버를 정의해야 합니다. 이러한 값은 화면의 치수, 패드 및 공의 초기 방향입니다.


extends Node2D

# Member variables
var screen_size
var pad_size
var direction = Vector2(1.0, 0.0)

func _ready():
    pass


이미 알고 있듯이, _ready() 함수는 첫번째로 호출되는 함수입니다(여기서는 필요 없는 _enter_tree() 다음입니다). 이 함수 안에서는, 두 가지를 할 수 있습니다. 먼저 프로세싱을 허락할 수 있습니다: set_process(true) 함수를 하는 목적입니다. 두 번째로는 두 멤버 변수를 초기화 할 수 있습니다.


extends Node2D

# Member variables
var screen_size
var pad_size
var direction = Vector2(1.0, 0.0)

func _ready():
    screen_size = get_viewport_rect().size
    pad_size = get_node("left").get_texture().get_size()
    set_process(true)


우리는 패드 노드 중 하나를 가져와서 pad_size를 초기화 했고, 그것의 텍스쳐 사이즈를 얻었습니다. screen_size는 게임 화면과 일치하는 Rect를 반환하고 그 크기를 저장하는 get_viewport_rect()를 사용해서 초기화했습니다.


자 이제, 우리의 공을 움직이게 하기 위해 우리의 스크립트에 다른 멤버들을 추가할 필요가 있습니다.


extends Node2D

# Member variables
var screen_size
var pad_size
var direction = Vector2(1.0, 0.0)

# Constant for ball speed (in pixels/second)
const INITIAL_BALL_SPEED = 80
# Speed of the ball (also in pixels/second)
var ball_speed = INITIAL_BALL_SPEED
# Constant for pads speed
const PAD_SPEED = 150

func _ready():
    screen_size = get_viewport_rect().size
    pad_size = get_node("left").get_texture().get_size()
    set_process(true)


결국엔 _process() 함수입니다. 아래의 모든 코드는 이 함수 안에 포함되어 있습니다.


우리는 계산을 위해 몇몇 변수들을 초기화해야 합니다. 먼저 공의 위치이고(노드에서), 두 번째는 각 패드를 위한 사각형입니다(Rect2). 이 사각형들은 패드와 공의 충돌 판정을 위해 사용됩니다. 기본적으로 스프라이트는 텍스쳐를 중심으로 배치하므로, pad_size/2의 작은 조정을 추가해야 합니다.


func _process(delta):
    var ball_pos = get_node("ball").get_pos()
    var left_rect = Rect2( get_node("left").get_pos() - pad_size*0.5, pad_size )
    var right_rect = Rect2( get_node("right").get_pos() - pad_size*0.5, pad_size )


이제, 공의 작은 움직임을 _process() 함수에서 추가해봅시다. 공의 위치는 ball_pos 변수에 있기 때문에, 합산은 간단합니다 :


# Integrate new ball position
ball_pos += direction * ball_speed * delta


이 코드 라인은 _process() 함수의 각 반복 마다 호출됩니다. 이는 각각의 새로운 프레임에서 공의 위치가 업데이트됨을 의미합니다.


이제 공은 새로운 위치를 가지는 데, 우리는 이제 창의 경계나 패드에 공이 충돌하는 지를 알아야 합니다. 먼저 바닥과 천장입니다 :


# Flip when touching roof or floor
if ((ball_pos.y < 0 and direction.y < 0) or (ball_pos.y > screen_size.y and direction.y > 0)):
    direction.y = -direction.y


두 번째로, 패드입니다: 만약 하나의 패드가 건드려진다면, 우리는 공의 X축 방향을 반대로 바꾸어야 합니다. 그리고 랜덤한 Y방향을 randf() 함수를 이용해서 주어야 합니다. 우리는 스피드를 조금 높여야 할 필요도 있습니다.


# Flip, change direction and increase speed when touching pads
if ((left_rect.has_point(ball_pos) and direction.x < 0) or (right_rect.has_point(ball_pos) and direction.x > 0)):
    direction.x = -direction.x
    direction.y = randf()*2.0 - 1
    direction = direction.normalized()
    ball_speed *= 1.1


마지막으로, 만약 공이 화면 바깥으로 나간다면 게임이 종료됩니다. 이것은 공의 X 위치가 0보다 작거나 화면의 크기보다 클 때입니다. 그렇다면 게임을 다시 시작하죠 :


# Check gameover
if (ball_pos.x < 0 or ball_pos.x > screen_size.x):
    ball_pos = screen_size*0.5
    ball_speed = INITIAL_BALL_SPEED
    direction = Vector2(-1, 0)


이 모든것이 완료되면, 노드는 공의 새 위치를 다음과 같이 계산합니다 :


get_node("ball").set_pos(ball_pos)


다음으로, 우리는 패드를 움직이는 것을 허용합니다. 우리는 유저의 입력에 관해서만 이들의 위치를 움직여야 합니다. 이것은 입력 클래스를 이용해 가능합니다 :

# Move left pad
var left_pos = get_node("left").get_pos()

if (left_pos.y > 0 and Input.is_action_pressed("left_move_up")):
    left_pos.y += -PAD_SPEED * delta
if (left_pos.y < screen_size.y and Input.is_action_pressed("left_move_down")):
    left_pos.y += PAD_SPEED * delta

get_node("left").set_pos(left_pos)

# Move right pad
var right_pos = get_node("right").get_pos()

if (right_pos.y > 0 and Input.is_action_pressed("right_move_up")):
    right_pos.y += -PAD_SPEED * delta
if (right_pos.y < screen_size.y and Input.is_action_pressed("right_move_down")):
    right_pos.y += PAD_SPEED * delta

get_node("right").set_pos(right_pos)


우리는 위에 있는 설정 섹션에서 사전 정의 된 4개의 행동을 사용합니다. 플레이어가 대표 키를 누르면, 그에 일치하는 행동이 촉발됩니다. 키를 누를 때 마다, 원하는 방향 쪽으로 새 위치를 간단하게 계산하고 노드에 적용합니다.


됐습니다!! 몇 개의 줄로 간단한 퐁을 만들어버렸어요.