Unity - '한 우산 아래'

우산에 담긴 물의 무게로 문을 여는 퍼즐 만들기

망고와플 2026. 4. 12. 15:12

회의 끝에 우산을 사용한 기본적인 예시 로직 몇개(일정 무게가 되면 눌리는 발판, 어떤 곳에 물을 부워 풀어나가는 퍼즐 등)를 구현해보기로 했다.

우산에 담긴 물을 퍼즐 조건으로 사용하기

이전 작업에서 우산을 거꾸로 들면 비를 받을 수 있고, 받은 물의 양만큼 플레이어의 무게가 증가하도록 만들었다.

처음에는 이 기능이 단순히 우산 안에 물이 찬다는 상태 표현에 가까웠다. 하지만 생각해보니 물이 찬 우산은 단순한 게이지가 아니라, 퍼즐에서 사용할 수 있는 조건이 될 수 있었다.

예를 들어 플레이어가 우산에 물을 충분히 담은 뒤 발판 위에 올라가면, 증가한 무게 때문에 발판이 눌리고 문이 열리는 식이다.

 

이번 작업의 목표는 다음과 같았다.

우산에 물을 담는다
-> 플레이어의 Rigidbody.mass가 증가한다
-> 무게 발판 위에 올라간다
-> 발판이 조건을 만족한다
-> 문이나 플랫폼이 반응한다

여기서 중요하게 생각한 점은 발판이 우산 시스템을 직접 알 필요는 없다는 것이다.

발판은 단지 자기 위에 올라온 Rigidbody의 mass만 읽으면 된다.

플레이어가 무거워진 이유가 우산 때문인지, 상자를 들었기 때문인지, 다른 추 오브젝트 때문인지는 발판 입장에서 중요하지 않다.

이렇게 만들어두면 나중에 우산뿐 아니라 상자, 돌, 추 같은 오브젝트도 같은 퍼즐 규칙에 넣을 수 있다.

무게를 읽는 WeightSensor

먼저 만든 것은 WeightSensor.cs다.

이 스크립트는 Trigger Collider 안에 들어온 Rigidbody들을 추적하고, 현재 총 무게를 계산한다.

구조는 대략 이렇게 잡았다.

WeightedButton
├─ ButtonVisual
├─ ReleasedPoint
├─ PressedPoint
└─ WeightSensor

WeightSensor의 역할은 단순하다.

- 센서 안에 들어온 Rigidbody들의 mass를 합산한다.

중요한 점은 Rigidbody가 센서에 들어온 순간의 mass만 저장하지 않는다는 것이다.

우산에 물이 담기면 플레이어의 Rigidbody.mass가 실시간으로 바뀐다.

따라서 센서는 매 FixedUpdate마다 현재 mass를 다시 읽어야 한다.

private void RefreshCurrentWeight()
{
    float totalWeight = 0.0f;

    foreach (KeyValuePair<Rigidbody, int> entry in bodyContactCounts)
    {
        Rigidbody body = entry.Key;
        if (body == null)
        {
            continue;
        }

        totalWeight += body.mass;
    }

    currentWeight = totalWeight;
}

이렇게 하면 플레이어가 발판 위에 올라간 뒤 우산에 물을 더 담거나, 반대로 물을 쏟아서 가벼워지는 상황도 반영할 수 있다.

또 센서 안에 들어온 Collider가 여러 개일 수 있기 때문에 Rigidbody 단위로 관리했다.

하나의 Rigidbody가 여러 Collider를 가지고 있을 수 있으므로, 같은 Rigidbody가 중복 계산되지 않도록 접촉 횟수도 함께 기록했다.

이 부분은 나중에 플레이어나 퍼즐 오브젝트 구조가 복잡해졌을 때 필요할 수 있다고 생각했다.

버튼은 조건만 판단하게 하기

다음으로 만든 것은 WeightedButton.cs다.

이 스크립트는 WeightSensor가 계산한 현재 무게를 보고, 버튼이 눌렸는지 아닌지를 판단한다.

현재 무게 >= Press Weight
→ 버튼 눌림

현재 무게 <= Release Weight
→ 버튼 해제

Press Weight와 Release Weight를 따로 둔 이유는 버튼이 경계값 근처에서 덜덜거리는 것을 막기 위해서다.

예를 들어 누르는 기준이 3.0이고, 해제 기준도 3.0이면 무게가 아주 조금만 흔들려도 버튼이 계속 눌렸다 풀릴 수 있다.

