포스트

TIL 2026-05-08

TIL 2026-05-08

2026-05-08 NBC_Master 옵저버 패턴 + push vs pull + BlueprintNativeEvent + 빌드 트러블슈팅

목차


오늘 한 일 요약

  1. NBC_Master 옵저버 패턴 — ItemBase 자율 구독 — 캐릭터 사망 시 레벨에 배치된 모든 아이템 Destroy. UMYHealthComponent::OnHealthDead (Subject) ↔ 각 AItemBase 인스턴스(Observer)가 BeginPlay에서 AddDynamic으로 자기 자신 구독, 발화 시 Destroy() 호출
  2. push vs pull 차이 정리 — “Tick으로 HP 체크해야 하는 거 아닌가?”에 대한 답. Tick(Pull)은 60fps × 게임시간만큼 반복 검사, Delegate(Push)는 0번 검사 + 한 번 Broadcast. invocation list / AddDynamic vs AddRaw 부수 학습
  3. BlueprintNativeEvent 명명 규칙 — 4건 빌드 에러 일괄 수정Execute_* (외부 호출 헬퍼) / *_Implementation (C++ override 가상 함수) 두 자동 생성 함수 동작 이해. TestMyInterface.h typo 1건 + Item_Cloth.h/cpp 2건 + Item_Wood.cpp 1건 + MyTorchlight.cpp 1건
  4. 빌드 트러블슈팅 3건 — (a) AActor::Instigator shadowing 차단 → 파라미터명 DeadInstigator로 rename, (b) Figma2UMG 플러그인 미설치 → .uproject"Optional": true 추가 후 develop push (0ddc88a), (c) BaseWeapon 헤더 bIsOverHeat vs cpp bIsOverheated typo 통일 → 빌드 통과 후 push (d84e83c)
  5. SpawnEmitterAtLocation 핸들 보관 패턴 학습bAutoDestroy=true 단발 PFX는 OK, 루프 PFX는 누수. 멤버 UParticleSystemComponent* 보관 vs SpawnEmitterAttached 사용 두 패턴 비교
  6. CS 19번 보강 3건 — 모의면접 답변에 “cache cold(캐시 콜드)” 영문 용어 추가, “레지스터 컨텍스트” 1줄을 PC/SP/범용 레지스터/컨텍스트 4줄로 분리, 꼬리질문 Q13~Q19 7개 추가(반복자 무효화 / 스택 오버플로 / race condition / IPC / Windows PE 이미지 / PCB·TCB)
  7. CS 20번 신규 — Stack Overflowraw/cs-notion/20_stack_overflow.md 신규 작성 (~6500단어 / 14섹션). 발생 원인 4가지 + 해결책 5가지 + 플랫폼별 스택 크기 + 언리얼 영역 + 디버깅 + 꼬리질문 10개
  8. NBC_Master 구현계획 작성scrum/NBC_Master_구현계획.md 7 Step + 승인 게이트 3개. Sandbox 패턴 채택, SandboxWeaponBase 버그 2건 명시, UTF-8 with BOM 인코딩 정책. 사용자 승인 대기 중
  9. 05/07 TIL 보강 작성5월/2026-05-07.md 신규 작성(~4300단어 / 880줄). NBC_Ch3 Public/Private 분리 + 팀원 PR 워크플로우 전수
  10. 다음 마스터 과제 — 스크럼 특이사항 반영scrum/2026-05-08.mdOnHealthDamaged(UI HP바) / OnHealthDead(사망 모션) 필수 + 퀘스트 시스템(100마리 처치 파티 공유) 도전 항목 추가


NBC_Master — 옵저버 패턴 (ItemBase 자율 구독)

요구사항 — 캐릭터가 데미지를 받아 사망할 때 레벨에 배치된 모든 AItemBase 자식을 일괄 Destroy.

