Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

점수 및 재시작

이 파트에서는 스코어, 음악 재생 및 게임을 재시작할 수 있는 코드를 추가하겠습니다.

변수의 현재 점수를 추적하고 최소한의 인터페이스를 사용하여 화면에 표시해야합니다. 이것을 위해서는 텍스트 레이블을 사용할 수 있습니다.

메인 씬에서 `Main`의 새로운 자식 노드로 :ref:`Control <class_Control>`을 추가하고, 이름을 `UserInterface`로 지정하세요. UI를 편집할 수 있는 2D 화면에 있는지 확인하세요.

Label 노드를 추가하고 노드 이름을 ``ScoreLabel``로 지정

image1

*인스펙터*에서 *Label*의 *Text*에 "Score: 0"과 같은 플레이스홀더를 설정하세요.

image2

또한, 텍스트는 기본적으로 게임의 배경과 같은 흰색입니다. 게임 실행 시 텍스트가 보이도록 색상을 변경해야 합니다.

테마 오버라이드까지 스크롤한 다음, 색상을 펼치고 글꼴 색상를 활성화하여 텍스트를 검정색으로 틴트를 입히세요. (이렇게 하면 흰색 3D 씬과 대비가 잘 됩니다)

image3

마지막으로, 뷰포트에서 텍스트를 클릭하고 드래그하여 왼쪽 상단 모서리에서 떨어지도록 이동하세요.

image4

UserInterface 노드를 사용하면 씬 트리의 한 지점에 UI를 그룹화하고 모든 자식에 전파되는 테마 리소스를 사용할 수 있습니다. 이를 사용하여 게임의 글꼴을 설정할 수 있습니다.

프로젝트 생성하기

다시 한 번 UserInterface 노드를 선택합니다. *인스펙터*에서 *Theme -> Theme*에 새 테마 리소스를 만드세요.

image5

테마를 클릭하여 아래 패널에서 테마 편집기를 엽니다. 이 편집기는 테마 리소스를 사용한 기본 UI 위젯들이 어떻게 보일지 미리 볼 수 있게 해줍니다.

|image1|

기본적으로 테마에는 몇 가지 속성만 있습니다: Default Base Scale, Default Font, Default Font Size.

더 보기

테마 리소스에 더 많은 속성을 추가하여 복잡한 사용자 인터페이스를 디자인할 수 있지만, 이는 본 문서의 범위를 벗어납니다. 테마의 생성과 편집에 대해 더 알고 싶다면 :ref:`doc_gui_skinning`을 참조하세요.

*Default Font*에는 컴퓨터에 있는 글꼴 파일을 사용합니다. 일반적인 글꼴 파일 형식으로는 TrueType Font (TTF)와 OpenType Font (OTF)가 있습니다.

파일시스템 독에서 fonts 디렉터리를 펼치고, 프로젝트에 포함된 Montserrat-Medium.ttf 파일을 디폴트 글꼴로 드래그하세요. 그러면 테마 미리보기에서 텍스트가 다시 나타납니다.

텍스트가 조금 작습니다. 디폴트 글꼴 크기22 픽셀로 설정하여 텍스트 크기를 늘리세요.

|image7|

점수 추적하기

다음으로 점수가 동작하게 해봅시다. ScoreLabel에 새로운 스크립트를 추가하고 score 변수를 정의하세요.

extends Label

var score = 0

몬스터를 잡을 때마다 점수가 1씩 올라가도록 해야 합니다. 이를 위해 몬스터가 잡힐 때 발생하는 squashed 시그널을 사용할 수 있습니다. 다만, 몬스터를 코드에서 생성하기 때문에, 편집기에서 직접 mob 시그널을 ScoreLabel에 연결할 수는 없습니다.

그 대신에, 매번 몬스터를 소환할 때마다 코드로 연결해야 합니다.

main.gd 스크립트를 엽니다. 아직 열려 있다면, 스크립트 편집기의 왼쪽 열에서 이름을 클릭하여 열 수 있습니다.

|image8|

또는 파일시스템 독에서 main.gd 파일을 더블 클릭하여 열 수도 있습니다.

