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

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

저번 시간에는 애니메이션 제어를 위한 상태 기계에 대해서 알아봤습니다.

이번 시간에는 지형의 디테일을 좀 더 추가해보겠습니다. 이 과정에서 시그널(Signal)에 대해서 알아보도록 하겠습니다.

 

World 씬(Scene)에서 가장 먼저 Bush들을 그룹화하는 작업을 거치도록 하겠습니다.

 

Ysort를 선택하고 +버튼을 클릭해 노드를 하나 생성합니다.

 

Node2D 선택 후 생성합니다.

 

이름을 'Bushes'으로 변경합니다.

 

Bush 오브젝트를 모두 생성합니다. 선택하실 때 Ctrl로 하나 씩 추가 선택하는 방법도 있고

정렬이 된 상태라면 한 번에 묶음 선택하시면 빠르게 선택하실 수 있습니다.

더보기

[Godot Engine] 09 - 배경과 타일 맵 -1부-

선택을 하셨으면 그 상태로 Shift를 누르고 최상위 노드 World 아래에 있는 Player 노드를 선택합니다.

그러면 묶음 선택이 됩니다. 이 기능은 여러 노드를 선택할 때 자주 쓰이니 꼭 익혀 둡시다.

제 화면에서는 Bush부터 Bush9까지 다 선택하면 될 것 같네요.

선택하신 후에는 드래그&드롭으로 아까 이름 바꾼 Bushes에 넣습니다.

 

하지만 이 상태로 끝난 것이 아닌데요. 실제로 게임 상에서 보면, Ysort안에 포함되어 있는 자식 노드임에 불구하고

Ysort기능이 작동하지 않는 것을 확인할 수 있습니다.

 

작동하지 않는 이유는 간단하게 Bushes의 위치를 기준으로 잡기 때문입니다.

따라서 Bushes안의 모든 자식 노드들에게 Ysort를 적용하려면, 부모인 Bushes의 유형을

Node2DYsort로 변경하여야 합니다.

 

Bushes우클릭하여 Change Type을 누릅니다.

'ysort'를 검색하고 Ysort를 선택 한 다음 Change를 눌러 변경합니다.

 

다시 확인해보면, 정상적으로 Ysort가 작동하는 것을 확인할 수 있습니다.

 

이번에 을 생성해봅시다. 이쪽에 있는 +버튼을 누릅니다.

 

2D Scene를 선택합니다.

 

최상위 부모 노드의 이름을 'Grass'로 변경합니다.

 

저장하기 위해서 Ctrl+S, World 폴더 안에 저장합니다.

 

+버튼을 눌러 자식 노드를 추가합니다. 'sprite'를 검색하고 Sprite를 선택하고 생성합니다.

 

파일 시스템(FileSystem)에서 World폴더 안에 있는 Grass.pngSprite텍스처(Texture) 항목에 넣습니다.

 

이미지를 왼쪽 위 좌측에 맞추기 위해 Centered체크 해제하고 Offsetxy의 값을 각각 '-8,-8' 주도록 하겠습니다.

 

Grass 에서 World 으로 되돌아옵니다. 

미리 그룹화하기 위해서 Ysort유형의 노드를 하나 더 만들도록 하겠습니다. 이전부터 다루지 않았지만

+버튼 말고도 노드를 추가하는 방법은 자식 노드로 추가하고 싶은 노드에다가 우클릭해서

Add Child Node or Ctrl+A를 눌러 추가할 수 돼있습니다.

 

Ysort를 추가합니다. 아까 Ysort를 추가한 기록이 있기 때문에 Recent: 항목에서 선택하셔도 됩니다.

 

Ysort 노드 이름을 'Grass'로 변경하겠습니다.

 

Grass가 선택되어 있는 상태에서 파일 시스템World 폴더 안에 있는

Grass.tscn2D 작업공간에 배치해보도록 하겠습니다. 적당히 꾸미고 싶은 곳에 배치해주세요.

 