구조 — UMYHealthComponent::OnHealthDead 가 Subject, 각 AItemBase 인스턴스가 Observer. ItemBase가 BeginPlay에서 플레이어의 HealthComponent를 찾아 자기 자신을 구독시키고, Broadcast 시 Destroy() 호출.

1
2
3
4
5
6
7
8
9
10
11
// AItemBase.h
UCLASS()
class AItemBase : public AActor
{
    GENERATED_BODY()
public:
    virtual void BeginPlay() override;

    UFUNCTION()
    void OnPlayerDead(AController* DeadInstigator);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// AItemBase.cpp
void AItemBase::BeginPlay()
{
    Super::BeginPlay();

    if (APawn* Player = UGameplayStatics::GetPlayerPawn(this, 0))
    {
        if (auto* Health = Player->FindComponentByClass<UMYHealthComponent>())
        {
            Health->OnHealthDead.AddDynamic(this, &AItemBase::OnPlayerDead);
        }
    }
}

void AItemBase::OnPlayerDead(AController* /*DeadInstigator*/)
{
    Destroy();
}

UMYHealthComponent 쪽은 OnHealthDead.Broadcast(GetOwnerController()) 만 호출하고 ItemBase에 대해 모름. 의존 방향은 ItemBase → HealthComponent 한 방향.

레벨에 배치된 ItemBase 100개가 있으면 100개가 각자 BeginPlay에서 구독 → Broadcast 1회로 100개가 각자 자기 자신 Destroy. 외부에서 일괄 처리하는 코드가 별도로 필요 없음.



push vs pull — Tick 검사 안 쓰고 델리게이트로 처리하는 이유

오늘 핵심 질문 — “Tick으로 매 프레임 HP 체크해야 하는 거 아닌가? 델리게이트라 상관없나?”

답이 push vs pull 차이의 핵심.

“Tick으로 매 프레임 HP 체크해야 하는 거 아닌가?”

직관적으로는 그래 보인다. “사망 시점을 알려면 매 프레임 HP <= 0인지 봐야 하지 않나” 같은 생각. 하지만 그게 pull 방식이고, 델리게이트는 push 방식이라 검사가 0번이다.

Pull (Tick) vs Push (Delegate) 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Pull (Tick) — 매 프레임 직접 묻기
void AItemBase::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (APawn* Player = UGameplayStatics::GetPlayerPawn(this, 0))
    {
        if (auto* Health = Player->FindComponentByClass<UMYHealthComponent>())
        {
            if (Health->GetCurrentHealth() <= 0.f)   // 매 프레임 if
            {
                Destroy();
            }
        }
    }
}

// Push (Delegate) — 등록만, 호출은 발신자가
void AItemBase::BeginPlay()
{
    /* ... 한 번 등록 ... */
    Health->OnHealthDead.AddDynamic(this, &AItemBase::OnPlayerDead);
}

void AItemBase::OnPlayerDead(AController*) { Destroy(); }   // 한 번 호출됨
  • Pull (Tick) — 60fps × 게임시간(가령 600초) = 36,000번 if 검사. 36,000번 모두 false. 사망 1번을 잡기 위해 35,999번 헛수고.
  • Push (Delegate)AddDynamic으로 (객체*, 함수포인터) 한 번 등록만 하고 끝. 사망 시점에 HealthComp가 Broadcast를 1번 호출하면 등록된 모든 객체의 함수가 한 번씩 자동 호출. Tick 검사 0번.

비유 — Pull은 “택배 왔어?” 매 1초마다 문 여는 행동, Push는 “오면 벨 누르세요”라고 한 번 적어두는 것. 사망처럼 언제 일어날지 모르고 한 번만 일어나는 이벤트는 누가 봐도 후자가 정답.

내부 동작 — invocation list

AddDynamic이 한 일은 (객체 포인터, 함수 포인터) 쌍을 HealthComp의 invocation list에 추가한 것.

