#pragma once

#include "CoreMinimal.h"
#include "HazardTypes.generated.h"

UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EHazardFlags : uint8
{
    None            = 0        UMETA(Hidden),
    Skid            = 1 << 0,
    HighLateralG    = 1 << 1,
    YawInstability  = 1 << 2,
    LaneDeparture   = 1 << 3,
    RolloverRisk    = 1 << 4,
    HarshManeuver   = 1 << 5,
    FallOff         = 1 << 6,
};
ENUM_CLASS_FLAGS(EHazardFlags);

UENUM(BlueprintType)
enum class EHazardPhase : uint8
{
    Enter   UMETA(DisplayName = "진입"),
    Sustain UMETA(DisplayName = "지속"),
    Exit    UMETA(DisplayName = "해제"),
};

USTRUCT(BlueprintType)
struct FHazardEvent
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadOnly)
    double TimeStamp = 0.0;

    UPROPERTY(BlueprintReadOnly, meta = (Bitmask, BitmaskEnum = "EHazardFlags"))
    int32 ActiveFlags = 0;

    UPROPERTY(BlueprintReadOnly)
    EHazardPhase Phase = EHazardPhase::Enter;

    UPROPERTY(BlueprintReadOnly)
    FVector WorldLocation = FVector::ZeroVector;

    UPROPERTY(BlueprintReadOnly)
    float Speed = 0.f;

    UPROPERTY(BlueprintReadOnly)
    float SlipAngleDeg = 0.f;

    UPROPERTY(BlueprintReadOnly)
    float LateralG = 0.f;

    UPROPERTY(BlueprintReadOnly)
    float YawRate = 0.f;

    UPROPERTY(BlueprintReadOnly)
    float CrossTrackError = 0.f;

    UPROPERTY(BlueprintReadOnly)
    float RollDeg = 0.f;
};

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
    FOnHazardDetected, const FHazardEvent&, HazardEvent);

 

struct

관련된 변수들을 하나의 묶음으로 정의하는 양식.
차 한 대를 표현할 때 색깔/속도/연료를 따로 변수로 풀어 쓰면 차 100대에 변수 300개가 된다.
struct로 한 묶음으로 만들면 차 한 대가 변수 하나로 표현된다.

struct FCar {
    FString Color;
    float Speed;
    float Fuel;
};

FCar MyCar;
MyCar.Color = "Red";
MyCar.Speed = 80.0f;


점(.)은 묶음 안의 멤버에 접근하는 표시. 함수에 넘길 때도 차 한 대 통째로 전달 가능.

언리얼 관례: struct 이름 앞에 F를 붙인다.
Class는 U 또는 A, Enum은 E, Struct는 F. 코드 첫 글자만 봐도 데이터 타입이 보인다.

HazardTypes.h의 FHazardEvent가 이거다.
위험 이벤트 한 건이라는 양식을 미리 정의해 두고, 감지 시 통째로 채워서 던진다.

enum

의미 있는 이름이 붙은 숫자 목록. 코드에서 3 같은 숫자만 쓰면 그게 뭔지 머리로 외워야 한다.
enum으로 이름을 붙이면 의미가 코드에 보인다.

enum class EWeekday {
    Monday,    // 0
    Tuesday,   // 1
    Wednesday, // 2
};

EWeekday Today = EWeekday::Wednesday;


값을 명시 안 하면 0부터 자동 증가.

enum class의 class는 이 enum의 이름들이 자기 영역 안에서만 유효하다는 뜻. EWeekday::Monday처럼 어느 enum의 Monday인지 명시해야 한다. 예전 C 스타일 enum은 이름 충돌이 잘 났는데 enum class는 그게 없다.

: uint8은 내부적으로 uint8(0~255)로 저장하라는 뜻. 안 쓰면 기본은 int(4바이트).
uint8이면 1바이트라 메모리 절약.

TArray

언리얼이 만든 가변 길이 배열. 같은 타입의 여러 개를 목록으로 들고 다닐 때 쓴다.

TArray<int32> Numbers;
Numbers.Add(10);
Numbers.Add(20);
Numbers[0];     // 10
Numbers.Num();  // 2


<int32>는 이 배열에 담을 타입을 지정. 컴파일러가 타입 안 맞으면 에러를 내준다.

비트플래그: 일반 enum 대신 1 << N으로 정의하는 이유

위험은 동시에 여러 개가 발생할 수 있다. 미끄러지면서 차선도 이탈하고 휘청거리는 식으로. 그런데 enum 변수 하나엔 값 하나만 담긴다. 그래서 각 위험에 비트 한 칸씩 배정해서 정수 한 칸 안에 동시 발생을 표현한다.

1 << 0  =  00000001  (= 1)
1 << 1  =  00000010  (= 2)
1 << 2  =  00000100  (= 4)
1 << 3  =  00001000  (= 8)


<<는 비트 왼쪽 시프트 연산자. 1 << 3은 1을 3칸 왼쪽으로 밀어서 8.

