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.

캐릭터 애니메이션

이번 마지막 단원에서는 Godot의 내장 애니메이션 도구를 사용해 캐릭터가 떠다니고 날갯짓하는 애니메이션을 만들어 보겠습니다. 여러분은 편집기에서 애니메이션을 설계하고 코드를 활용해 게임에 생동감을 불어넣는 방법을 배우게 될 것입니다.

image0

먼저 애니메이션 편집기 사용에 대한 소개부터 시작해 보겠습니다.

애니메이션 편집기 사용하기

엔진에는 편집기에서 애니메이션을 작성하는 도구가 제공됩니다. 그런 다음 코드를 사용하여 런타임에서 애니메이션을 재생하고 제어할 수 있습니다.

플레이어 씬을 열고, Player 노드를 선택한 다음, AnimationPlayer 노드를 추가하세요.

애니메이션 독은 화면 하단 패널에 표시됩니다.

image1

AnimationPlayer에는 상단에 툴바와 애니메이션 드롭다운 메뉴, 중앙에 현재 비어 있는 트랙 편집기, 그리고 하단에 필터, 스냅, 줌 옵션이 포함되어 있습니다.

애니메이션을 만들어 봅시다. 애니메이션 -> 새로 만들기를 클릭하세요.

image2

애니메이션의 이름을 "float"로 짓습니다.

image3

애니메이션을 만들면, 시간을 초 단위로 표시해 주는 타임라인이 나타납니다.

image4

애니메이션이 게임 시작 시 자동으로 재생되도록 설정하고, 반복되게 만들어야 합니다.

그렇게 하려면, 여러분은 애니메이션 툴바에 있는 자동 재생 버튼(자동 재생)과 루프 화살표를 각각 클릭할 수 있습니다.

image5

여러분은 오른쪽 상단에 있는 핀 아이콘을 클릭하여 애니메이션 편집기를 고정할 수도 있습니다. 이렇게 하면 뷰포트를 클릭하고 노드를 선택 해제할 때 편집기가 접히는 것을 방지합니다.

|image1|

독의 오른쪽 상단에서 애니메이션 지속 시간을 1.2초로 설정하세요.

회색 리본이 약간 넓어지는 것을 볼 수 있습니다. 이것은 애니메이션의 시작과 끝을 보여주며, 수직 파란색 선은 타임 커서입니다.

|image7|

타임라인을 확대/축소하려면 오른쪽 하단에 있는 슬라이더를 클릭하고 드래그할 수 있습니다.

|image8|

플로트 애니메이션

애니메이션 플레이어 노드를 사용하면 필요한 만큼 많은 노드의 대부분 속성을 애니메이션화할 수 있습니다. 인스펙터에서 속성 옆에 키 아이콘을 확인하세요. 이들 중 아무거나 클릭하여 키프레임을 생성할 수 있습니다. 키프레임은 해당 속성의 시간과 값 쌍입니다. 키프레임은 타임라인에서 시간 커서가 있는 위치에 삽입됩니다.

첫 번째 키를 삽입해 봅시다. 여기서는 Character 노드의 위치와 회전을 모두 애니메이션할 것입니다.

Character를 선택하고, 인스펙터에서 변형 섹션을 확장하세요. 위치회전 옆에 키 아이콘을 클릭하세요.

|image9|

../../_images/curves.webp

이 튜토리얼에서는 기본 선택 사항인 리셋 트랙을 생성하기만 하면 됩니다

각 키프레임을 나타내는 다이아몬드 아이콘과 함께 두 개의 트랙이 편집기에 나타납니다.

|image10|

다이아몬드를 클릭하고 드래그하여 시간상으로 이동할 수 있습니다. 위치 키를 0.3초로, 회전 키를 0.1초로 이동하세요.

|image11|

회색 타임라인을 클릭하고 드래그하여 시간 커서를 0.5초로 이동합니다.

timeline_05_click

인스펙터에서 위치Y 축을 0.65미터로, 회전X 축을 8로 설정합니다.

인스펙터 패널에 속성이 보이지 않으면 먼저 독에서 Character 노드를 다시 클릭하세요.

image12

양 속성에 대한 키프레임 만들기

second_keys_both

이제 타임라인에서 드래그하여 위치 키프레임을 ``0.7``초로 이동합니다.

image13

참고

애니메이션 원리에 대한 강의는 이 튜토리얼의 범위를 벗어납니다. 모든 것을 균등하게 시간과 간격을 두고 싶지는 않다는 점에 유의하세요. 대신 애니메이터는 두 가지 핵심 애니메이션 원칙인 타이밍과 간격을 활용합니다. 캐릭터의 움직임에 상쇄와 대비를 적용하여 캐릭터가 살아있는 듯한 느낌을 주고 싶습니다.

시간 커서를 애니메이션 끝(``1.2``초)으로 이동합니다. Y 위치를 약 ``0.35``로 설정하고 X 회전을 ``-9``도로 설정합니다. 다시 한 번 두 속성 모두에 대한 키를 만듭니다.

animation_final_keyframes

재생 버튼을 클릭하거나 Shift + D 를 눌러 결과를 미리 볼 수 있습니다. 중지 버튼을 클릭하거나 S 를 눌러 재생을 중지합니다.

image14