1
2
3
4
5
HealthComp::OnHealthDead.invocation_list
  ├─ (Item_001*, &AItemBase::OnPlayerDead)
  ├─ (Item_002*, &AItemBase::OnPlayerDead)
  ├─ (Character*, &ANBC_MasterCharacter::OnPlayerDead)
  └─ ...

Broadcast가 하는 일 — invocation list를 순회하며 target->Function(args) 한 번씩 호출. 그게 끝. 추상적이지만 실제 메모리에는 TArray<FScriptDelegate> 같은 배열이 들어 있고, Broadcast는 for 루프 한 번이다.

언제 Tick / 언제 Delegate

상황
매 프레임 위치/회전 갱신Tick (값이 매 프레임 바뀜)
매 프레임 거리 검사 후 액션Tick 또는 Timer (주기성)
이벤트성 1회 변화 (사망·도착·획득)Delegate
외부 시스템에 변화 알림Delegate
키 입력InputAction (델리게이트의 일종)

규칙 — “값이 연속적으로 변하면 Tick, 이산 사건이면 Delegate.”

AddDynamic vs AddRaw — UE 델리게이트의 두 부류

UE 델리게이트는 두 부류.

  • Dynamic (UFUNCTION 기반)AddDynamic / AddUnique / AddDynamicLambda. UPROPERTY/UFUNCTION 시스템 위에 동작 → 약한 참조처럼 GC 안전. 등록한 객체가 GC로 사라져도 Broadcast 시점에 안전 처리. 직렬화 가능, BP에 노출 가능. 단 UFUNCTION() 필수.
  • Raw (AddRaw / AddSP / AddLambda) — 일반 C++ 함수 포인터 기반. 빠르지만 객체가 사라져도 invocation list에 남아있음 → dangling pointer로 크래시 위험. RemoveAll(this)로 명시적 해제 필요. UObject가 아닌 일반 클래스에서 쓸 때만.

UE 게임플레이 코드에서는 AddDynamic이 기본값. 라이프사이클 안전성이 더 중요하기 때문.



BlueprintNativeEvent 명명 규칙 — _Implementation / Execute_

오늘 NBC_Master에서 미완성 코드 4건이 빌드 안 되던 원인 — BlueprintNativeEvent UFUNCTION을 잘못 override / 호출했기 때문. UHT 자동 생성 함수 두 개의 역할을 정확히 알아야 한다.

UHT가 자동 생성하는 두 가지

1
2
3
4
5
6
7
8
9
10
11
// 인터페이스 헤더
UINTERFACE(MinimalAPI, Blueprintable)
class UTestMyInterface : public UInterface { GENERATED_BODY() };

class ITestMyInterface
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
    void OnFireDetected(float Temp, FVector Loc);
};

BlueprintNativeEvent로 마킹된 순간 UHT는 두 함수를 자동으로 만든다.

  1. Execute_OnFireDetected(UObject* Target, float Temp, FVector Loc) — C++에서 호출할 때 쓰는 정적 헬퍼. 내부에서 “BP override가 있으면 BP 함수 / 없으면 C++ _Implementation“을 분기 디스패치.
  2. OnFireDetected_Implementation(float Temp, FVector Loc) — C++에서 override할 가상 함수. 자식 클래스에서 이 이름으로 override해야 됨.
1
2
3
4
5
6
7
8
9
// 자식 클래스에서 override할 때 — _Implementation 붙임
class AItem_Cloth : public AActor, public ITestMyInterface
{
public:
    virtual void OnFireDetected_Implementation(float Temp, FVector Loc) override;
};

// 외부에서 호출할 때 — Execute_ 헬퍼 사용 (직접 OnFireDetected() 호출 금지)
ITestMyInterface::Execute_OnFireDetected(TargetActor, 100.f, FVector::ZeroVector);

4건 빌드 에러 일괄 수정

수정한 5개 파일.

