본 게시글은 'HeartBeast'의 Make an Action RPG 파트의 내용을 다룹니다.
본 강의는 연계 강의이기 때문에 꼭 이전 편들을 보면서 진행하셔야 내용을 이해하시는데 어려움이 없습니다.

한 번째 파트인 애니메이션 트리입니다.

저번 시간에는 지형에 충돌을 넣어서 절벽을 구현하였습니다. 지형 디테일은 나중에도 추가하겠지만

이번 시간에는 애니메이션 제어를 위한 상태 기계(State Machine)를 배워보도록 하겠습니다.

 

상태 기계는 흔히 알려져 있는 유한 상태 기계(FSM, Finite-state machine) 파이프 라인을 따라가는 편이며,

이는 AI를 제어하는데 효과적으로 사용됩니다.

 

그럼 한번 진행해봅시다!

 

상태 기계를 코드로 구현하기 전에, 몇 가지 애니메이션을 추가하겠습니다.

주인공 캐릭터의 애니메이션 편집하기 위해서 Player씬(Scene)으로 들어갑니다.

 

그전에 애니메이션 계속 자동 재생이 되기 때문에 애니메이션 트리를 끄도록 하겠습니다.

우선 탭에서 AnimationTree를 선택합니다.

 

Active 항목을 체크 해제합니다.

 

이렇게 되면 이제는 코드에서 제어를 해야 합니다. 스크립트(Script) 편집기로 넘어가도록 하겠습니다.

 

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
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)

func _ready(): 부분을 추가합니다. 그 밑에 다음과 같이 코드를 작성합니다.

 

다시 2D 작업공간(2D workspace)으로 돌아옵니다. 그다음에 AnimationPlayer를 선택합니다.

 

Animation 버튼을 눌러 New를 선택합니다.

 

이름을 'AttackDown'으로 정하도록 하겠습니다.

 

애니메이션 시간을 '0.4'초로 맞춥니다.

 

프레임을 '36'에 맞춘 뒤 오른쪽 키 아이콘🔑으로 키를 생성합니다.

 

커서를 한 번 옮겨주시고, 프레임 값을 +1 한 뒤 키를 '40'이 될 때까지 추가적으로 생성합니다.

 

키를 다 생성했으면, 다시 애니메이션을 생성합니다. 이름은 'AttackLeft'로 짓습니다.

 

애니메이션 시간을 동일하게 '0.4'초로 맞추고 프레임을 '32'에 맞춘 뒤 키🔑를 생성합니다.

이후 커서를 한 번 옮기고 프레임 값을 +1 한 뒤 값이 '35'가 될 때까지 생성합니다.

 

다시 애니메이션을 생성합니다. 이름은 'AttackRight'로 짓습니다.

 

방금 전처럼 애니메이션 시간을 맞춥니다. 프레임은 '24'에서 '28'이 될 때까지 키🔑를 생성하시면 됩니다.

 

마지막 애니메이션을 생성합니다. 이름은 'AttackUp'으로 합니다.

 

동일하게 애니메이션 시간을 맞춰줍니다. 프레임은 '28'에서 '32'가 될 때까지 키🔑를 생성합니다.

 

다 생성했으면, 생성한 키에 문제가 없는지 확인하시고 스크립트 편집기로 넘어갑니다.

 

우선 우리는 간단한 상태 기계를 구현할 것인데요.

enum 타입을 사용하여 상태를 세 가지로 정의할 것입니다.

 

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
32
33
34
35
36
37
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)

MOVE, ROLL, ATTACK 이렇게 세 가지를 작성하였습니다.

 

enum 타입의 경우 제일 앞에 오는 값은 0에서부터 시작합니다.

한마디로 상수로써의 의미도 가진다는 뜻인데요. MOVE0, ROLL1, ATTACK2를 가리킵니다.

뒤에 콤마(,) 찍고 더 추가한다면 3,4,5,6으로 1씩 늘어나게 됩니다.

 

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
32
33
34
35
36
37
38
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)

그리고 아래에 var state를 정의하고 MOVE로 초깃값을 정하도록 하겠습니다.

 

상태 기계를 구현하기 전에 개체가 어떠한 상태를 가지고 있는지에 대해서 미리 생각해둬야 합니다.

흔히 이것을 디자인한다고 말하는데요. 여기서는 우리가 이미 정해둔 세 가지 상태(MOVE, ROLL, ATTACK)를 가지고

예를 들어 설명하도록 하겠습니다.

 

세 가지 상태들은 서로 전환이 가능한 상태가 되어야 하고, 어떤 상태는 애니메이션이 끝날 때 다른 상태로 전환되도록

구현을 하여야 합니다. 예시로, MOVE 상태에서 공격 버튼(A)을 눌러서 공격한다고 합시다. 이때 A를 누르면

