시그널

소개

시그널은 옵저버 패턴의 Godot 버전입니다. 시그널로 노드는 다른 노드로 메시지를 보내고 다른 노드는 이것을 받고 응답할 수 있습니다. 예를 들어, 버튼의 눌림 여부를 파악하기 위해 주기적으로 버튼을 확인하는 대신 버튼이 눌렸을 때 시그널을 방출(emit)할 수 있습니다.

주석

옵저버 패턴에 대해 더 알아보려면 다음 주소를 참고하세요: http://gameprogrammingpatterns.com/observer.html

시그널은 게임 객체들을 분리하는 도구로, 이를 활용하여 더 체계적이고 관리하기 쉬운 코드를 작성할 수 있습니다. 게임 객체가 다른 객체가 존재하도록 강요하는 대신, 한 객체가 방출한 시그널을 필요로 하는 다른 객체만 구독하거나 응답할 수 있도록 합니다.

아래에서 여러분의 프로젝트에 시그널을 활용할 수 있는 방법을 담은 예시들을 살펴보겠습니다.

타이머(Timer) 예제

시그널이 어떻게 작동하는지 알아보기 위해, Timer 노드를 사용하겠습니다. Node와 두 자식 노드가 있는 씬을 만듭니다: 하나는 Timer이고 다른 하나는 Sprite입니다. 스프라이트의 텍스쳐로 Godot 아이콘을 쓸 수도 있고, 원하는 어떤 이미지를 놓아도 좋습니다. 루트 노드에 스크립트를 붙이되, 아직 아무 코드도 넣지 마세요.

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

../../_images/signals_node_setup.png

Timer 노드의 속성에서, Autostart 옆에 박스를 "On" 하세요. 그렇게 하면 씬을 실행할 때 자동으로 타이머가 실행합니다. 또한 Wait Time을 1초로 처리할 수 있습니다.

"인스펙터" 탭 옆에 "노드"라는 라벨이 붙은 탭이 있습니다. 이 탭을 클릭하면 선택한 노드에서 방출할 수 있는 모든 시그널이 표시됩니다. Timer 노드의 경우, 주목해야 할 시그널은 "timeout"입니다. 이 시그널은 Timer가 0이 될 때마다 방출합니다.

../../_images/signals_node_tab_timer.png

"timeout()" 시그널을 클릭하고 그리고 "연결하기..."를 누릅니다. 다음과 같은 창이 나타나는데, 여기서 당신은 시그널을 어떻게 연결할 지를 정의할 수 있습니다:

../../_images/signals_connect_dialog_timer.png

왼쪽에는 씬의 노드가 표시되고 시그널을 "받는" 노드를 선택할 수 있습니다. Timer 노드는 적색입니다 - 이는 오류가 아니며 신호를 방출하는 노드임을 시각적으로 나타냅니다. 루트 노드를 선택합니다.

경고

대상 노드는 반드시 스크립트가 붙게 되고 그렇지 않으면 오류 메시지를 받습니다.

창의 아래쪽을 보시면 "Method In Node"라고 적혀있는 영역이 있습니다. 대상 노드의 스크립트에서 이것을 함수 이름으로 사용합니다. 기본적으로, Godot는 명명 규칙에 따라 _on_<노드이름>_<시그널이름>으로 함수 이름을 만들지만 원한다면 바꿀 수 있습니다.

"연결"을 클릭하면 스크립트 안에 함수가 만들어진 것을 볼 수 있습니다:

extends Node

func _on_Timer_timeout():
    pass # replace with function body
public class TimerExample : Node
{
    private void _on_Timer_timeout()
    {
        // Replace with function body.
    }
}

이제 자리 표시자 코드는 시그널을 받게 되면 실행시키길 원하는 어떤 코드든지 변환될 수 있습니다. 스프라이트 깜박임을 만들어 봅시다:

extends Node

func _on_Timer_timeout():
    $Sprite.visible = !$Sprite.visible
public class TimerExample : Node
{
    public void _on_Timer_timeout()
    {
        var sprite = GetNode<Sprite>("Sprite");
        sprite.Visible = !sprite.Visible;
    }
}

씬을 실행하면 Sprite가 매 초마다 깜박이는 것을 볼 수 있습니다. Timer의 Wait Time 속성을 변경하여 주기를 변경할 수 있습니다.

노드에서 시그널 연결하기

에디터에서 뿐만 아니라 코드에서도 시그널 연결을 만들 수 있습니다. 보통은 코드를 통해 노드를 인스턴스할 때 필요하기 때문에 에디터에서 이런 시그널 연결을 만들 수는 없습니다.

먼저, Timer의 "노드" 탭에서 연결 끊기를 클릭해서 시그널의 연결을 풉니다.

../../_images/signals_disconnect_timer.png

코드에서 연결을 만들기 위해, connect 함수를 사용할 수 있습니다. _ready()안에 이 함수를 넣으면 연결을 실행하는 준비가 된 것입니다. 함수의 문법은 <소스_이름>.connect(<시그널_이름>, <대상_이름>, <대상_함수_이름>)입니다. 이것이 Timer 연결을 위한 코드입니다:

extends Node

func _ready():
    $Timer.connect("timeout", self, "_on_Timer_timeout")

func _on_Timer_timeout():
    $Sprite.visible = !$Sprite.visible
public class TimerExample : Node
{
    public override void _Ready()
    {
        GetNode("Timer").Connect("timeout", this, nameof(_on_Timer_timeout));
    }