그래서 누르는 기준은 3.0, 풀리는 기준은 2.5처럼 약간의 여유를 두었다.

버튼이 눌리면 실제 발판 오브젝트는 PressedPoint 쪽으로 이동하고, 버튼이 풀리면 ReleasedPoint 쪽으로 돌아간다.

Transform targetPoint = isPressed ? pressedPoint : releasedPoint;

buttonVisual.position = Vector3.MoveTowards(
    buttonVisual.position,
    targetPoint.position,
    moveSpeed * Time.fixedDeltaTime);

여기서 buttonVisual만 움직이도록 한 것이 중요했다.

작업 중 실수도 있었다.

처음에는 ButtonVisual에 실제 발판 오브젝트가 아니라 WeightedButton 루트 오브젝트를 넣었다. 그러자 버튼 전체가 움직였다.

문제는 루트 오브젝트가 움직이면 그 안에 있는 ReleasedPoint, PressedPoint도 같이 움직인다는 점이다.

결과적으로 버튼은 목표 지점을 향해 움직이는데, 목표 지점도 같이 움직여버렸다.

그래서 발판이 계속 위로 올라가는 이상한 상황이 생겼다.

이후 구조를 이렇게 정리했다.

루트 오브젝트 - 관리자 역할

Visual 오브젝트 - 실제로 움직이는 오브젝트

Point 오브젝트 - 움직이지 않는 기준점

즉 올바른 구조는 다음과 같다.

WeightedButton
├─ ButtonVisual       실제로 움직이는 발판
├─ ReleasedPoint      안 눌린 위치
├─ PressedPoint       눌린 위치
└─ WeightSensor       무게 감지 Trigger

문은 PuzzleMover로 분리하기

문은 PuzzleMover.cs로 처리했다.

PuzzleMover는 어떤 버튼이 자신을 작동시키는지 알지 않는다.

그냥 Activate()가 호출되면 ActivePoint로 움직이고, Deactivate()가 호출되면 InactivePoint로 움직인다.

문 구조는 이렇게 잡았다.

PuzzleDoor
├─ DoorVisual
├─ ClosedPoint
└─ OpenPoint

여기서도 발판과 같은 실수가 생길 수 있다.

PuzzleMover의 Moving Target에 PuzzleDoor 루트를 넣으면 문 전체가 움직인다.

그러면 그 안에 있는 OpenPoint도 같이 움직이고, 문은 계속 OpenPoint를 따라가면서 끝없이 올라가게 된다.

그래서 문도 같은 규칙을 따른다.

PuzzleDoor 루트 - 관리자

DoorVisual - 실제로 움직이는 문

ClosedPoint / OpenPoint - 움직이지 않는 기준점

실제 이동은 단순하게 MoveTowards를 사용했다.

Transform targetPoint = activated ? activePoint : inactivePoint;

movingTarget.position = Vector3.MoveTowards(
    movingTarget.position,
    targetPoint.position,
    moveSpeed * Time.fixedDeltaTime);

처음부터 물리 기반 문으로 만들 수도 있었지만, 퍼즐에서는 예측 가능성이 중요하다고 생각했다.

문이 열리는 위치와 속도가 일정해야 플레이어가 퍼즐 조건을 이해하기 쉽기 때문이다.

UnityEvent로 버튼과 문 연결하기

버튼과 문은 직접 코드로 강하게 묶지 않고, UnityEvent로 연결했다.

처음에는 버튼 스크립트 안에서 문을 직접 참조하는 방식도 생각할 수 있다.

[SerializeField] private PuzzleMover door;

private void SetPressed(bool pressed)
{
    if (pressed)
    {
        door.Activate();
    }
    else
    {
        door.Deactivate();
    }
}

이 방식은 단순하고 빠르다.

하지만 버튼이 항상 문만 여는 것은 아니다.

나중에는 버튼을 밟았을 때 문이 열릴 수도 있고, 플랫폼이 움직일 수도 있고, 불이 꺼질 수도 있고, 어떤 오브젝트가 사라질 수도 있다.

그때마다 WeightedButton 안에 새로운 참조를 추가하면 버튼 스크립트가 점점 복잡해진다.

그래서 이번에는 UnityEvent를 사용했다.

[SerializeField] private UnityEvent onPressed = new UnityEvent();
[SerializeField] private UnityEvent onReleased = new UnityEvent();