엔진이 키프레임 사이를 보간하여 연속 애니메이션을 생성하는 것을 볼 수 있습니다. 하지만 현재로서는 모션이 매우 로봇처럼 느껴집니다. 이는 기본 보간이 선형이어서 실제 세계에서 생명체가 움직이는 방식과 달리 지속적인 전환을 일으키기 때문입니다.

이징 곡선을 사용하여 키프레임 간의 전환을 제어할 수 있습니다.

타임라인에서 처음 두 키를 클릭하고 드래그하여 상자로 선택하세요.

image15

인스펙터*에서 두 키의 속성을 동시에 편집할 수 있으며, 여기에서 *이징 속성을 볼 수 있습니다.

image16

곡선을 클릭하고 왼쪽으로 드래그하세요. 이렇게 하면 이즈 아웃되어, 시간 커서가 다음 키프레임에 도달할 때 초기에는 빠르게 전환되다가 느려집니다.

image17

애니메이션을 다시 재생하여 차이점을 확인하세요. 전반부가 이미 약간 더 탄력적으로 느껴질 것입니다.

회전 트랙의 두 번째 키프레임에 이즈 아웃을 적용하세요.

image18

두 번째 위치 키프레임에 대해서는 반대로, 오른쪽으로 드래그하세요.

image19

최종 씬은 다음과 같이 보일 것입니다.

image20

참고

애니메이션은 애니메이션화된 노드의 속성을 매 프레임마다 업데이트하여 초기 값을 오버라이드합니다. Player 노드를 직접 애니메이션화하면 코드에서 노드를 이동할 수 없습니다. 이럴 때 Pivot 노드가 유용합니다. Character를 애니메이션화했더라도 스크립트에서 Pivot을 이동 및 회전하고 애니메이션 위에 변경사항을 레이어링할 수 있습니다.

게임을 플레이하면 플레이어의 캐릭터가 떠다니게 됩니다!

캐릭터가 바닥에 너무 가까이 있다면, 피벗 을 위로 이동하여 상쇄할 수 있습니다.

애니메이션 제어하기

코드를 사용하여 플레이어의 입력에 따라 애니메이션 재생을 제어할 수 있습니다. 캐릭터가 움직일 때 애니메이션 속도를 변경해 보겠습니다.

Player 옆에 스크립트 아이콘을 클릭하여 Player 의 스크립트를 여세요.

image21

_physics_process() 에서 direction 벡터를 확인하는 줄 뒤에 다음 코드를 추가하세요.

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

이 코드는 플레이어가 움직일 때 재생 속도를 4 배로 곱하도록 합니다. 멈추면 재생 속도를 노멀로 재설정합니다.

피벗 이 애니메이션 위에 변환을 레이어링할 수 있다고 언급했습니다. 다음 코드 줄을 사용하여 점프할 때 캐릭터가 호를 그리도록 만들 수 있습니다. _physics_process() 의 끝에 추가하세요.

func _physics_process(delta):
    #...
    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

몹 애니메이션화하기

Godot에서 애니메이션을 사용하는 또 다른 유용한 팁이 있습니다. 유사한 노드 구조를 사용하는 한, 다른 씬으로 복사할 수 있습니다.

예를 들어, Mob 씬과 Player 씬 모두 피벗Character 노드를 가지고 있으므로, 이들 간에 애니메이션을 재사용할 수 있습니다.

Player 씬을 열고, AnimationPlayer 노드를 선택한 다음, 애니메이션 > 애니메이션 관리...를 클릭하세요. float 애니메이션 옆에 애니메이션을 클립보드로 복사 버튼(작은 사각형 두 개)을 클릭합니다. 확인을 클릭하여 창을 닫습니다.

다음으로 mob.tscn을 열고, AnimationPlayer 자식 노드를 생성하고 선택합니다. 애니메이션 > 애니메이션 관리를 클릭한 다음 라이브러리 추가를 클릭합니다. "전역 라이브러리가 생성됩니다."라는 메시지가 표시됩니다. 텍스트 필드를 비워 두고 확인을 클릭합니다. 붙여넣기 아이콘(클립보드)을 클릭하면 창에 나타납니다. 확인을 클릭하여 창을 닫습니다.

다음으로, 하단 패널의 애니메이션 편집기에서 자동 재생 버튼(자동 재생)과 루핑 화살표(애니메이션 루핑)도 켜져 있는지 확인하세요. 그걸로 끝입니다. 이제 모든 몬스터가 float 애니메이션을 재생합니다.

몬스터의 random_speed 에 따라 재생 속도를 변경할 수 있습니다. Mob 의 스크립트를 열고 initialize() 함수의 끝에 다음 줄을 추가하세요.

func initialize(start_position, player_position):
    #...
    $AnimationPlayer.speed_scale = random_speed / min_speed

이것으로 첫 번째 완전한 3D 게임 코딩을 마쳤습니다.

축하합니다!

다음 파트에서는 여러분이 배운 내용을 빠르게 요약하고 더 많은 것을 배울 수 있도록 몇 가지 링크를 제공할 것입니다. 하지만 지금은 여러분의 코드를 확인할 수 있도록 완전한 player.gdmob.gd 를 제공합니다.

다음은 Player 스크립트입니다.

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal movement being very fast
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        # Setting the basis property will affect the rotation of the node.
        $Pivot.basis = Basis.looking_at(direction)
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

다음은 Mob 의 스크립트입니다.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.speed_scale = random_speed / min_speed

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node