_on_mob_timer_timeout() 함수의 하단에 다음 줄을 추가하세요:

func _on_mob_timer_timeout():
    #...
    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

이 라인은 mob가 squashed 시그널을 보낼 때 ScoreLabel 노드가 이를 받아 _on_mob_squashed() 함수를 호출하도록 한다는 의미입니다.

플레이어 스크립트로 돌아가서 스쿼시 및 바운스를 코딩하세요.

여기서 점수를 증가시키고 화면에 표시되는 텍스트를 업데이트합니다.

func _on_mob_squashed():
    score += 1
    text = "Score: %s" % score

두 번째 줄에서는 score 변수의 값을 %s에 대체하여 사용합니다. 이 기능을 사용하면 Godot가 자동으로 값을 문자열로 변환해 주기 때문에, 레이블에 텍스트를 출력하거나 print() 함수를 사용할 때 편리합니다.

이제 게임을 실행해 적을 잡아보면 점수가 올라가는 것을 확인할 수 있습니다.

|image9|

참고

복잡한 게임에서는 사용자 인터페이스를 게임 세계와 완전히 분리하고 싶을 수도 있습니다. 그런 경우 레이블에서 점수를 관리하지 않고, 별도의 객체에 저장할 수 있습니다. 그러나 프로토타입 단계나 간단한 프로젝트에서는 코드를 간단하게 유지하는 것이 좋습니다. 프로그래밍은 항상 균형을 맞추는 작업입니다.

게임 다시 플레이

이제 죽은 후에 다시 플레이할 수 있는 기능을 추가해 보겠습니다. 플레이어가 죽으면 화면에 메시지를 표시하고 입력을 기다리도록 할 것입니다.

main.tscn 씬으로 돌아가서 UserInterface 노드를 선택한 후, 자식 노드로 :ref:`ColorRect <class_ColorRect>`를 추가하고 이름을 ``Retry``로 설정하세요. 이 노드는 화면을 어둡게 하는 오버레이로 사용될 단색 사각형을 채우는 역할을 합니다.

이 노드가 뷰포트 전체를 덮도록 하려면, 툴바의 Anchor Preset 메뉴를 사용하세요.

|image10|

메뉴를 열고 공간 전체 명령을 적용하세요.

|image11|

아직 아무 일도 일어나지 않았습니다. 정확히는 거의 아무 변화도 없고, 초록색 핀 4개만 선택 상자의 모서리로 이동했습니다.

image12

이는 UI 노드(녹색 아이콘이 표시된 모든 노드)가 부모 노드의 선택 상자를 기준으로 anchor와 margin을 사용하기 때문입니다. 여기서 UserInterface 노드는 크기가 작으며, Retry 노드는 그에 의해 제한됩니다.

UserInterface 노드를 선택하고 거기에도 *앵커 프리셋 -> 공간 전체*를 적용합니다. Retry 노드가 이제 전체 뷰포트에 걸쳐 있어야 합니다.

이제 화면을 어둡게 만들기 위해 색상을 변경해 봅시다. Retry 노드를 선택하고 인스펙터*에서 *Color*를 어두우면서도 투명한 색으로 설정합니다. 이를 위해 색상 선택기에서 *A 슬라이더를 왼쪽으로 드래그하세요. 이 슬라이더는 색상의 알파(Alpha)값, 즉 불투명도/투명도를 제어할 수 있습니다.

image13

다음으로, ``Retry``의 자식 노드로 :ref:`Label <class_Label>`을 추가하고 *Text*를 "Press Enter to retry."로 설정하세요. 이 Label을 화면 중앙에 이동하고 고정하려면 *Anchor Preset -> Center*를 적용하세요.

image14

재시작 옵션 코딩

이제 플레이어가 죽었을 때와 다시 플레이할 때 Retry 노드를 표시하거나 숨기는 코드를 작성할 차례입니다.

main.gd 스크립트를 엽니다. 먼저 게임 시작 시 오버레이를 숨기고자 합니다. 이를 위해 _ready() 함수에 다음 줄을 추가하세요.

func _ready():
    $UserInterface/Retry.hide()