UnityEvent는 Unity에서 제공하는 직렬화 가능한 이벤트 타입이다.

Inspector에서 어떤 오브젝트의 어떤 함수를 호출할지 연결할 수 있다.

코드에서는 버튼이 눌렸는지만 판단하고, 실제 반응은 이벤트를 호출하는 방식으로 처리한다.

if (isPressed)
{
    onPressed.Invoke();
}
else
{
    onReleased.Invoke();
}

즉 WeightedButton은 PuzzleMover를 직접 모른다.

그저 눌렸다/풀렸다(Release)는 이벤트를 발생시킨다.

실제 연결은 Inspector에서 한다.

On Pressed  -> PuzzleMover.Activate()
On Released -> PuzzleMover.Deactivate()

이렇게 만들면 버튼은 입력 장치가 되고, 문이나 플랫폼은 출력 장치가 된다.

둘 사이의 관계는 코드가 아니라 Inspector에서 연결된다.

UnityEvent는 어떻게 동작하는가

UnityEvent를 이해할 때, 언리얼의 Delegate를 떠올리면 조금 더 감이 잘 온다.

언리얼에서 Delegate는 어떤 이벤트가 발생했을 때, 그 이벤트에 바인딩된 함수를 호출하는 구조다.

예를 들어 버튼이 눌렸을 때 OnPressed라는 Delegate를 실행하면, 여기에 등록된 함수들이 호출된다.

간단히 보면 이런 흐름이다.

이벤트를 가진 객체
-> Delegate를 가지고 있음

다른 객체
-> Delegate에 자기 함수를 바인딩함

이벤트 발생
-> Delegate.Broadcast() 또는 Execute()
-> 바인딩된 함수들이 호출됨

언리얼에서는 델리게이트 종류가 여러 가지 있다.

//하나의 함수만 연결
Single-cast Delegate

//여러 함수들 연결
Multicast Delegate

//리플렉션 시스템을 통해 UObject 함수와 연결 가능
Dynamic Delegate

//여러 UObject 함수를 연결할 수 있고, Blueprint에서도 바인딩 가능
Dynamic Multicast Delegate

이 중에서 UnityEvent와 사용감이 가장 비슷한 것은 Dynamic Multicast Delegate 쪽이라고 느꼈다.

예를 들어 언리얼에서는 이런 식으로 선언할 수 있다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPressed);

UPROPERTY(BlueprintAssignable)
FOnPressed OnPressed;

이렇게 해두면 Blueprint에서 OnPressed에 이벤트를 연결할 수 있다.

그리고 C++ 코드에서 특정 순간에 다음처럼 호출한다.

OnPressed.Broadcast();

그러면 Blueprint나 코드에서 바인딩된 함수들이 실행된다.

UnityEvent도 비슷한 흐름을 가진다.

[SerializeField] private UnityEvent onPressed = new UnityEvent();

Inspector에서 이 onPressed 이벤트에 오브젝트와 함수를 연결할 수 있다.

그리고 코드에서는 특정 순간에 다음처럼 호출한다.

onPressed.Invoke();

그러면 Inspector에 연결된 함수들이 실행된다.

즉 흐름만 비교하면 이렇게 볼 수 있다.

Unreal
Delegate에 함수 바인딩
-> Broadcast()

Unity
UnityEvent에 함수 연결
-> Invoke()

조금 더 구체적으로 비교하면 다음과 같다.

Unreal Dynamic Multicast Delegate
- UPROPERTY(BlueprintAssignable)로 노출
- Blueprint에서 이벤트에 함수 연결 가능
- Broadcast()로 연결된 함수 호출
- UObject 리플렉션 시스템을 기반으로 함수 연결
- 여러 함수를 동시에 바인딩 가능
UnityEvent
- [SerializeField]로 Inspector에 노출
- Inspector에서 오브젝트와 함수 연결 가능
- Invoke()로 연결된 함수 호출
- Unity 직렬화 시스템을 통해 연결 정보 저장
- 여러 함수를 동시에 연결 가능

그래서 이번 작업의 WeightedButton은 언리얼식으로 생각하면 이런 느낌이다.

WeightedButton
-> OnPressed라는 Dynamic Multicast Delegate를 가지고 있음

PuzzleMover
-> OnPressed에 Activate 함수를 바인딩함

버튼이 눌림
-> OnPressed.Broadcast()
-> PuzzleMover.Activate() 실행

Unity에서는 이렇게 된다.