파일잘못된 형태고친 형태
TestMyInterface.hExcute_OnFireDetected (typo)OnFireDetected
Item_Cloth.hOnFireDetected overrideOnFireDetected_Implementation override
Item_Cloth.cppvoid AItem_Cloth::OnFireDetected(...)void AItem_Cloth::OnFireDetected_Implementation(...)
Item_Wood.cppvoid AItem_Wood::OnFireDetected(...)void AItem_Wood::OnFireDetected_Implementation(...)
MyTorchlight.cppExcute_OnFireDetected(...) 호출Execute_OnFireDetected(...)

TestMyInterface.hExcute_ (Excute) typo 1건은 인터페이스 선언 자체에 함수 이름을 잘못 적은 것이라, 그 함수에 의존하던 모든 자식 클래스가 줄줄이 깨졌다. 인터페이스 헤더는 한 글자만 틀려도 폭탄.

왜 이렇게 만들어두었나 — BP override 디스패치

답 — BP에서 override했을 가능성 때문. C++ 클래스에서 OnFireDetected_Implementation을 만들어둬도, 그 클래스를 BP로 상속받아서 BP 그래프에 같은 이름 함수를 새로 짜면, 런타임에는 BP 버전이 우선이어야 한다.

Execute_ 헬퍼가 그 분기를 자동 처리:

1
2
3
4
5
6
7
// 의사코드
ITestMyInterface::Execute_OnFireDetected(Target, Temp, Loc)
{
    if (BP override 함수가 존재) Target->ProcessEvent(BP함수, Args);   // BP 호출
    else                          static_cast<ITestMyInterface*>(Target)
                                   ->OnFireDetected_Implementation(Temp, Loc);   // C++ 호출
}

C++에서 직접 OnFireDetected(...) 호출하는 코드가 컴파일 안 되도록 막아둔 것도 이 분기 보호용. 무조건 Execute_ 헬퍼를 거치게 강제.



오늘의 트러블슈팅

4-1. AActor::Instigator shadowing

1
2
ItemBase.h(29): Error : Function parameter: 'Instigator' cannot be defined in 'OnPlayerDead'
                       as it is already defined in scope 'AActor' (shadowing is not allowed)

원인 — AActor가 protected TObjectPtr<APawn> Instigator 멤버를 보유. 자식 클래스에서 UFUNCTION 파라미터 이름을 같게 적으면 UHT가 shadowing으로 빌드 차단.

1
2
3
4
5
6
7
// 차단
UFUNCTION()
void OnPlayerDead(AController* Instigator);   // ← 부모의 멤버명과 충돌

// 통과
UFUNCTION()
void OnPlayerDead(AController* DeadInstigator);

해결 — 파라미터명 InstigatorDeadInstigator로 rename. 한 글자 prefix만 붙여도 OK.

같은 함정 회피 팁 — UFUNCTION 파라미터에서 다음 이름은 피한다:

피할 이름출처
InstigatorAActor::Instigator
OwnerUObject::GetOuter() 별칭 / AActor::Owner
Role / RemoteRoleAActor::Role (네트워킹)
RootComponentAActor::RootComponent
TagsAActor::Tags

또는 In* prefix 컨벤션 (InInstigator, InOwner)을 써서 일관되게 회피 가능. UE 엔진 코드 자체도 이 패턴을 자주 쓴다.

4-2. Figma2UMG 플러그인 미설치 — "Optional": true

NBC_Ch3 develop 브랜치 빌드에서:

1
2
Unable to find plugin 'Figma2UMG' (referenced via NBC_Ch3_TeamProject.uproject).
Error MSB3073 : ... 종료(코드: 6)

상황 — .uproject에 enabled로 등록만 되고 실제 설치는 안 된 상태. E-PR-03(Figma → UMG 변환) 미래 작업용으로 표시만 해둠. 팀원이 빌드하면 일괄 실패.

해결 — UE5.5의 "Optional": true 필드 사용.

1
2
3
4
5
6
7
8
9
{
    "Plugins": [
        {
            "Name": "Figma2UMG",
            "Enabled": true,
            "Optional": true
        }
    ]
}