이제, 플레이어가 타격받았을 때, 오버레이가 표시됩니다.

func _on_player_hit():
    #...
    $UserInterface/Retry.show()

마지막으로, Retry 노드가 보일 때 플레이어의 입력을 받아, 엔터 키를 누르면 게임을 다시 시작해야 합니다. 이를 위해, 모든 입력에서 호출되는 내장 콜백 함수 _unhandled_input()를 이용할 것입니다.

Retry가 보이는 상태에서 플레이어가 미리 정의된 ui_accept를 입력하면, 현재 씬을 다시 불러옵니다.

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()

get_tree() 함수는 전역 SceneTree 오브젝트에 접근할 권한을 부여하며 이 권한을 통해 현재 씬을 다시 불러오고 다시 시작할 수 있습니다.

음악 추가하기

배경에서 계속 재생되는 음악을 추가하기 위해, 우리는 Godot의 또 다른 기능인 오토로드를 사용할 것입니다.

오디오를 재생하려면 AudioStreamPlayer 노드를 씬에 추가하고 오디오 파일을 붙이기만 하면 됩니다. 씬을 시작하면 자동으로 재생될 수 있습니다. 하지만 다시 재생하기 위해 하는 것처럼 씬을 다시 불러오면 오디오 노드도 재설정되고 음악이 처음부터 다시 시작됩니다.

오토로드 기능을 사용하면 Godot가 게임 시작 시 현재 장면 외부에서 노드나 장면을 자동으로 불러올 수 있습니다. 또한 전역적으로 접근 가능한 객체를 생성하는 데도 사용할 수 있습니다.

메뉴로 가서 새 씬을 클릭하거나 현재 열려 있는 장면 옆에 + 아이콘을 사용하여 새 씬을 만드세요.

image15

다른 노드 버튼을 클릭하여 AudioStreamPlayer를 만들고, 이름을 MusicPlayer로 변경하세요.

image16

art/ 디렉터리에 있는 음악 House In a Forest Loop.ogg인스펙터스트림 속성에 클릭하고 드래그하여 추가하세요. 또한, 자동 재생을 켜서 게임 시작 시 음악이 자동으로 재생되도록 설정하세요.

image17

music_player.tscn과 같은 이름으로 씬을 저장합니다.

씬이나 스크립트를 오토로드하려면 메뉴에서 프로젝트 -> 프로젝트 설정…을 선택하고 전역 -> 오토로드 탭을 클릭하세요.

경로 필드에 씬의 경로를 입력해야 합니다. 폴더 아이콘을 클릭하여 파일 브라우저를 열고 music_player.tscn을 더블 클릭하세요. 그런 다음, 오른쪽의 추가 버튼을 클릭하여 노드를 등록하세요.

image18

music_player.tscn은 이제 여러분이 열거나 실행하는 모든 씬에 로드됩니다. 따라서 지금 게임을 실행하면, 모든 씬에서 음악이 자동으로 재생됩니다.

이 단원을 마무리하기 전에, 작동 원리를 간단히 살펴보겠습니다. 게임을 실행하면 독이 원격로컬 두 개의 탭으로 변경됩니다.

image19

원격 탭을 사용하면 실행 중인 게임의 노드 트리를 볼 수 있습니다. 이 탭에서 Main 노드와 씬에 포함된 모든 요소, 그리고 아래에서 인스턴스화된 몹들을 확인할 수 있습니다.

image20

맨 위에는 오토로드된 MusicPlayer와 게임의 뷰포트를 나타내는 root 노드가 있습니다.

이것으로 이번 단원을 마칩니다. 다음 단계에서는 게임에 애니메이션을 추가하여 더 멋지게 만들어 보겠습니다.

참조를 위해 전체 main.gd 스크립트는 다음과 같습니다.

extends Node

@export var mob_scene: PackedScene

func _ready():
    $UserInterface/Retry.hide()


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

    # We connect the mob to the score label to update the score upon squashing one.
    mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())

func _on_player_hit():
    $MobTimer.stop()
    $UserInterface/Retry.show()

func _unhandled_input(event):
    if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
        # This restarts the current scene.
        get_tree().reload_current_scene()