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.

플레이어 코딩

이 단원에서는 플레이어 움직임과 애니메이션을 추가하고 콜리전을 감지하도록 설정해 보겠습니다.

이제 내장 노드 만으로는 얻을 수 없는 몇 가지 기능을 추가해야 하므로 스크립트를 만들겁니다. Player 노드를 클릭하고 "스크립트 붙이기" 버튼을 누르세요:

../../_images/add_script_button.webp

스크립트 설정 창에서 디폴트 설정으로 둘 수 있습니다. 그냥 "만들기"를 클릭하세요:

참고

C#이나 다른 언어로 스크립트를 만들려면 만들기를 누르기 전에 언어 드롭 다운 메뉴에서 언어를 선택하세요.

../../_images/attach_node_window.webp

참고

GDScript를 처음 접하는 경우 계속하기 전에 씬 스크립팅하기을 읽어주세요.

이 오브젝트가 필요로 하는 멤버 변수를 선언함으로써 시작합시다:

extends Area2D

@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

Using the export keyword on the first variable speed allows us to set its value in the Inspector. This can be handy for values that you want to be able to adjust just like a node's built-in properties. Click on the Player node and you'll see the property now appears in the Inspector in a new section with the name of the script. Remember, if you change the value here, it will override the default value specified in the script (the script won't be modified).

경고

여러분이 C#을 사용한다면, 새로운 외부변수(export variables)나 시그널을 보기 위해서 프로젝트 구성물(assemblies)을 다시 빌드할 필요가 있습니다. 이 컴파일은 편집기 밑의 "Mono" 단어를 클릭해 Mono 패널이 나타나게 한 후 "프로젝트 빌드(Build Project)" 버튼을 눌러서 수동으로 진행됩니다.

../../_images/build_dotnet1.webp
../../_images/export_variable.webp

player.gd 스크립트에는 _ready()``와 ``_process() 함수가 이미 포함되어 있어야 합니다. 위에 표시된 기본 템플릿을 선택하지 않았다면, 이 강의를 따르면서 이 함수를 생성하세요.

_ready() 함수는 노드가 씬 트리에 들어올 때 호출되는데, 이 때가 게임 창의 크기를 알아보기 좋은 순간입니다:

func _ready():
    screen_size = get_viewport_rect().size

이제 _process() 함수를 사용해서 플레이어가 무엇을 할 지 정의할 수 있습니다. _process()는 매 프레임마다 호출되므로, 게임에서 자주 변하는 요소들을 업데이트하기 위해서 사용할 수 있습니다. 플레이어에게는, 다음과 같은 작업이 필요합니다:

  • 입력을 확인합니다.

  • 주어진 방향으로 이동합니다.

  • 적절한 애니메이션을 재생합니다.

먼저, 입력을 확인해야 합니다 - 플레이어가 키를 누르는 중일까요? 이 게임에서, 우리는 방향키 입력을 확인해야 합니다. 입력 액션은 프로젝트 설정의 "입력 맵"에 정의되어 있습니다. 여기에서 사용자 지정 이벤트를 정의하고 여기에 다른 키, 마우스 이벤트, 혹은 다른 입력을 할당할 수도 있습니다. 이 데모에서, 우리는 키보드의 방향키가 할당된 기본 이벤트를 사용할 것입니다.

프로젝트 -> 프로젝트 설정을 클릭하여 프로젝트 설정 창을 열고, 상단의 입력 맵 탭을 클릭합니다. 상단 표시줄에 "move_right"를 입력하고 "추가" 버튼을 클릭하여 move_right 액션을 추가합니다.

../../_images/input-mapping-add-action.webp

이 작업에 키를 할당해야 합니다. 오른쪽의 "+" 아이콘을 클릭하여 이벤트 관리자 창을 엽니다.

../../_images/input-mapping-add-key.webp

"입력을 받는 중..." 필드가 자동으로 선택됩니다. 키보드에서 "오른쪽" 키를 누르면 메뉴가 이제 다음과 같아야 합니다.

../../_images/input-mapping-event-configuration.webp

"확인" 버튼을 선택합니다. 이제 '오른쪽' 키가 'move_right' 동작에 연결됩니다.

이 단계를 반복하여 매핑을 3개 더 추가합니다:

  1. 왼쪽 화살표 키에 매핑된 move_left입니다.

  2. 위쪽 화살표 키에 매핑된 move_up입니다.

  3. 그리고 아래쪽 화살표 키에 매핑된 move_down입니다.

씬 트리는 다음과 같아야 합니다:

../../_images/input-mapping-completed.webp

프로젝트 설정을 닫으려면 "닫기" 버튼을 클릭합니다.

참고

여기에선 각 입력 동작에 하나의 키만 매핑했지만 여러 개의 키, 조이스틱 버튼 또는 마우스 버튼을 동일한 입력 동작에 매핑할 수 있습니다.

Input.is_action_pressed()를 사용해서 키가 눌러졌는지를 감지할 수 있는데, 눌러지면 true 값을 반환하고 그렇지 않으면 false 값을 반환합니다.

func _process(delta):
    var velocity = Vector2.ZERO # The player's movement vector.
    if Input.is_action_pressed("move_right"):
        velocity.x += 1
    if Input.is_action_pressed("move_left"):
        velocity.x -= 1
    if Input.is_action_pressed("move_down"):
        velocity.y += 1
    if Input.is_action_pressed("move_up"):
        velocity.y -= 1

    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite2D.play()
    else:
        $AnimatedSprite2D.stop()

먼저 velocity(0, 0)으로 설정합니다. 기본적으로 플레이어는 움직이지 않아야 합니다. 그런 다음 각 입력을 확인하고 velocity에서 더하거나 빼서 최종 방향을 얻습니다. 예를 들어, 오른쪽 방향키아래쪽 방향키을 동시에 누르고 있으면 결과 velocity 벡터는 (1, 1)이 됩니다. 이 경우 동시에 수평과 수직 방향으로 이동하기 때문에 플레이어는 수평으로 이동할 때보다 대각선으로 더 빠르게 이동합니다.

우리는 velocity를 정규화(normalize)하면, 즉 velocity의 길이(length)1로 설정한 다음, 원하는 속도를 곱하면 이 문제를 방지할 수 있습니다. 이렇게 하면 더 이상 대각선 방향 이동속도가 더 빠르지 않습니다.

이제까지 벡터 수학을 안 써봤거나 복습이 필요하다면 벡터 수학에서 Godot에서 벡터를 어떻게 사용하는지 볼 수 있습니다. 보면 좋지만 나머지 튜토리얼을 위해 꼭 필수적인 것은 아닙니다.

이제 AnimatedSprite에서 play()stop()을 호출할 수 있도록 플레이어가 움직이고 있는지를 확인할 것입니다.

$get_node()의 줄임말입니다. 그래서 위의 코드인 $AnimatedSprite.play()get_node("AnimatedSprite").play()와 같습니다.

GDScript에서, $는 현재 노드에서 상대적인 경로에 있는 노드를 반환하거나, 노드가 없다면 null 값을 반환합니다. AnimatedSprite가 현재 노드의 자식인 상태이므로, $AnimatedSprite를 사용할 수 있습니다.

이동 방향이 있으므로 이제 플레이어의 위치를 업데이트할 수 있습니다. 또한 clamp()를 사용해 플레이어가 화면에서 나가는 것을 방지할 수 있습니다. 클램핑(Clamping)은 값을 주어진 범위로 제한하는 것을 의미합니다. _process 함수의 맨 아래에 다음을 추가합니다(else 아래에 들여쓰기가 되어 있지 않은지 확인하세요):

position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)