ATTACK 상태로 전환됩니다. 그리고 공격을 마치면 ATTACK 상태에서 MOVE 상태로 전환되도록 할 것입니다.

 

ROLL도 마찬가지입니다. 대신 ATTACK 상태에서 구르기 버튼을 눌러 바로 하게 할 것인지(흔히 캔슬이라고 말합니다.)

아니면 MOVE 상태에서 구르기를 했을 때만 구를 수 있도록 할 것인지는 이런 시스템을 디자인하는 기획자나

혹은 여러분의 몫입니다.

 

이러한 과정을 이제 미리 설계를 해두고 그다음에 코드로 구현하는 것이 바람직합니다.

본 설명이 조금 이해가 잘되지 않는다면, 본문 하단에 상태 기계에 대한 설명을 링크 걸어두었으니 확인하시면 됩니다.

아무튼 코드를 작성할 때 위의 내용을 참고하여 구현하겠습니다.

 

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
32
33
34
35
36
37
38
39
40
41
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    move_state()
        
func move_state():
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)

우선은 우리가 기존에 func _physics_process(delta):에 작성했던 내용들은 하나의 함수로 만들 것입니다.

함수명은 move_state()로 하겠습니다. 그리고 이전의 내용을 잘라내기든 복사하여

func move_state(): 함수 안에 붙여 넣도록 하겠습니다.

 

그러면 이런 식으로 에러가 뜰 텐데, 당연히 delta변수를 인식하지 못해서입니다. move_state()의 괄호 안에

delta를 매개변수로 주도록 하고, func _physics_process(delta):안에 있는 코드

move_state()의 괄호 안에 인자 값으로 delta를 주도록 하겠습니다.

 

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
32
33
34
35
36
37
38
39
40
41
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    move_state(delta)
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)

그래서 다음과 같이 수정하시면 됩니다.

 

당연한 거지만, 이렇게 구조를 바꿔도 게임에는 전혀 변화가 없습니다.

단지 코드만 조직화시켰다고 보시면 됩니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    match state:
        MOVE:
            move_state(delta)
        
        ROLL:
            pass
        
        ATTACK:
            attack_state()
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)
 
func attack_state():
    pass

추가로 함수를 더 작성하겠습니다.

우선 아래에 func attack_state():를 작성하고 안에다가 pass라고 적습니다.

 

그다음 func _physics_process(delta):에서

match문을 사용해 state 변수를 제어합니다.

match문은 다른 언어에서 switch문과 유사한 기능을 가지고 있습니다만, 더 편한 것은

switchcase와 항상 붙어 다니는데, 여기서는 딱히 써주시지 않아도 됩니다.

 

최종적으로 다음과 같이 match state: 안에 MOVE:, ROLL:, ATTACK:를 각각 작성하였습니다.

이렇게 하면 state의 값에 따라서 실행되는 함수가 분기로 나뉘어서 실행됩니다.

 

코드를 마저 작성하기 전에, 단축키를 추가로 지정하도록 하겠습니다.

좌측 상단 메뉴에서 Project - Project Settings…를 선택합니다.

 

위의 메뉴 중 Input Map를 선택합니다.

 

스크롤을 내려봅시다. 이동키를 방향키 말고도 W, A, S, D도 추가할 것인데요. 우선은

왼쪽 키를 넣어봅시다. ui_left 항목의 오른쪽에 있는 +버튼을 클릭한 뒤 나오는 창에서 A키를 키보드에서 누릅시다.

 

A를 누르셨으면 이렇게 입력이 들어옵니다. OK 버튼을 눌러 진행합니다. 동일한 방법으로

그 밑에 있는 ui_right, ui_up, ui_down 항목에 각각 D, W, S키를 추가합니다.

 

다음과 같이 추가하셨으면 됩니다.

 

액션을 추가하기 위해 Action:에 'attack'을 입력하고 Add버튼을 눌러 추가합니다.

 

'Space'키를 추가합니다. 또한 WASD 사용자를 위한 'J'키도 추가하겠습니다.

사실 공격 버튼은 제가 앞서 말한 두 개의 키 말고도 여러분이 임의로 추가하셔도 됩니다.

저 같은 경우는 마우스 좌클릭도 추가하였습니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    match state:
        MOVE:
            move_state(delta)
        
        ROLL:
            pass
        
        ATTACK:
            attack_state()
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)
    
    if Input.is_action_just_pressed("attack"):
        state = ATTACK
 
func attack_state():
    pass

이어서 코드를 작성합니다. 방금 추가한 공격 버튼에 대해서

is_action_just_pressed("attack"): 라는 조건문을 작성하였습니다.