배치하실 때 이런 식으로 일일이 파일 시스템에서 꺼내는 방법도 있지만,

 

Ctrl+D를 이용해서 기존 꺼를 복사해 재배치하는 방법도 존재합니다.

 

배치를 다하고 확인해보면, Ysort도 잘 적용되어 있는 것을 확인할 수 있습니다.

 

Grass 으로 돌아갑니다. 스크립트를 생성해주세요.

 

이 부분을 드래그해서 Ctrl+K로 주석을 풀어줍니다.

 

중간 부분은 지워서 다음과 같이 만들어주세요.

 

1
2
3
4
5
extends Node2D
 
func _process(delta):
    if Input.is_action_just_pressed("attack"):
        queue_free()

코드는 다음과 같이 작성합니다.

 

별거 없기는 하지만 queue_free에 대해서 설명을 하도록 하겠습니다.

해당 노드를 메모리에서 지우기 위해 사용되는 함수인데요.

이 함수랑 free함수가 있습니다. 결과적으로는 지워지는 건 같지만

과정에 차이가 있습니다.

 

free는 즉시 처리하긴 하지만, 삭제되었는지 보장하지는 않고

(삭제 시도는 하지만, 지웠다는 사실을 알 수 없다는 겁니다.)

queue_freefree보다는 상대적으로 안전하게 삭제하며 삭제 유무가 좀 더 확실합니다.

그리고 queue_free의 이름에서 알 수 있듯이 큐(queue)에 넣고 free()시킨다는

의미이므로 free의 확장으로 볼 수 있습니다.

 

참고로 메모리에서 지운다는 것은 게임에서는 노드 혹은 오브젝트를 제거하는 역할은 한다는 뜻입니다.

 

한 번 게임에서 확인해봅시다. 공격 버튼을 누르면, 방금 깔았던 풀이 순식간에 제거되어야 합니다.

 

풀이 너무 순식간에 사라져서 부자연스럽기 때문에 풀이 베어지는 듯한 애니메이션을 추가할 것입니다.

새로운 을 하나 더 만듭니다. 아까처럼 2D Scene을 선택해주시고요. 이름을 'GrassEffect'으로 변경합니다.

 

저장(Ctrl+S)을 한 번 해줍시다. 그럼 이전 저장했던 창이 나올 텐데요. 버튼을 눌러 상위 폴더로 이동 한 뒤

 

Effects 폴더 안에다가 저장을 해주세요.

 

노드를 추가합니다. 'anima'를 검색해서 나오는 AnimatedSprite를 선택하고 생성합니다.

 

우측 인스펙터(Inspector) 뷰에서 Frames 항목에서 [empty]˅를 클릭하면 나오는 New SpriteFrames를 선택합니다.

 

생성된 SpriteFrames을 클릭합니다.

 

default라고 적혀 있는 이 부분을 클릭해서 'Animate'로 이름을 변경합니다.

 

하단의 Speed(FPS)는 '15'로 변경해주세요.

 

시트 아이콘을 클릭합니다.

 

Effects 폴더 안에 있는 GrassEffect.png를 선택하고 열어줍니다.

 

HorizontalVertical를 조절해서 해당 이미지를 프레임으로 나눌 수 있습니다.

 

이 이미지는 수평으로 쭉 나열되어있는 이미지니까 Vertical을 '1'로 바꿉시다.

 

우선은 Horizontal는 이미지에 대해 얼마큼 공간이 배정되어 있는지 알아야 하는데
예전에 지형 시트와 마찬가지로 규격을 알면 쉽게 프레임으로 나눌 수 있습니다.
예를 들어 160x32라는 이미지가 있고 32x32 나눠서 한 장으로 만들 수 있다는 사실을
알고 있다면 그대로 하면 된다는 것이죠. 이 이미지도 크기도 마침 160x32이기 때문에
이 이미지는 Horizontal를 '5' 주게 되면 5장으로 잘라 쓸 수 있다는 사실을 알 수 있습니다.

 