비트플래그를 안 쓰고 일반 enum이었다면 동시 발생 표현하려고 TArray을 따로 들고 다녀야 한다.
메모리 사용량 들쭉날쭉, 비교는 배열 순회 필요, 직렬화 복잡.

비트플래그는 int32 한 칸(4바이트)에 최대 32가지 위험 조합을 다 담는다.
위험이 1개든 7개든 메모리 동일. 비교도 한 줄.

비트 연산: OR로 켜고, AND로 확인

비트 켜기는 OR(|). 둘 중 하나라도 1이면 1.

ActiveFlags |= (int32)EHazardFlags::Skid;
ActiveFlags |= (int32)EHazardFlags::LaneDeparture;
// 결과: 00001001


비트 확인은 AND(&). 둘 다 1이어야 1.

   00001001    (ActiveFlags)
 & 00000001    (Skid 마스크)
 ─────────
   00000001    (Skid 자리만 통과)


Skid 값(00000001)이 가면 역할을 한다. 가면 구멍이 뚫린 자리(=1인 자리)는 비치고, 막힌 자리(=0인 자리)는 안 보인다. LaneDeparture가 켜져 있어도 그 자리는 가면에 막혀서 결과에 안 나타난다.
오직 Skid 자리만 통과. 이게 마스킹.

if ((ActiveFlags & (int32)EHazardFlags::Skid) != 0) {
    // Skid 켜져 있음
}


결과가 0이 아니면 해당 비트가 켜진 상태.

XOR(^)은 토글. 같으면 0, 다르면 1.
켜져 있으면 끄고, 꺼져 있으면 켜는 동작.

ENUM_CLASS_FLAGS 매크로가 하는 일

enum class는 타입이 엄격해서 비트 연산자를 직접 못 쓴다. 컴파일러가 에러를 낸다.

EHazardFlags A = EHazardFlags::Skid;
EHazardFlags B = EHazardFlags::LaneDeparture;
EHazardFlags C = A | B;  // 컴파일 에러

 

매크로 없으면 일일이 정수로 변환해야 한다.

EHazardFlags C = static_cast<EHazardFlags>(
    static_cast<int32>(A) | static_cast<int32>(B)
);


static_cast<타입>(값)은 타입 강제 변환.
EHazardFlags를 int32로 봐 → 비트 OR → 다시 EHazardFlags로 봐.

ENUM_CLASS_FLAGS(EHazardFlags) 한 줄을 쓰면 이 변환들을 자동 처리하는 |, &, ^ 연산자가 EHazardFlags 타입에 등록된다. 그러면 그냥 A | B 자연스럽게 쓸 수 있다.

포인터와 -> 화살표

언리얼에선 액터, 컴포넌트, 서브시스템 같은 무거운 객체를 포인터로 다룬다
객체를 통째로 복사하면 비싸니까 그 객체가 있는 주소만 들고 다닌다

점과 화살표의 차이는 원본이냐 포인터냐

FCar MyCar;            // 직접 가지고 있음
MyCar.Speed = 80;       // 점으로 접근

FCar* CarPointer = &MyCar;   // 포인터 (주소만 가진 종이쪽지)
CarPointer->Speed = 80;       // 화살표로 접근


->는 객체의 멤버나 함수에 접근하는 표시

AgentDataLogger->RecordHazard(Event);
// = AgentDataLogger가 가리키는 객체의 RecordHazard 함수를 Event 넘기면서 호출

 

언리얼 코드 전반에 ->가 정말 자주 나온다.

UENUM, USTRUCT, UPROPERTY: 언리얼 리플렉션 시스템

이 enum/struct/변수를 언리얼 리플렉션 시스템에 등록해달라는 뜻.
등록되면 블루프린트에서 변수 타입으로 쓸 수 있고, 에디터에서 표시되고, 직렬화도 된다.

UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
  • BlueprintType: 블루프린트에서 이 타입을 변수로 만들 수 있게.
  • Bitflags: 비트플래그 enum이라고 에디터에 알림. 체크박스 여러 개로 표시.
  • UseEnumValuesAsMaskValuesInEditor = "true": 1 << 0처럼 직접 명시한 값을 그대로 마스크로 쓰라는 뜻. 안 쓰면 0,1,2,3을 자동으로 1,2,4,8로 해석하던 옛 동작.
UPROPERTY(BlueprintReadOnly, meta = (Bitmask, BitmaskEnum = "EHazardFlags"))
int32 ActiveFlags = 0;


타입은 int32지만 에디터에서 보여줄 땐 EHazardFlags 체크박스 목록으로 표시.
디자이너가 어떤 위험이 활성화됐는지 체크박스로 볼 수 있다.

USTRUCT 위의 GENERATED_BODY()도 비슷한 매크로. 리플렉션에 필요한 코드를 자동 생성한다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam

방송 채널을 만드는 매크로. 어떤 일이 일어났을 때 호출될 함수들의 묶음을 정의한다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
    FOnHazardDetected, const FHazardEvent&, HazardEvent);