그 아래에 state = ATTACK를 추가로 작성합니다.

 

게임을 실행해서 한 번 결과를 확인해봅시다.

어라? 공격 버튼을 누르면 움직이지 않네요? 뭔가 잘못된 걸 까요?

아닙니다. 당연히 state의 값이 ATTACK으로 바뀌었기 때문에 정상적으로 작동하고 있는 겁니다.

이제 attack_state를 수정해야겠지요?

 

다시 2D 작업공간으로 돌아온 다음, AnimationTree를 선택합니다.

 

아무 데나 우클릭해서 새로운 BlendSpace2D를 생성합니다.

 

노드 연결 아이콘을 클릭해 IdleAttack으로 드래그&드롭

같은 방식으로 반대쪽인 AttackIdle으로 연결합니다.

 

선택툴을 다시 선택해서 위치를 적당히 보기 좋게 잡아주시고 연필 ✏️아이콘을 눌러 편집 모드로 들어갑니다.

 

애니메이션 파트에 쓴 요령(Trick)을 쓰기 위해  y좌표의 두 값을 각각 '1.1', '-1.1'으로 변경합니다.

애니메이션 생성⬥✏️ 아이콘을 선택합니다.

각 축의 끝 부분에 다음과 같이 애니메이션을 추가합니다.

위쪽은 AttackUp애니메이션이 아닌 AttackDown입니다. 이는 인게임 좌표계와 다르다고 설명한 바가 있죠?

더보기

[Godot Engine] 08 - 애니메이션 트리 (Animation Tree)

추가하면서 왼쪽, 오른쪽은 그렇다 쳐도 위, 아래는 게임 스크린 좌표와 차이가 있다는 것을 느끼실 텐데요.

그 이유는 BlendSpace2D에서는 위가 양수이기 때문입니다. 이점만 주의하시면 됩니다.

 

Blend˅ 버튼을 클릭합니다.

Blend(혼합)을 점 선 모양으로 바꿉니다.

 

넣은 애니메이션들이 잘 적용되는지 확인하기 위해 root를 눌러 이전으로 돌아갑니다.

 

우측 인스펙터(Inspector) 뷰에서 Active를 체크합니다. 그리고

Attack 버튼을 클릭하여 애니메이션을 재생시킵니다.

마지막으로 Attack 옆에 있는 연필✏️을 눌러 편집 모드로 들어갑니다.

 

Blend Position을 바꾸는 툴이 선택되어있는지 확인하고 마우스를 꾹 누른 상태로 이리저리 옮겨봅시다.

 

다시 root 버튼을 눌러 이전 페이지로 돌아갑니다.

 

초기 위치를 Idle로 맞추기 위해 Active 항목을 체크했다 풀었다 반복하여 다음과 같은 상태로 만들어줍니다.

 

스크립트 편집기로 넘어갑니다.

우리는 아까 전에 Attack이라는 새로운 BlendSpace2D를 만들었기 때문에

이를 활용하기 위한 코드를 작성할 것입니다. 이전에 작성한 Run부분을 복사해서 붙여 넣은 뒤 빠르게 이름을 바꿔봅시다.

해당 부분에 커서를 두고 Ctrl+D를 누르면 그대로 한 줄 복사가 됩니다.

 

대소문자는 꼭 구분해주세요!

이 부분만 바꾸면 되겠죠?

 

방금 추가한 코드에 대해서 몇몇 사람들은 이러한 궁금증이 생길 수 도 있습니다.

공격 관련된 코드임에도 불구하고 attack_state가 아닌 move_state함수에 넣는 경우는 왜일까요?

여기에는 두 가지의 이유가 존재합니다.

 

1. attack_state 함수에서는 input_vector변수에 접근할 수 없습니다.

2. attack_state 함수가 실행되면 공격하는 모션이 나올 텐데,

우리는 모션이 나오는 동안 방향이 바뀌는 것을 원치 않습니다. 그것은 이상하게 보일 수 도 있기 때문입니다.