WeightedButton
-> UnityEvent onPressed를 가지고 있음

Inspector
-> onPressed에 PuzzleMover.Activate() 연결

버튼이 눌림
-> onPressed.Invoke()
-> PuzzleMover.Activate() 실행

코드로 보면 비교가 더 쉽다.

// Unreal
OnPressed.Broadcast();

// Unity
onPressed.Invoke();

둘 다 이벤트 발생 사실을 알리고, 등록된 반응들을 호출한다는 점에서는 비슷하다.

둘 다 옵저버 패턴인가?

큰 관점에서는 둘 다 옵저버 패턴과 관련이 있다고 볼 수 있다.

옵저버 패턴은 어떤 객체의 상태 변화나 이벤트 발생을 다른 객체들이 구독하고 있다가, 이벤트가 발생하면 알림을 받는 구조다.

//이벤트를 가지고 있음
Subject

//이벤트를 구독함
Observer

//Observer들에게 알림
Subject에 변화 발생

이번 구조에 대입하면 이렇게 볼 수 있다.

//Subject
WeightedButton

//Event
onPressed, onReleased

//Observer
PuzzleMover, BurnableDoor, MovingPlatform 등

//Notify
UnityEvent.Invoke()

언리얼도 비슷하다.

//Subject
Button Actor

//Event
OnPressed Delegate

//Observer
Door Actor, Platform Actor 등

//Notify
Delegate.Broadcast()

그래서 개념적으로는 둘 다 옵저버 패턴의 형태를 띤다고 볼 수 있다.

다만 전통적인 옵저버 패턴과 완전히 똑같다고 말하기보다는, 게임 엔진에서 사용할 수 있게 변형된 이벤트/델리게이트 시스템이라고 보는 편이 더 정확할 것 같다.

전통적인 옵저버 패턴에서는 보통 옵저버 객체가 특정 인터페이스를 구현하고, Subject가 그 옵저버 목록을 관리하면서 Notify()를 호출한다.

예를 들면 이런 느낌이다.

//IObserver
OnNotify()

//Subject
List<IObserver>
NotifyAll()

하지만 UnityEvent나 언리얼 Delegate는 꼭 특정 Observer 인터페이스를 구현할 필요가 없다.

대신 함수 자체를 이벤트에 연결한다.

전통적인 옵저버 패턴
→ 객체가 인터페이스를 구현하고 구독됨

UnityEvent / Unreal Delegate
→ 함수가 이벤트에 연결됨

그래서 더 정확히 말하면:

UnityEvent와 Unreal Delegate는 옵저버 패턴의 아이디어를 엔진에 맞게 구현한 이벤트 시스템에 가깝다.

라고 정리할 수 있을 것 같다.

UnityEvent와 Unreal Delegate의 차이

비슷한 점도 많지만, 사용하면서 느껴지는 차이도 있다.

언리얼은 C++ 코드 레벨에서 델리게이트 타입을 명확히 선언하고, AddDynamic, AddUObject, AddLambda 같은 방식으로 바인딩하는 경우가 많다.

OnPressed.AddDynamic(this, &ADoor::Open);

Blueprint에 노출하고 싶다면 UPROPERTY(BlueprintAssignable) 같은 매크로를 붙인다.

반면 UnityEvent는 Unity의 Inspector 중심 워크플로우와 잘 맞는다.

[SerializeField] private UnityEvent onPressed;

이렇게 필드를 열어두면, Inspector에서 바로 오브젝트를 드래그하고 함수를 선택할 수 있다.

또 한 가지 차이는 연결 정보가 저장되는 방식이다.

언리얼의 Dynamic Delegate는 리플렉션 시스템을 통해 UObject와 UFunction을 찾아 호출한다.

UnityEvent는 Unity 직렬화 시스템을 통해 대상 오브젝트와 메서드 이름 같은 호출 정보를 Scene이나 Prefab에 저장한다.

그래서 UnityEvent를 사용하면 코드가 깔끔해지는 대신, 연결 관계가 코드 파일만 봐서는 잘 보이지 않을 수 있다.

예를 들어 WeightedButton.cs만 보면 onPressed.Invoke()가 호출된다는 것은 알 수 있다.

하지만 실제로 그 이벤트에 PuzzleMover.Activate()가 연결되어 있는지, BurnableDoor.BurnAway()가 연결되어 있는지는 Inspector를 열어봐야 한다.

이 점은 편리함과 동시에 단점이기도 하다.