Optional: true면 플러그인이 설치돼 있으면 로드, 없으면 조용히 스킵. 빌드 통과. 보유자는 그대로 사용 가능.

다른 옵션 비교:

옵션동작단점
Enabled: false보유자도 사용 못 함
항목 자체 제거향후 다시 추가할 때 노이즈
Optional: true있으면 로드, 없으면 스킵없음 (UE5.5+ 한정)

Optional이 양쪽(보유자·미보유자) 다 빌드 통과시키는 유일한 옵션. 적용 결과 develop에 chore: Figma2UMG 플러그인 Optional 처리 커밋 push (0ddc88a).

4-3. BaseWeapon bIsOverheated vs bIsOverHeat typo

1
2
BaseWeapon.cpp(96): Error C2065 : 'bIsOverheated': 선언되지 않은 식별자
BaseWeapon.cpp(124): Error C2065 : 'bIsOverheated': 선언되지 않은 식별자

원인 — 헤더와 cpp의 변수명 불일치.

1
2
3
4
5
6
// BaseWeapon.h (54행)
bool bIsOverHeat;        // ← 헤더는 'OverHeat'

// BaseWeapon.cpp (96, 124행)
bIsOverheated = true;    // ← cpp는 'Overheated'
bIsOverheated = false;

OverHeat vs Overheated — 사람 눈으로는 같은 의미지만 컴파일러는 다른 식별자로 본다.

해결 — replace_all로 cpp의 bIsOverheated를 헤더 이름 bIsOverHeat로 통일. Reload 과열 진입(96행)과 OnOverHeatEnd 해제(124행) 두 군데 모두 수정. 빌드 통과 후 push (d84e83c).

교훈 — 변수명은 영어 단어 분리(OverHeat vs Overheated)에서 갈리기 쉽다. IDE의 rename refactor를 쓰거나, 헤더에서 변수명을 바꿀 때 cpp도 같이 바꾸는 습관 필요. typo 한 번에 빌드 전체가 막힘.



SpawnEmitterAtLocation 핸들 보관 패턴

Item_Cloth/Wood의 OnFireDetected_Implementation에서 UGameplayStatics::SpawnEmitterAtLocation 반환값을 무시하던 코드 검토 중 학습.

반환값 무시의 함정

1
2
3
4
5
void AItem_Cloth::OnFireDetected_Implementation(float Temp, FVector Loc)
{
    UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), FireEffect, Loc);
    // 반환된 UParticleSystemComponent*를 받지 않음
}

이게 위험한 이유 — SpawnEmitterAtLocation이 만드는 건 월드에 독립으로 부유하는 컴포넌트. 액터의 자식이 아니라서 액터가 Destroy돼도 같이 안 죽는다.

PFX 종류bAutoDestroy 기본값반환값 무시 시
단발 (시퀀스 끝나는 폭발 등)true시퀀스 끝나면 자동 사라짐 → OK
루프 (지속 화염 등)true (자동 정리는 시퀀스 끝 기준이라 루프는 끝이 없음)영원히 떠있음 → 누수

단발이면 운 좋게 자동 정리, 루프면 메모리·파티클 시스템에 잔존. 동일한 코드가 PFX 종류에 따라 동작이 갈리니 반환값을 받아두는 게 안전.

안전 패턴 두 가지 — 멤버 보관 vs Attached

A. 멤버 보관 + EndPlay 정리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AItem_Cloth.h
UPROPERTY()
TObjectPtr<UParticleSystemComponent> FireEffectComp;

// .cpp
void AItem_Cloth::OnFireDetected_Implementation(float Temp, FVector Loc)
{
    FireEffectComp = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), FireEffect, Loc);
}

void AItem_Cloth::EndPlay(const EEndPlayReason::Type Reason)
{
    if (FireEffectComp) FireEffectComp->DestroyComponent();
    Super::EndPlay(Reason);
}

명시적이고 책에서 권장. 단점 — 코드량 증가.

B. SpawnEmitterAttached로 변경