    public void _on_Timer_timeout()
    {
        var sprite = GetNode<Sprite>("Sprite");
        sprite.Visible = !sprite.Visible;
    }
}

커스텀 시그널

Godot에 당신의 커스텀 시그널을 선언할 수 있습니다:

extends Node

signal my_signal
public class Main : Node
{
    [Signal]
    public delegate void MySignal();
}

시그널은 선언하면, 커스텀 시그널은 인스펙터에 나타나고 노드의 내장 시그널과 같은 방식으로 연결할 수 있습니다.

코드를 통해 시그널을 방출하기 위해선, emit 함수를 사용합니다:

extends Node

signal my_signal

func _ready():
    emit_signal("my_signal")
public class Main : Node
{
    [Signal]
    public delegate void MySignal();

    public override void _Ready()
    {
        EmitSignal(nameof(MySignal));
    }
}

발사 예제

시그널 사용의 다른 예제로, 마우스 방향으로 회전하며 발사할 수 있는 플레이어를 생각해봅시다. 마우스 버튼을 클릭할 때 마다, 플레이어의 위치에 총알의 인스턴스를 만듭니다. 자세한 설명은 인스턴스(Instance)를 참고하세요.

하지만, 총알이 플레이어의 자식으로 추가된다면, 플레이어가 회전하는 것에 따라 "붙어있는 채로" 남아있을 것입니다:

../../_images/signals_shoot1.gif

대신, 총알을 플레이어의 움직임에 독립하도록 해야 합니다 - 일단 발사가 되면, 플레이어가 그것에 영향을 주지 않고 직선으로 계속 나아가야 합니다. 플레이어의 자식으로 씬 트리에 추가하는 대신, 총알을 "메인" 게임 씬의 자식으로 추가하는 것이 더 합리적입니다, 이렇게 하면 총알이 플레이어의 부모나 더 위로 올라갈 수도 있습니다.

총알에 직접 이 코드를 추가하는 것으로 가능합니다:

var bullet_instance = Bullet.instance()
get_parent().add_child(bullet_instance)
Node bulletInstance = Bullet.Instance();
GetParent().AddChild(bulletInstance);

그러나 이것은 다른 문제를 불러올 수 있습니다. 이제 "Player" 씬을 독립적으로 테스트해 보신다면, 액세스할 부모 노드가 없기 때문에, 발사를 할 때 팅길 것입니다. 이는 플레이어 코드를 독립적으로 테스트하기가 더 까다로울 뿐만 아니라, 메인 씬의 노드 구조를 바꾸기로 한다면 플레이어의 부모는 더 이상 총알을 받기에 적절한 노드가 아니게 됩니다.

해결책은 플레이어로부터 총알을 "방출"하는 시그널을 사용하는 것입니다. 그렇게 하면 플레이어는 총알이 어떻게 되는지 "알" 필요가 없습니다 - 이 시그널에 연결된 어떤 노드라도 총알을 "받을" 수 있고 총알을 스폰하기 위한 적절한 행동을 취해줄 수 있습니다.

이것이 시그널을 사용해 플레이어가 총알을 방출하는 코드입니다:

extends Sprite

signal shoot(bullet, direction, location)

var Bullet = preload("res://Bullet.tscn")

func _input(event):
    if event is InputEventMouseButton:
        if event.button_index == BUTTON_LEFT and event.pressed:
            emit_signal("shoot", Bullet, rotation, position)

func _process(delta):
    look_at(get_global_mouse_position())
public class Player : Sprite
{
    [Signal]
    delegate void Shoot(PackedScene bullet, Vector2 direction, Vector2 location);

    private PackedScene _bullet = GD.Load<PackedScene>("res://Bullet.tscn");

    public override void _Input(InputEvent event)
    {
        if (input is InputEventMouseButton mouseButton)
        {
            if (mouseButton.ButtonIndex == (int)ButtonList.Left && mouseButton.Pressed)
            {
                EmitSignal(nameof(Shoot), _bullet, Rotation, Position);
            }
        }
    }

    public override _Process(float delta)
    {
        LookAt(GetGlobalMousePosition());
    }
}

메인 씬에서, 이제 플레이어의 시그널을 연결합니다 ("노드" 탭에 나타날 것입니다).

func _on_Player_shoot(Bullet, direction, location):
    var b = Bullet.instance()
    add_child(b)
    b.rotation = direction
    b.position = location
    b.velocity = b.velocity.rotated(direction)
public void _on_Player_Shoot(PackedScene bullet, Vector2 direction, Vector2 location)
{
    var bulletInstance = (Bullet)bullet.Instance();
    AddChild(bulletInstance);
    bulletInstance.Rotation = direction;
    bulletInstance.Position = location;
    bulletInstance.Velocity = bulletInstance.Velocity.Rotated(direction);
}

이제 총알은 움직임을 유지한 채로 플레이어의 회전에 독립을 유지할 것입니다:

../../_images/signals_shoot2.gif

결론

Godot의 내장 노드 타입의 대다수는 이벤트를 감지하는 데 사용할 수 있는 시그널을 제공합니다. 예를 들어, 동전 하나를 나타내는 Area2D <class_Area2D>`는 플레이어의 물리 바디가 충돌 모양에 들어갈 때마다 ``body_entered` 시그널을 방출하며, 이를 통해 플레이어가 동전을 언제 먹었는 지를 알 수 있습니다.

다음 섹션은, 당신의 첫 게임으로, 서로 다른 게임 구성 요소들을 여러 시그널을 사용하여 연결하는 것을 포함한 완전한 게임을 만들 것입니다.