[Godot Engine] 12 - 시그널(Signal) Updated 05.13
본 게시글은 '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
의 유형을
Node2D
→ Ysort
로 변경하여야 합니다.
Bushes
에 우클릭하여 Change Type
을 누릅니다.
'ysort'를 검색하고 Ysort
를 선택 한 다음 Change
를 눌러 변경합니다.
다시 확인해보면, 정상적으로 Ysort가 작동하는 것을 확인할 수 있습니다.
이번에 씬을 생성해봅시다. 이쪽에 있는 +
버튼을 누릅니다.
2D Scene
를 선택합니다.
최상위 부모 노드의 이름을 'Grass'로 변경합니다.
저장하기 위해서 Ctrl+S, World
폴더 안에 저장합니다.
+
버튼을 눌러 자식 노드를 추가합니다. 'sprite'를 검색하고 Sprite
를 선택하고 생성합니다.
파일 시스템(FileSystem)에서 World
폴더 안에 있는 Grass.png를 Sprite
의 텍스처(Texture) 항목에 넣습니다.
이미지를 왼쪽 위 좌측에 맞추기 위해 Centered를 체크 해제✘
하고 Offset의 x와 y의 값을 각각 '-8,-8' 주도록 하겠습니다.
미리 그룹화하기 위해서 Ysort유형의 노드를 하나 더 만들도록 하겠습니다. 이전부터 다루지 않았지만
+
버튼 말고도 노드를 추가하는 방법은 자식 노드로 추가하고 싶은 노드에다가 우클릭해서
Add Child Node
or Ctrl+A를 눌러 추가할 수 돼있습니다.
Ysort
를 추가합니다. 아까 Ysort
를 추가한 기록이 있기 때문에 Recent: 항목에서 선택하셔도 됩니다.
Ysort 노드 이름을 'Grass'로 변경하겠습니다.
Grass
가 선택되어 있는 상태에서 파일 시스템에 World
폴더 안에 있는
Grass.tscn를 2D 작업공간에 배치해보도록 하겠습니다. 적당히 꾸미고 싶은 곳에 배치해주세요.
배치하실 때 이런 식으로 일일이 파일 시스템에서 꺼내는 방법도 있지만,
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_free
는 free
보다는 상대적으로 안전하게 삭제하며 삭제 유무가 좀 더 확실합니다.
그리고 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를 선택하고 열어줍니다.
Horizontal과 Vertical를 조절해서 해당 이미지를 프레임으로 나눌 수 있습니다.
이 이미지는 수평으로 쭉 나열되어있는 이미지니까 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항목의 x와 y값을 각각 '-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
를 선택해서 열어봅시다.
내용이 좀 많은데 우선적으로 봐야 할 것은, Properties랑 Methods입니다. 한 번 훑어만 보시고
저기에 있는 것을 몇 가지를 직접 써보면서 알아가 봅시다.
스크립트를 눌러서 편집으로 돌아오시고,
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.position
을 Grass
노드의 global.position
으로 변경합니다.
global.position
는 해당 씬 공간 상의 원점을 기준으로 하는데요. 따라서 Grass
가 씬에 배치된 위치 값을
변수grassEffect
의 위치에 대입해서 공격 버튼을 눌렀을 때 풀이 베어지는 효과가 그 위치에서 나타나게 됩니다.
다시 게임을 실행하고, 공격 버튼을 누르면 풀 위치에 이펙트가 생성되는 것을 볼 수 있습니다.
이번 파트는 여기 까지지만, 아직 어색한 부분이 남아있죠?
다음 편인 히트 박스(Hitbox) 파트에서 이를 해결하게 될 것입니다.
그럼 다음 편에 뵙겠습니다.
+) 05/13 1부에서 통합으로 업데이트 함.
+) 05/16 일부 오탈자 수정함
'개발일지 > 고도' 카테고리의 다른 글
[Godot Engine] 고도 엔진 에디터 "NO DC" 해결법 (1) | 2023.10.13 |
---|---|
[Godot Engine] 13 - 히트 박스(Hitbox) & 콜리전 레이어 Updated 23.05.22 (7) | 2021.05.23 |
[Godot Engine] 11 - 애니메이션 제어를 위한 상태 기계 (2) | 2021.05.06 |
[Godot Engine] 10 - 오토 타일과 충돌 설정 (2) | 2021.04.30 |
[Godot Engine] 09 - 배경과 타일 맵 (4) | 2021.04.14 |