1
2
3
4
5
6
7
8
9
10
void AItem_Cloth::OnFireDetected_Implementation(float Temp, FVector Loc)
{
    UGameplayStatics::SpawnEmitterAttached(
        FireEffect,
        GetRootComponent(),
        NAME_None,
        Loc,
        FRotator::ZeroRotator,
        EAttachLocation::KeepWorldPosition);
}

루트 컴포넌트의 자식으로 attach → 액터 Destroy 시 자동으로 함께 정리. 한 줄 변경으로 끝.

비교 — 책에서 권장은 A지만, 실무는 B가 한 줄로 끝나서 더 흔하다. 아이템에 붙어다니는 화염이라면 B가 의미적으로도 자연스러움.



CS 19번 보강 (3건)

모의면접 답변에 영문 용어 보강

이전엔 “캐시도 차갑습니다” 한국어만 있었는데 — 영문 용어 모르는 채 면접 답변하면 꼬리질문 들어올 때 막힘. "cache cold"(캐시 콜드) 영문 용어 + 의미 풀이 추가.

“…이 과정에서 새로 스케줄된 프로세스의 메모리 페이지·캐시 라인은 코어 캐시에 없는 상태 — cache cold(캐시 콜드)라고 하며, 이후 첫 메모리 접근들이 모두 cache miss → DRAM fetch로 이어지면서 추가 지연이 발생합니다…”

“레지스터 컨텍스트” 1줄 → 4줄 분리

핵심 개념 표에서 “레지스터 컨텍스트”를 1줄로 압축해뒀는데 — 면접에서 “레지스터 컨텍스트가 정확히 뭐가 들어가요?” 질문 받으면 답이 흐려지는 문제. 4줄로 분리:

용어설명
PC (Program Counter)다음 실행할 명령어 주소. 컨텍스트 스위칭 시 가장 먼저 저장
SP (Stack Pointer)현재 스레드의 스택 최상단. 호출 프레임 위치
범용 레지스터x86-64 기준 rax·rbx·rcx·rdx·rsi·rdi·r8~r15
레지스터 컨텍스트PC + SP + 범용 레지스터 + 플래그 등의 묶음. PCB/TCB에 통째로 저장

꼬리질문 Q13~Q19 (7개) 추가

기존 Q1~Q12 외에 새 꼬리질문 7개:

Q주제핵심
Q13반복자 무효화vector 재할당 시 전체 무효 / list 노드 단위 / map 트리 회전 / unordered_map rehash 시 전체
Q14스택 오버플로 발생 조건스택 크기 초과(Win 1MB / Linux 8MB), 무한 재귀, 거대 지역 변수
Q15재귀 → 스택 오버플로피보나치 naive O(2^n) / 메모이제이션 O(n) / TCO(꼬리 재귀 최적화)
Q16race condition + mutex vs spin lock둘 다 상호 배제. mutex = sleeping(짧으면 비효율), spin = busy-wait(짧을 때 유리)
Q17IPC 정의 / 6가지 종류파이프·메시지큐·공유메모리·세마포어·소켓·시그널. 격리·속도 트레이드오프
Q18Windows 프로세스 이미지PE 포맷 / Image Name·Path·Base / 같은 exe로 다중 프로세스 가능
Q19PCB와 컨텍스트 스위칭냉장고 비유(요리 도중 다른 요리로 → 도마 위 정리 후 메모) / 4단계 흐름 / PCB(프로세스) vs TCB(스레드) 크기 차이

Q15에서 메모이제이션과 TCO를 같이 다룬 게 효과적. “재귀가 항상 위험한가?”라는 자연 질문에 “꼬리 재귀 + 컴파일러 최적화면 반복문과 동등”이라는 답을 같이 박아둠.



CS 20번 신규 — Stack Overflow