장점
- 코드 수정 없이 Inspector에서 반응을 바꿀 수 있음

단점
- 연결 관계가 코드만 봐서는 잘 안 보임

이번 작업에서도 실제로 어떤 버튼이 어떤 문과 연결되어 있는지 확인하기 어려워졌다.

그래서 F3 디버그에서 UnityEvent 연결 관계를 점선으로 표시하도록 했다.

이벤트 연결만 바꿔도 퍼즐 성격이 달라진다

UnityEvent를 사용하니 같은 버튼과 문이라도 연결 방식에 따라 퍼즐 성격을 바꿀 수 있었다.

//밟고 있는 동안만 열림
On Pressed  -> Activate()
On Released -> Deactivate()

이 방식은 가장 기본적인 압력판 느낌이다.

플레이어가 발판 위에 있을 때만 문이 열리고, 내려오면 다시 닫힌다.

//밟으면 올라가고, 내려오면 현재 위치에서 멈춤
On Pressed  -> Activate()
On Released -> Pause()

이 방식은 조금 더 퍼즐 장치에 가깝다.

발판을 밟고 있는 동안만 문이 올라가고, 내려오면 그 위치에서 멈춘다.

다시 밟으면 멈춘 위치에서 이어서 올라간다.

//한 번 밟으면 끝까지 열림
On Pressed  -> Activate()
On Released -> 비워둠

이 방식은 일회성 스위치에 가깝다.

버튼을 잠깐 밟기만 해도 문이 끝까지 열린다.

이 부분이 꽤 마음에 들었다.

새로운 버튼 스크립트를 계속 만들지 않아도, 이벤트 연결만 바꿔서 퍼즐의 성격을 바꿀 수 있기 때문이다.

그래서 PuzzleMover에는 Pause()도 추가했다.

버튼에서 내려왔을 때 문이 다시 닫히는 것이 아니라, 현재 위치에서 멈추게 하기 위해서다.

public void Pause()
{
    paused = true;
}

public void Resume()
{
    paused = false;
}

public void SetActivated(bool value)
{
    activated = value;
    paused = false;
}

이렇게 해두면 Activate()나 Deactivate()가 다시 호출될 때는 멈춤 상태가 풀리고, 해당 방향으로 다시 움직인다.

F3로 퍼즐 연결 확인하기

UnityEvent를 사용하면 구조가 유연해지는 대신, Inspector를 열어보기 전까지는 어떤 버튼이 어떤 오브젝트와 연결되어 있는지 한눈에 알기 어렵다.

그래서 기존 F3 디버그에 퍼즐 연결 표시도 추가했다.

처음에는 실선으로 연결을 표시했는데, 화면을 너무 많이 가려서 보기 힘들었다.

특히 버튼 라벨과 선이 겹치면서 글자를 읽기 불편했다.

그래서 연결선은 얇고 반투명한 점선으로 바꾸고, 점선이 천천히 흐르도록 했다.

초록색 점선
- OnPressed 연결

하늘색 점선
- OnReleased 연결

선이 흐르게 만든 이유는 단순히 예쁘게 보이기 위해서만은 아니었다.

정적인 선은 그냥 두 오브젝트 사이의 관계처럼 보이지만, 흐르는 점선은 버튼에서 문 쪽으로 신호가 전달된다는 느낌을 준다.

퍼즐 연결을 디버깅하는 화면에서는 이 차이가 꽤 크게 느껴졌다.

또 선이 버튼 UI 글자를 가리지 않도록, 라벨 아래쪽이 아니라 위쪽에서 선이 빠져나가게 조정했다.

처음에는 버튼의 월드 기준점에서 바로 선을 그렸는데, 이러면 라벨 중앙을 선이 지나가면서 글자를 읽기 어려웠다.

그래서 Game View에서는 라벨의 위쪽 가장자리에서 선이 시작되도록 바꿨다.

(사실 현재로써는 큰 차이는 없었다. 하지만 나중에 많은 퍼즐이 들어가면 라벨을 계속 가리게 될테니 수정했다.)

기존
ButtonVisual 위쪽 중앙에서 선 시작
→ 라벨 글자를 가림

변경
버튼 라벨 위쪽에서 선 시작
→ 글자를 덜 가림

이제 F3를 켜면 버튼의 현재 상태, 필요한 무게, 연결된 이벤트 개수, 그리고 어떤 대상과 연결되어 있는지를 확인할 수 있다.