_process() 함수에 있는 매개변수 delta 는 (이전 프레임이 완료되는데 걸린 시간인) "프레임 길이(frame length)" 를 참조합니다. 이 값을 사용하면 프레임 레이트가 변경되어도 플레이어의 이동속도를 항상 일정하게 유지할 수 있습니다.

"현재 씬 실행"(F6, macOS에서는 Cmd + R)을 클릭하고 플레이어가 화면의 모든 방향으로 움직일 수 있는지 확인하세요.

경고

"디버거" 패널에 다음과 같은 오류가 표시되는 경우

Attempt to call function 'play' in base 'null instance' on a null instance

이는 AnimatedSprite 노드의 이름을 잘못 입력했음을 의미합니다. 노드 이름은 대소문자를 구분하며 $NodeName또는 get_node("NodeName")이 씬 트리에 표시되는 이름과 일치해야 합니다.

애니메이션 선택하기

이제 플레이어가 이동할 수 있으므로 방향에 따라 AnimatedSprite가 재생 중인 애니메이션을 변경해야 합니다. 플레이어가 오른쪽으로 걷는 것을 보여주는 "walk" 애니메이션이 있습니다. 이 애니메이션은 왼쪽 이동을 위해 flip_h 속성을 사용해 수평으로 뒤집혀져야 합니다. 아래로 이동하려면 flip_v로 수직으로 뒤집혀져야 하는 "up" 애니메이션도 있습니다. 이 코드를 _process() 함수의 끝에 배치해 보겠습니다:

if velocity.x != 0:
    $AnimatedSprite2D.animation = "walk"
    $AnimatedSprite2D.flip_v = false
    # See the note below about the following boolean assignment.
    $AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite2D.animation = "up"
    $AnimatedSprite2D.flip_v = velocity.y > 0

참고

위 코드의 불리언 값 할당은 프로그래머를 위한 일반적인 축약입니다. 우리는 비교 테스트(불리언)를 수행하고 또한 불리언 값을 할당하기 때문에 두 가지를 동시에 수행할 수 있습니다. 아래 코드와 위의 한 줄 불리언 할당 명령문을 비교해보세요:

if velocity.x < 0:
    $AnimatedSprite2D.flip_h = true
else:
    $AnimatedSprite2D.flip_h = false

씬을 다시 재생하고 애니메이션이 각 방향에서 올바른지 확인합니다.

흔한 실수로 애니메이션 이름을 잘못 입력합니다. SpriteFrames 패널의 애니메이션 이름은 코드에 입력한 것과 일치해야 합니다. 애니메이션 이름을 "Walk"라고 이름을 지은 경우 코드에 대문자 "W"를 사용해야 합니다.

플레이어가 제대로 움직이고 있다고 생각하면 다음 줄을 _ready()에 추가해보세요. 게임이 시작될 때 플레이어가 숨겨집니다:

hide()

콜리전 준비하기

우리는 Player가 적과 닿았다는 것을 감지하길 원하지만 아직 적을 만들지 않았습니다! 충돌이 작동하도록 우리는 Godot의 시그널(signal) 기능을 사용할 것이기 때문에 괜찮습니다.

스크립트 상단에 다음을 추가합니다. GDScript를 사용하는 경우 extends Area2D 뒤에 추가합니다. C#을 사용하는 경우 public partial class Player : Area2D 뒤에 추가합니다:

signal hit

이것은 적과 충돌할 때 플레이어가 방출하는 "hit"라는 사용자 지정 시그널을 정의합니다. 콜리전을 감지하기 위해 Area2D를 사용합니다. Player 노드를 선택하고 인스펙터 탭 옆에 "노드" 탭을 클릭해 플레이어가 방출할 수 있는 시그널 목록을 확인해보세요:

../../_images/player_signals.webp

우리의 커스텀 "hit" 시그널도 거기에 있다는 점에 주목하세요! 적들은 RigidBody2D 노드가 될 것이기 때문에, 우리는 body_entered(body: Node) 시그널이 필요합니다. 이 시그널을 적과 플레이어와 닿았을 때 방출할 것입니다. "연결"을 누르면 "시그널을 메서드에 연결" 창이 열립니다. 우리는 어떤 설정도 바꿀 필요가 없으므로 "연결"을 누르세요.

Godot가 스크립트에서 바로 그 이름을 가진 함수를 생성합니다. 지금 당장 기본 설정을 변경할 필요는 없습니다.

경고

외부 텍스트 편집기(예: Visual Studio Code)를 사용하는 경우, 현재 버그가 발생하여 Godot가 이를 지원하지 않습니다. 외부 편집기로 전송되기는 하지만 새로운 기능은 제공되지 않습니다.

이 경우 플레이어의 스크립트 파일에 함수를 직접 작성해야 합니다.

../../_images/player_signal_connection.webp

함수에 신호가 연결되었음을 나타내는 녹색 아이콘에 주목하세요. 이는 함수가 존재한다는 의미가 아니라, 신호가 해당 이름의 함수에 연결을 시도할 것이라는 의미입니다. 따라서 함수의 철자가 정확히 일치하는지 두 번 확인하세요!

다음과 같은 주의 사항이 있습니다:

func _on_body_entered(_body):
    hide() # Player disappears after being hit.
    hit.emit()
    # Must be deferred as we can't change physics properties on a physics callback.
    $CollisionShape2D.set_deferred("disabled", true)

적이 플레이어를 칠 때마다 시그널이 방출됩니다. 우리는 두 번 이상 hit 시그널이 발동되지 않도록 플레이어의 콜리전을 비활성화 해야 합니다.

참고

엔진의 콜리전 처리 도중에 영역의 콜리전 모양을 비활성화하면 오류가 발생할 수 있습니다. set_deferred()를 사용하면 Godot가 모양을 비활성화 하기에 안전해질 때까지 기다려줍니다.

마지막으로 새로운 게임을 시작할 때 플레이어를 초기화하기 위해 호출할 수 있는 함수를 추가합니다.

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false

플레이어가 작동하는 것을 확인했으므로, 다음 단원에서는 적을 작업할 것입니다.