시트에 관해서는 디자이너와 미리 상의를 해야 하는 부분입니다. 예를 들어 이펙트 시트를 만들건대
한 이미지에 공간을 어떤 식으로 배정하는지에 대해서 말이죠. 그렇게 해서 받은 게 아니라면
여러분들은 엔진에서 일일이 수치 조절하면서 맞추거나 포토샵 같은 걸로 잘라보거나 
뭐 다른 식으로 계산해야 할 것입니다.

말이 조금 길어졌는데 핵심은 여러분들이 이런 이미지가 나열된 이미지를 봤을 때
"아! 이건 애니메이션 시트(Animation Sheet) 구나" 하고 단 번에 알아차리고
이를 활용할 줄 알면 됩니다. 혹은 여러분이 디자이너까지 겸한다면 이건 두말할 것도 없겠죠?

 

따라서 Horizontal의 값을 '5' 주고 각 프레임을 순차적으로 클릭하고 나서

마지막에 Add 5 Frame(s) 버튼을 클릭합니다.

 

하단에 이미지 5개가 추가된 것을 확인할 수 있습니다.

다시 인스펙터 뷰로 넘어가서 Playing 항목을 체크 해서 켜봅시다.

 

프레임이 자동으로 재생되는 것을 확인할 수 있습니다.

반복 재생되는 것은 Speed(FPS) 항목 밑에 있는 Loop가 활성화되어 있어서 그렇습니다.

 

Loop옆의 버튼을 클릭하여 꺼주도록 합시다. 그리고 아까 켰던 Playing 항목, 그리고 

그 밑에 있는 Centered 항목도 체크 해제 해주세요.

 

저번에 추가한 풀의 Offset에 맞춰 동일하게 Offset항목의 xy값을 각각 '-8, -8'를 주도록 합니다.

 

뷰에서 GrassEffect를 선택합니다.

 

스크립트 생성 아이콘을 클릭합니다.

 

Create를 눌러 생성합니다.

 

스크립트(Script) 편집기에서 다음과 같이 주석 부분을 모조리 없애주세요.

 

1
2
3
4
5
6
extends Node2D
 
onready var animatedSprite = $AnimatedSprite
 
func _ready():
    pass # Replace with function body.

그리고 코드를 다음과 같이 작성합니다.

 

우리가 AnimatedSprite를 처음 다뤄보는 것이기 때문에 아는 게 없습니다. 하지만

친절하게(?)도 고도 엔진 Docs에서는 어떻게 쓸 수 있는지 확인할 수 있습니다.

 

우측 상단에 있는 Search help를 클릭하여 

 

'animated'라고 검색 한 뒤 보이는 AnimatedSprite를 선택해서 열어봅시다.

 

내용이 좀 많은데 우선적으로 봐야 할 것은, PropertiesMethods입니다. 한 번 훑어만 보시고

저기에 있는 것을 몇 가지를 직접 써보면서 알아가 봅시다.

 

스크립트를 눌러서 편집으로 돌아오시고,

 

1
2
3
4
5
6
extends Node2D
 
onready var animatedSprite = $AnimatedSprite
 
func _ready():
    animatedSprite.play("Animate")

다음과 같이 작성해봅시다.

 

이전처럼 ▶️버튼 말고 이번에는.🎬안에 재생▶️버튼이 있는 아이콘을 선택해주세요.

이 아이콘은 해당 만 재생해주는 기능입니다. 이후에는 씬 재생이라고 부르도록 하겠습니다.

 

눌러보면 딱히 재생되는 것도 아니고 그냥 이상태로 머물러있는데요.

 

이는 이미 프레임이 다 재생되어있는 상태이기 때문에 스프라이트를 재생하는 코드를 넣는다고 해서 또 재생되지는 않습니다.

따라서 프레임을 되감아준 다음 재생하면 되는데요. 아까 우리가 봤던 Docs에서 이를 수정하는 속성이 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
extends Node2D
 