raw/cs-notion/20_stack_overflow.md 신규 생성. ~6500단어, 14개 섹션.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.  스택 오버플로란
2.  발생 원인 4가지
    2-1. 무한 재귀 (base case 누락)
    2-2. 너무 깊은 재귀 (피보나치 등)
    2-3. 거대 지역 변수 (큰 배열을 함수 안에)
    2-4. 무한 상호 호출 (A→B→A→...)
3.  해결책 5가지
    3-1. base case 명시
    3-2. 반복문(loop) 변환
    3-3. 메모이제이션 / DP
    3-4. TCO (꼬리 재귀 최적화)
    3-5. 명시적 std::stack 자료구조 사용
4.  플랫폼별 스택 크기
    Windows 1MB / Linux 8MB / macOS 8MB
    스레드 vs 메인 스레드 차이
5.  언리얼 영역
    FRunnableThread::Create 스택 인자
    BehaviorTree 깊이 제한
    Tick 안 재귀 호출 절대 금지
6.  디버깅
    Windows 0xC00000FD (STATUS_STACK_OVERFLOW)
    Linux SIGSEGV
    Visual Studio 콜스택 분석
7~14. 꼬리질문 10개

19번 Q14·Q15 꼬리질문에서 다룬 내용을 정식 노트로 확장. 언리얼 영역 섹션이 핵심:

  • FRunnableThread::Create(Runnable, Name, StackSize, ...) — 스택 크기 명시 가능
  • BehaviorTree 깊이가 너무 깊으면 평가 중 스택 폭발
  • Tick은 매 프레임 호출되니 그 안에서 재귀 호출하면 한 프레임 안에 폭발


NBC_Master 구현계획 작성 (plan-agent)

scrum/NBC_Master_구현계획.md 신규. 7 Step + 승인 게이트 3개 구조.

1
2
3
4
5
6
7
8
9
Step 1. Sandbox 패턴 채택 결정 (이미 80% 진행)
Step 2. SandboxWeaponBase 버그 2건 수정
        - UpdateAmmo 버그: '=' → '-=' (탄약 차감)
        - LinetraceOneShot: ApplyDamage 추가 (라인트레이스 결과에 데미지 누락)
Step 3. SandboxRifle 설계 (단발 + 자동 연사)
Step 4. SandboxShotgun 설계 (펠릿 산탄 — VRandCone)
Step 5. NBC_MasterCharacter ADS 이식 (VoidUnreal 코드 활용) ← 도전
Step 6. UTF-8 with BOM 인코딩 정책 (NBC_Ch3 mojibake 사고 재발 방지)
Step 7. 통합 테스트

승인 게이트 — Step 2 시작 전 / Step 5 시작 전 / Step 7 시작 전. 사용자 승인 받고 진행.

UTF-8 with BOM 정책이 별도 Step인 이유 — Ch3에서 한글 주석 깨짐(mojibake) 사고를 한 번 겪었음. NBC_Master는 처음부터 BOM 강제로 시작.

진행은 사용자 승인 대기 중.



05/07 TIL 보강 작성

어제(05/07) TIL이 미작성 상태였음 — 오늘 회고로 보강.

5월/2026-05-07.md 신규 작성. ~4300단어 / 880줄.

핵심 내용 두 갈래:

  1. NBC_Ch3 Public/Private 모듈 분리 — 27 파일 git mv (헤더 13 / cpp 12 / 모듈.h+cpp / OWNER.txt 6) + include 풀 경로 갱신 19파일
  2. 팀원 PR 워크플로우 전수 — 브랜치 전략 / 작업 흐름 8단계 / Conventional Commits / PR 본문 양식 / 자주 하는 실수 4가지 / GUI 도구

세부는 별도 파일 참조.



다음 마스터 과제 — 스크럼 특이사항 반영

오늘 만든 UMYHealthComponent 델리게이트를 활용한 다음 과제 항목을 scrum/2026-05-08.md 특이사항 섹션에 추가.

