C++ 델리게이트 심화: 언리얼 스타일을 단순화한 구조로 확장하기

//
//

📚 델리게이트 시리즈

1편: 함수 포인터
2편: std::function
3편: Delegate 구현
4편: 엔진 스타일 Delegate

🚀 들어가며

이전 글에서는 std::function을 기반으로
👉 멀티캐스트 델리게이트 구조를 직접 구현해봤습니다.

class Delegate
{
public:
    void Add(std::function<void()> func)
    {
        functions.push_back(func);
    }

    void Broadcast()
    {
        for (auto& func : functions)
        {
            func();
        }
    }

private:
    std::vector<std::function<void()>> functions;
};

이 구조만으로도 충분히 이벤트 시스템을 만들 수 있습니다.

하지만 실제 게임 엔진에서는 여기서 한 단계 더 나아갑니다.

❓ “등록된 함수 중 일부만 제거하려면?”
❓ “객체가 파괴되었을 때 자동으로 정리하려면?”
❓ “더 빠르게 호출하려면?”

👉 이 문제들을 해결하기 위해 엔진 스타일 델리게이트 구조가 등장합니다.


🎯 기존 구조의 한계

기존에 만든 구조는 간단하지만 다음 문제가 있습니다.


❌ 개별 제거 불가능

functions.push_back(func);

👉 특정 함수만 제거하기 어렵습니다


❌ 객체 생명주기 관리 없음

onClick.Add([&player]()
{
    player.Attack();
});

👉 player가 사라지면?

👉 댕글링 참조 위험


❌ 성능 오버헤드

std::function → 유연하지만 비용 있음

🎯 엔진이 선택한 해결 방식

게임 엔진에서는 다음 구조를 사용합니다.

함수 리스트
+ 등록 ID
+ 객체 추적
+ 안전한 제거

🧠 핵심 설계 아이디어

1️⃣ 함수마다 ID 부여

함수 등록
↓
고유 ID 반환
↓
ID로 제거

2️⃣ 객체 기반 바인딩

객체 + 함수 묶기

3️⃣ 안전한 실행

유효한 객체만 실행

🎮 개선된 Delegate 구현

✔ 등록 ID 구조
(아래 코드는 C++17 이상 필요합니다.)

#include <vector>
#include <functional>
#include <algorithm>

class Delegate
{
public:
    using DelegateID = size_t;

    DelegateID Add(std::function<void()> func)
    {
        functions.emplace_back(nextId, func);
        return nextId++;
    }

    void Remove(DelegateID id)
    {
        functions.erase(
            std::remove_if(functions.begin(), functions.end(),
                [id](const auto& pair)
                {
                    return pair.first == id;
                }),
            functions.end());
    }

    void Broadcast()
    {
        // 이 코드는 C++17 이상 필요합니다.
        for (auto& [id, func] : functions)
        {
            func();
        }
    }

private:
    std::vector<std::pair<DelegateID, std::function<void()>>> functions;
    DelegateID nextId = 0;
};

🎯 사용 예제

Delegate onClick;

auto id1 = onClick.Add([]()
{
    std::cout << "Sound\n";
});

auto id2 = onClick.Add([]()
{
    std::cout << "UI\n";
});

onClick.Broadcast();

👉 출력:

Sound
UI

🔥 특정 함수 제거

onClick.Remove(id1);

onClick.Broadcast();

👉 출력:

UI

🎯 이 구조의 의미

등록 → ID 반환
↓
ID 기반 관리
↓
선택적 제거 가능

👉 이것이 엔진 구조의 핵심입니다


🧠 객체 기반 바인딩 개선

문제:

[&player]() { player.Attack(); }

👉 객체가 사라지면 위험


✔ 안전한 방식 (weak_ptr)

std::weak_ptr<Player> weakPlayer = player;

onClick.Add([weakPlayer]()
{
    if (auto p = weakPlayer.lock())
    {
        p->Attack();
    }
});

🎯 의미

객체 생존 확인
↓
살아있으면 실행
↓
아니면 무시

👉 안전한 이벤트 시스템


🎮 엔진에서의 실제 구조

게임 엔진에서는 이런 식으로 동작합니다.

객체 등록
↓
델리게이트 바인딩
↓
이벤트 발생
↓
유효한 객체만 실행

🔥 왜 이렇게 복잡한가?

단순히 콜백을 넘어서:

✔ 안정성
✔ 성능
✔ 확장성

👉 이 세 가지를 동시에 만족해야 하기 때문입니다


🎯 구조 비교

단계 특징
함수 포인터 단순
std::function 유연
Delegate 확장
엔진 Delegate 안전 + 관리

//

🧠 시리즈 전체 흐름

함수 포인터
↓
std::function
↓
Delegate
↓
엔진 Delegate

👉 이 흐름이 이해되면 이벤트 시스템이 보입니다.


🎯 핵심 정리

  • 델리게이트는 이벤트 시스템의 핵심 구조이다
  • ID 기반으로 등록/제거를 관리한다
  • weak_ptr을 이용해 객체 생명주기를 안전하게 처리한다
  • 엔진에서는 성능과 안정성을 위해 더 복잡한 구조를 사용한다

실제 언리얼의 델리게이트는 매크로, UObject 생명주기, 리플렉션 시스템과 연결되어 훨씬 복잡합니다.

이 글의 코드는 그 구조를 그대로 구현한 것이 아니라,
엔진 델리게이트가 왜 ID 관리와 객체 생명주기 처리를 필요로 하는지를 이해하기 위해 예제를 단순화했습니다.


🎮 마무리

이 시리즈를 통해 우리는 다음을 직접 구현해봤습니다.

  • 함수 포인터
  • std::function
  • 델리게이트
  • 엔진 스타일 델리게이트

👉 이 모든 구조는 결국 하나로 연결됩니다.

이벤트 시스템

이 구조를 이해하면 게임 엔진의 입력 시스템과 이벤트 흐름이 어떻게 연결되는지 보이기 시작합니다.


🚀 한 단계 더 나아가고 싶다면

델리게이트 구조는
입력 시스템, UI 이벤트, 게임 로직과 깊게 연결됩니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다