이름을 단어별로 뜯어보면:

  • DELEGATE: 위임자, 콜백 시스템.
  • MULTICAST: 여러 명이 동시에 들을 수 있음. 단일 채널은 듣는 사람이 하나뿐.
  • DYNAMIC: 블루프린트에서도 구독 가능. 약간 느리지만 호환성을 얻는다.
  • OneParam: 방송할 때 인자 1개를 같이 보냄. 여기선 FHazardEvent.

방송 쪽:

// 헤더에 채널 선언
UPROPERTY(BlueprintAssignable)
FOnHazardDetected OnHazardDetected;

// cpp에서 방송
FHazardEvent Event;
// ... 채워넣기
OnHazardDetected.Broadcast(Event);


구독 쪽:

HazardDetector->OnHazardDetected.AddDynamic(
    this, &UAgentDataLogger::OnHazardEventReceived);

UFUNCTION()  // Dynamic 델리게이트는 UFUNCTION이 필수
void OnHazardEventReceived(const FHazardEvent& HazardEvent) {
    // 받아서 처리
}

 

핵심 설계 포인트: 방송하는 쪽은 구독자의 존재를 모른다. 

델리게이트를 안 쓰고 직접 호출하는 방식이면 이렇게 된다.

// 결합도 높은 방식
AgentDataLogger->RecordHazard(Event);
UIWidget->ShowAlert(Event);
SoundManager->PlayWarning(Event);

 

방송하는 쪽이 듣는 쪽 모두를 직접 알아야 한다.
듣는 시스템이 추가될 때마다 방송하는 쪽 코드를 수정해야 한다. 결합도가 높아진다.

델리게이트는 이 결합을 끊는다. 한쪽이 변해도 다른 쪽이 안 흔들린다.

 

다음에 비슷한 패턴 만났을 때 참고용 코드

패턴 1: 동시 발생 가능한 상태들을 비트플래그로

UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EMyStateFlags : uint8
{
    None        = 0       UMETA(Hidden),
    StateA      = 1 << 0,
    StateB      = 1 << 1,
    StateC      = 1 << 2,
    StateD      = 1 << 3,
};
ENUM_CLASS_FLAGS(EMyStateFlags);

 

사용:

int32 ActiveStates = 0;
ActiveStates |= (int32)EMyStateFlags::StateA;  // 켜기

if ((ActiveStates & (int32)EMyStateFlags::StateA) != 0) {
    // StateA 활성화 상태
}

ActiveStates &= ~(int32)EMyStateFlags::StateA;  // 끄기 (NOT으로 비트 반전한 후 AND)

패턴 2: 이벤트 한 건을 통째로 전달하는 struct

USTRUCT(BlueprintType)
struct FMyEvent
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadOnly)
    double TimeStamp = 0.0;

    UPROPERTY(BlueprintReadOnly, meta = (Bitmask, BitmaskEnum = "EMyStateFlags"))
    int32 ActiveFlags = 0;

    UPROPERTY(BlueprintReadOnly)
    FVector WorldLocation = FVector::ZeroVector;

    // 측정값들...
};

패턴 3: 단계가 있는 이벤트(Enter/Sustain/Exit)

UENUM(BlueprintType)
enum class EMyEventPhase : uint8
{
    Enter   UMETA(DisplayName = "진입"),
    Sustain UMETA(DisplayName = "지속"),
    Exit    UMETA(DisplayName = "해제"),
};

패턴 4: 멀티캐스트 델리게이트로 이벤트 방송

선언(보통 헤더 상단 또는 별도 Types.h에):

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
    FOnMyEvent, const FMyEvent&, EventData);

 

방송하는 쪽 헤더:

UPROPERTY(BlueprintAssignable)
FOnMyEvent OnMyEvent;

 

방송하는 쪽 cpp:

FMyEvent EventData;
// 채워넣기
OnMyEvent.Broadcast(EventData);

 

구독하는 쪽 헤더:

UFUNCTION()
void HandleMyEvent(const FMyEvent& EventData);

 

구독하는 쪽 cpp(보통 BeginPlay나 초기화 시점):

if (SourceComponent)
{
    SourceComponent->OnMyEvent.AddDynamic(this, &UMyClass::HandleMyEvent);
}

 

해제(EndPlay나 소멸 시점):

if (SourceComponent)
{
    SourceComponent->OnMyEvent.RemoveDynamic(this, &UMyClass::HandleMyEvent);
}

패턴 변형 가이드

  • 인자 0개면 DECLARE_DYNAMIC_MULTICAST_DELEGATE, 2개면 _TwoParams, 3개면 _ThreeParams.
  • 블루프린트 호환 필요 없으면 DYNAMIC 빼고 DECLARE_MULTICAST_DELEGATE_OneParam. 더 빠르지만 BP에서 못 씀.
  • 듣는 쪽이 하나만이면 MULTICAST 빼고 DECLARE_DYNAMIC_DELEGATE_OneParam.

+ Recent posts