onready var animatedSprite = $AnimatedSprite
 
func _ready():
    animatedSprite.play("Animate")
 
func _process(delta):
    if Input.is_action_just_pressed("attack"):
        animatedSprite.frame = 0
        animatedSprite.play("Animate")

_ready 함수에다가 두면, 키자마자 바로 재생돼서 못 볼 수도 있으니 우선 테스트를 위해

_process 함수에 작성하도록 하겠습니다. 그리고 if문 조건으로 공격 버튼을 누르면

프레임이 되감아진 후 재생되는 코드를 작성했습니다.

 

씬 재생을 눌러서 확인해봅시다. 공격 버튼을 눌러 재생되면 됩니다.

 

1
2
3
4
5
6
7
extends Node2D
 
onready var animatedSprite = $AnimatedSprite
 
func _ready():
    animatedSprite.frame = 0
    animatedSprite.play("Animate")

애니메이션이 재생되는 것 까지 확인했고, _process 함수는 지우고

작성한 코드는 수정해서 _ready 함수에 넣도록 하겠습니다.

 

씬 재생해서 확인해보고, 문제없으면 뷰에서 AnimatedSprite를 선택해줍시다.

 

우측 인스펙터 뷰 옆에 있는 Node 탭을 클릭합니다.

 

이제 시그널(Signal)에 대해서 설명하도록 하겠습니다.

시그널은 노드와 노드 간의 통신을 위해 만들어졌습니다.
시그널은 독자적인 고도 엔진의 기능인데, 사실상 유니티나 언리얼에서도 비슷한 게 존재하긴 합니다.
특히 언리얼에서는 블루프린트 간의 통신을 위해 다양한 방법이 존재합니다.

아무튼 시그널에서 대해서 더 이야기하자면 간단하게
시그널은 일종의 메시지라고 보시면 됩니다.
메시지를 보내고 그 메시지를 어떤 곳에서 받아서 처리할 수 있습니다.

다시 말해 게임에서는
특정 이벤트를 시그널로 만들어서 그 이벤트가 발생할 때
함수를 호출하거나 거기서 처리하도록 코드를 짤 수도 있습니다.

이는 코루틴처럼 yield 구문을 사용할 수 있습니다.
yield는 어려운 문법은 아니지만 이를 여기서 설명하기엔 내용이 길어지니
차후에 다룰 수 있다면 짚고 넘어가도록 하겠습니다.

 

우리가 여기서 사용할 이벤트는 animation_finished()입니다. 더블 클릭합니다.

 

Connect를 눌러 시그널을 연결합니다.

 

자동으로 스크립트 편집기창으로 이동됩니다.

이 부분이 방금 연결한 시그널 함수입니다.

 

1
2
3
4
5
6
7
8
9
10
extends Node2D
 
onready var animatedSprite = $AnimatedSprite
 
func _ready():
    animatedSprite.frame = 0
    animatedSprite.play("Animate")
 
func _on_AnimatedSprite_animation_finished():
    queue_free()

pass를 지우고 이전에 활용한 queue_free()를 작성하도록 하겠습니다.

 

우측 Node 탭에서 _on_AnimatedSprite_animation_finished()를 누르면

초점이 해당 함수로 맞춰져 코드를 작성할 때 바로 이동하여 편집할 수 있습니다.

 

씬 재생을 해서 결과를 확인해봅시다. 사라지면 잘 작동하고 있는 겁니다.

 

아무튼 애니메이션으로 사용할 이펙트는 준비되었고,

Grass 으로 가서 상호작용 될 수 있도록 코드를 작성해봅시다.

만약에 Grass 을 닫아놓으셨다면 파일 시스템에서 World - Grass.tscn를 클릭해서 여시면 됩니다.

 

스크립트 편집기로 들어갑시다.

 

1
2
3
4
5
6
7
8
9
10
extends Node2D
 
const GrassEffect = preload("res://Effects/GrassEffect.tscn")
 