따라서 공격하는 도중 갑자기 다른 방향으로 바꾸지 못하도록 이렇게 작성합니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    match state:
        MOVE:
            move_state(delta)
        
        ROLL:
            pass
        
        ATTACK:
            attack_state()
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationTree.set("parameters/Attack/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)
    
    if Input.is_action_just_pressed("attack"):
        state = ATTACK
 
func attack_state():
    animationState.travel("Attack")

travel도 아래에 써줘야겠지요? 최종적으로 이렇게 작성하시면 됩니다.

 

인게임에서도 확인해봅시다. 공격했을 때 휘두르고 멈추면 잘 작동하고 있는 겁니다.

이게 끝이면 좋겠지만, 직접 보니까 아직 뭔가 더 수정해야겠다는 생각이 들 겁니다.

 

AnimationPlayer를 선택해서 AttackDown부터 수정하도록 하겠습니다.

다른 엔진도 그렇고 애니메이션이 끝나면 대체로 어떠한 메서드(함수)를 호출하는 기능이 존재합니다.

고도 엔진도 마찬가지로 그 기능이 있는데요. 바로 활용해보도록 하겠습니다.

 

우선 +Add Track버튼을 클릭해서 나오는 항목 중 Call Method Track을 선택합니다.

 

열리는 창에서 Player를 선택합니다. 그다음 OK 버튼을 누릅니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    match state:
        MOVE:
            move_state(delta)
        
        ROLL:
            pass
        
        ATTACK:
            attack_state()
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.= Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationTree.set("parameters/Attack/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
    else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)
    
    if Input.is_action_just_pressed("attack"):
        state = ATTACK
 
func attack_state():
    animationState.travel("Attack")
 
func attack_animation_finished():
    state = MOVE

메서드로 사용할 함수를 하나 추가하겠습니다.

아래에 func attack_animation_finished():를 작성합니다.

그리고 안에다 state = MOVE를 작성했습니다.

 

그다음 커서를 0.4초에 옮깁니다.

 

커서 근처에 우클릭을 해서 Insert Key를 선택합니다.

 

방금 작성한 코드의 함수 attack_animation_finished()를 선택하고 Open 버튼을 클릭합니다.

 

이렇게 키가 생성되었습니다. 이 키는 드래그해서 원하는 시간대로 옮길 수 있습니다. 같은 방식으로

AttackDown 말고도 AttackUp, AttackLeft, AttackRight 이 세 가지 또한 동일한 구간에 키를 생성해주세요.

애니메이션을 교체한 뒤 +Add Track으로 메서드 트랙을 생성하고, 함수를 선택해 키를 만드시면 됩니다.

 

다 하셨다면 플레이를 한번 눌러서 확인해봅시다.

 

음, 재생은 잘됩니다만, 공격할 때 공격한 방향으로 앞으로 나아가는 느낌이 드네요. 이 부분을 수정하도록 합시다.

 

이동 중에 공격을 하게 되면 attack_state()를 실행하고 애니메이션이 종료됨에 따라

attack_animation_finished()에 의해 다시 stateMOVE로 전환됩니다.

그러면 move_state()를 매 프레임에 실행하게 되는데 이때 공격하기 전에 이동했던

조작 때문에 속도(Velocity) 값이 남아 있어서 move_and_slide(velocity)에 의해 남은 값만큼 이동합니다.

 

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
extends KinematicBody2D
 
const ACCELERATION = 500
const MAX_SPEED = 80
const FRICTION = 500
 
enum {
    MOVE,
    ROLL,
    ATTACK
}
 
var state = MOVE
var velocity = Vector2.ZERO
 
onready var animationPlayer = $AnimationPlayer
onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
 
func _ready():
    animationTree.active = true
 
func _physics_process(delta):
    match state:
        MOVE:
            move_state(delta)
        
        ROLL:
            pass
        
        ATTACK:
            attack_state()
        
func move_state(delta):
    var input_vector = Vector2.ZERO
    input_vector.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_vector.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_vector = input_vector.normalized()
    
    if input_vector != Vector2.ZERO:
        animationTree.set("parameters/Idle/blend_position", input_vector)
        animationTree.set("parameters/Run/blend_position", input_vector)
        animationTree.set("parameters/Attack/blend_position", input_vector)
        animationState.travel("Run")
        velocity = velocity.move_toward(input_vector * MAX_SPEED, ACCELERATION * delta)
     else:
        animationState.travel("Idle")
        velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
        
    velocity = move_and_slide(velocity)
    
    if Input.is_action_just_pressed("attack"):
        state = ATTACK
 
func attack_state():
    velocity = Vector2.ZERO
    animationState.travel("Attack")
 
func attack_animation_finished():
    state = MOVE

따라서 공격을 실행할 때 속도 값을 Vector2.ZERO를 넣어 멈추도록 설정하면 됩니다.

코드는 다음과 같습니다.

 

잘 작동하네요!

 

상태 기계에 대한 보충 설명 그리고 이번 파트에서 어떻게 디자인되었는지에 대한 설명은

HeartBeast의 영상 [5:53~7:51]에서 설명하고 있으니 필요하시다면 보시는 것을 추천드립니다.

 

다음 파트에서는 지형에 볼륨을 주기 위해 풀을 추가하고 시그널(Signal)을 사용해 주인공이 공격 버튼을 눌렀을 때

전체적으로 풀이 베어지는 연출을 구현하도록 하겠습니다.

그럼 다음 파트에서 뵙겠습니다.

728x90
728x90