우선순위항목활용 델리게이트
필수 1UI HP 바 갱신OnHealthDamaged.Broadcast(CurrentHealth, MaxHealth, FinalDamage) 시그니처를 위젯이 구독
필수 2실제 사망 모션·연출OnHealthDead 구독 → Ragdoll 켜기 / DeathMontage 재생 / Disable Input
도전퀘스트 시스템 (100마리 처치 + 파티 공유)옵저버 패턴 동일 구조 — 몬스터 사망 델리게이트 → 퀘스트 컴포넌트 카운트 증가 → 파티 broadcast

도전 항목 설계 메모 — 100마리 처치 카운트 자체는 단순 +1이지만, 파티 공유 달성도는 멀티플레이어 RPC가 필요함. 4차 리팩터링에서 배운 “Subject가 알 필요 없는 영역에 의존하지 않게 한다”를 그대로 적용 — 퀘스트 컴포넌트가 “몬스터 사망”을 알아야지 몬스터가 “퀘스트”를 알면 안 됨.



오늘 배운 것 정리

  1. 옵저버 패턴 — 자기 자신 라이프사이클은 자기가 구독한다 — Subject(OnHealthDead)는 broadcast만, 책임은 각 Observer 인스턴스(AItemBase)가 가져감. 의존 방향은 ItemBase → HealthComponent 한 방향, 그 반대 절대 금지
  2. push vs pull 구분은 “값이 연속이냐 이산이냐” — 매 프레임 변하는 위치는 Tick(pull), 한 번 일어나는 사망 이벤트는 Delegate(push). 60fps × 게임시간만큼의 헛수고 검사를 0번으로 만드는 핵심
  3. AddDynamic이 한 일은 (객체, 함수) 쌍을 invocation list에 추가한 것뿐 — Broadcast는 그 리스트를 for 루프 한 번. 추상적이지만 메모리에는 단순한 배열. 안전성은 UFUNCTION/UPROPERTY가 GC와 협조해서 챙김
  4. BlueprintNativeEvent_Implementation override + Execute_ 호출이 강제된다 — BP override 디스패치 자동화의 대가. 직접 함수명 호출은 컴파일 차단. 인터페이스 헤더에 typo 한 글자가 자식 클래스 전체를 깨뜨림
  5. shadowing 차단은 부모 멤버명을 외워서 피한다Instigator/Owner/Role/RootComponent/Tags. 또는 In* prefix 컨벤션으로 일관 회피
  6. .uproject"Optional": true는 미설치 플러그인이 빌드를 막지 않게 한다 — UE5.5+ 한정. 보유한 팀원은 그대로 로드, 없는 팀원은 조용히 스킵. Enabled false / 항목 제거 / Optional 셋 중 양쪽 다 만족시키는 건 Optional 뿐
  7. SpawnEmitterAtLocation은 “월드 부유 컴포넌트”라 액터와 라이프사이클 분리 — 단발은 자동 정리되지만 루프는 누수. 멤버 보관 vs Attached 두 패턴 중 실무는 Attached가 한 줄 끝
  8. bOverHeat vs bOverheated 같은 영어 단어 분리 typo는 빌드 전체를 막는다 — IDE rename refactor 또는 헤더-cpp 동시 수정 습관 필요


내일 할 일

  • ⬜ Ch3 팀플 — C-AL-01 히트스톱 마무리 (CustomTimeDilation 0.05~0.1s)
  • ⬜ Ch3 팀플 — C-AL-02 카메라 셰이크 두 번째(RangedCameraShake) + 무기 분기 통합
  • ⬜ NBC_Master — 구현계획 Step 2 (SandboxWeaponBase 버그 2건 수정) — 사용자 승인 시 진입
  • ⬜ NBC_Master — OnHealthDamaged 구독 위젯으로 HP 바 갱신 (필수 1)
  • ⬜ CS 21번 진입 결정 — 19/20번 다음 토픽 (deadlock·세마포어·뮤텍스 심화 등 후보)
  • ⬜ 노션 동기화 — Ch3 AL 카테고리 진행률 갱신
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.