func _process(delta):
    if Input.is_action_just_pressed("attack"):
        var grassEffect = GrassEffect.instance()
        var world = get_tree().current_scene
        world.add_child(grassEffect)
        queue_free()

Const 변수로 하나 선언해줍시다.

영상에서는 load를 쓰는데 나중에는 preload로 바꾸므로 우리는 미리 쓰도록 하겠습니다.

두 개의 차이에 간략하게 설명드리자면,

load는 매 시점마다 로드를 하여 사용하는 방식이고

preload는 한번 로드한 시점에서 매번 새로 로드하는 것이 아니라 한 번 로드했던 것을 재사용합니다.

따라서 아무래도 성능 차이가 있겠죠?

 

아무튼 계속 진행해서 if 조건 문안에 작성하는데

var grassEffect = GrassEffect.instance() 이 부분은

인스턴스(Instance)라고 하는데요.

 

이 이미지는 따라 하지 마시고 눈으로만 봐주세요.

말 그대로 생성입니다. 다음과 같은 행동도 인스턴스라고 볼 수 있죠.

이러한 행동을 코드로써 작동되게 됩니다.

인스턴스노드grassEffect 변수에 담깁니다.

 

1
2
3
        var world = get_tree().current_scene
        world.add_child(grassEffect)
        queue_free()

그런데 인스턴스만 하면 실제로 에 생성되지 않기 때문에, 현재 을 가져오는 코드를 추가로 작성하게 됩니다.

그게 바로 var world를 변수로 취하고 get_tree().current_scene을 값으로 준 것이죠.

이러면 현재 이름, 즉 이 노드가 있는 의 이름을 가져옵니다.

변수world의 값은 실제로 World 을 가리키고 있는 것이죠.

 

이름만 가져오면 끝이 아니죠? 자식으로 붙여줘야 합니다.

그래서 world.add_child(grassEffect) 를 작성합니다. 변수grassEffect는 아까 인스턴스 했던

노드를 가져와 변수World의 자식 노드로 붙게 됩니다. 그렇게 World 노드에 붙게 되고,

제 기존 Grass 노드들은 뒤에 queue_free에 의해 제거가 되게 됩니다.

 

이러한 일련의 과정은 Remote를 통해 확인할 수 있습니다.

그리고 독립적으로 실행되는 것을 알 수 있네요.

 

또한 실시간으로 값도 편집할 수 있고, 값이 변하는 것도 Remote로 통해 볼 수 있습니다.

 

아무튼 이렇게 코드를 작성하고 공격 버튼을 누르면 엉뚱한 곳에서 풀 이펙트가 생기는데요.

 

1
2
3
4
5
6
7
8
9
10
11
extends Node2D
 
const GrassEffect = preload("res://Effects/GrassEffect.tscn")
 
func _process(delta):
    if Input.is_action_just_pressed("attack"):
        var grassEffect = GrassEffect.instance()
        var world = get_tree().current_scene
        world.add_child(grassEffect)
        grassEffect.global_position = global_position
        queue_free()

코드를 한 줄 더 작성해주도록 합니다.

변수grassEffect의 위치, global.positionGrass 노드global.position으로 변경합니다.

global.position는 해당 공간 상의 원점을 기준으로 하는데요. 따라서 Grass에 배치된 위치 값을

변수grassEffect의 위치에 대입해서 공격 버튼을 눌렀을 때 풀이 베어지는 효과가 그 위치에서 나타나게 됩니다.

 

다시 게임을 실행하고, 공격 버튼을 누르면 풀 위치에 이펙트가 생성되는 것을 볼 수 있습니다.

 

이번 파트는 여기 까지지만, 아직 어색한 부분이 남아있죠?

다음 편인 히트 박스(Hitbox) 파트에서 이를 해결하게 될 것입니다.

그럼 다음 편에 뵙겠습니다.

 

+) 05/13 1부에서 통합으로 업데이트 함.

+) 05/16 일부 오탈자 수정함

728x90
728x90