Debug Anchor 추가

디버그 선의 연결 기준점도 직접 지정할 수 있게 했다.

기본적으로 버튼 쪽 선은 ButtonVisual의 Renderer 위쪽 중앙에서 시작한다.

문 쪽 선은 PuzzleMover의 Moving Target 위쪽 중앙을 기준으로 한다.

하지만 오브젝트 크기나 카메라 각도에 따라 선이 보기 좋지 않을 수 있다.

그래서 Debug Anchor를 따로 넣을 수 있게 했다.

WeightedButton
├─ ButtonVisual
├─ ButtonDebugAnchor
├─ ReleasedPoint
├─ PressedPoint
└─ WeightSensor

ButtonDebugAnchor 같은 빈 오브젝트를 만들고, WeightedButton의 Debug Anchor에 넣으면 그 위치에서 선이 시작된다.

문도 마찬가지다.

PuzzleDoor
├─ DoorVisual
├─ DoorDebugAnchor
├─ ClosedPoint
└─ OpenPoint

PuzzleMover의 Debug Anchor에 DoorDebugAnchor를 넣으면 선이 그 위치로 연결된다.

이렇게 해두면 나중에 아트 오브젝트가 복잡해져도, 디버그 선이 보기 좋은 위치에서 나오도록 직접 조정할 수 있다.

퍼즐 툴에 대한 고민

작업 중 발판과 문을 한 번에 생성하는 에디터 툴도 생각했다.

실제로 기획자나 아트 작업자가 나중에 퍼즐을 배치해야 한다면 툴은 필요할 것이다.

모든 사람이 매번 WeightSensor, ButtonVisual, ReleasedPoint, PressedPoint, UnityEvent 연결을 직접 맞추는 것은 실수가 많을 수 있다.

하지만 지금 단계에서는 자동 생성 툴을 먼저 만드는 것보다, 직접 오브젝트를 배치하고 연결하면서 구조를 익히는 편이 더 맞다고 판단했다.

아직은 퍼즐 문법을 만드는 단계에 가깝다.

퍼즐 구조가 더 쌓이기 전에 툴부터 크게 만들면, 나중에 퍼즐 구조가 바뀔 때 툴도 같이 흔들릴 가능성이 있다.

그래서 지금은 자동 생성 툴보다, 나중에 잘못된 연결을 검사해주는 도구가 먼저일 것 같다.

예를 들면 이런 식이다.

Moving Target에 루트가 들어가 있으면 경고
Point가 Visual의 자식이면 경고
WeightSensor의 Is Trigger가 꺼져 있으면 경고
OnPressed 연결 상태 확인
OnReleased 연결 상태 확인
Debug Anchor 누락 확인

퍼즐을 몇 개 더 직접 만들어보고, 반복되는 실수와 불편함이 보이면 그때 툴로 확장하는 편이 좋을 것 같다.

마무리

이번 작업으로 우산의 물 무게가 실제 퍼즐 조건으로 연결되기 시작했다.

아직은 단순한 발판과 문이지만, 구조 자체는 확장 가능하게 잡으려고 했다.

//무게를 읽는다
WeightSensor

//조건을 판단한다.
WeightedButton

//반응대상을 연결한다.
UnityEvent

//실제 오브젝트를 움직인다
PuzzleMover

발판은 우산 시스템을 직접 알지 않고 Rigidbody mass만 읽는다.

버튼은 눌림 이벤트만 발생시키고, 실제 반응은 연결된 오브젝트가 결정한다.

또 UnityEvent를 사용하면서, 버튼과 문의 관계를 코드로 강하게 묶지 않고 Inspector에서 조합할 수 있게 되었다.

이 방식은 언리얼의 Dynamic Multicast Delegate처럼, 이벤트가 발생했을 때 외부에서 연결된 함수들을 호출하는 구조와 비슷하게 이해할 수 있었다.

물론 Inspector 기반 연결은 코드만 봐서는 관계가 잘 보이지 않는다는 단점도 있었다.

그래서 F3 디버그에서 연결 관계를 점선으로 표시하도록 보완했다.

이번 작업은 단순히 문 하나를 여는 기능이라기보다, 우산 시스템이 퍼즐 문법으로 이어지기 시작한 첫 단계였다고 생각한다.

나중에는 이 구조를 기반으로 플랫폼, 엘리베이터, 불타는 장애물, 양팔저울 같은 퍼즐로도 확장해볼 수 있을 것 같다.