Sparta Unreal 부트캠프 120일차

정찬호·2026년 5월 22일

코딩 테스트 연습


프로그래머스 - 테이블 해시 함수

이전에 푼 적 있는 문제입니다.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

struct Data
{
    vector<int> data;
    int offset;
    bool operator<(const Data &d)
    {
        if(this->data[offset]<d.data[offset])
            return true;
        else if(this->data[offset]==d.data[offset])
        {
            if(this->data[0]>d.data[0])
                return true;
            else
                return false;
        }
        return false;
    }
};

bool comp(Data &a, Data &b)
{
    if(a<b)
        return false;
    return true;
}

int solution(vector<vector<int>> data, int col, int row_begin, int row_end) {
    int answer = 0;
    vector<Data> datas;
    for(int i=0;i<data.size();i++)
    {
        Data d;
        d.data=data[i];
        d.offset=col-1;
        datas.push_back(d);
    }
    sort(datas.begin(),datas.end());
    for(int i=row_begin-1;i<=row_end-1;i++)
    {
        Data d=datas[i];
        int num=0;
        for(int j=0;j<d.data.size();j++)
        {
            num+= d.data[j]%(i+1);
        }
        answer^=num;
    }
    return answer;
}

최대한 코드를 보지 않기 위해 조심해야겠네요.

DataTable이 들어오고 data 내부의 모든 속성은 int형입니다.
키 값의 중복이 없다는 것이 보장됩니다.

정렬함수는 입력받은 col을 인덱스 삼아 col번째 컴럼 값을 기준으로 오름차순 정렬
col번째 컬럼의 값이 같다 => 키 값으로 내림차순 정렬

정렬된 데이터에서 S_i를 i 번째 행의 튜플에 대해 각 컬럼의 값을 i 로 나눈 나머지들의 합으로 정의합니다
=> 정렬된 순서가 i이고 data의 모든 컬럼 값을 i로 나눈 나머지의 합을 S로 정의한다.

row_begin ~ row_end의 모든 S를 bitwise XOR한 값을 해시로 삼아서 반환하면 끝.

bitset 사용하는게 좋으려나? 값의 범위가 1000000까지라 좀 핵갈리네요.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int staticCol;
bool compare(vector<int>& first, vector<int>& second)
{
    return first[staticCol] == second[staticCol] ? first[0] > second[staticCol]
        : first[staticCol] < second[staticCol];
}

int solution(vector<vector<int>> data, int col, int row_begin, int row_end) {
    int answer = 0;
    staticCol = col;
    
    sort(data.begin(), data.end(), compare);
    
    return answer;
}

sort 까지는 완료? 전역변수를 만드는 것이 잘한 짓은 아닌 것 같은데 지금은 더 나은 방법이 생각 안나니 일단 패스.
생각해보니 bitset을 사용하면 xor 사용하는 순간 0이던 부분들이 1이 될 가능성이 엄청 높네요. 굳이 사용할 필요 없이 ^ 연산자를 쓰면 되는 거였어요.

1차 시도 코드

#include <string>
#include <vector>
#include <algorithm>
#include <bitset>
using namespace std;

int staticCol;
bool compare(vector<int>& first, vector<int>& second)
{
    return first[staticCol] == second[staticCol] ? first[0] > second[staticCol]
        : first[staticCol] < second[staticCol];
}

int solution(vector<vector<int>> data, int col, int row_begin, int row_end) {
    int answer = -1;
    staticCol = col;
    
    sort(data.begin(), data.end(), compare);
    
    for(int i = row_begin; i <= row_end; i++)
    {
        int S = 0;
        for(int num : data[i])
        {
            S += (num % data[i][col]);
        }
        
        if(answer == -1)
        {
            answer = S;
        }
        else
        {
            answer ^= S;
        }
    }
    return answer;
}

입구컷입니다.
입력값 : [[2, 2, 6], [1, 5, 10], [4, 2, 9], [3, 8, 3]], 2, 2, 3
기대값 : 4
결과값 : 0

2차 입구컷 코드입니다.

#include <string>
#include <vector>
#include <algorithm>
#include <bitset>
using namespace std;

int staticCol;
bool compare(vector<int>& first, vector<int>& second)
{
    return first[staticCol] == second[staticCol] ? first[0] > second[staticCol]
        : first[staticCol] < second[staticCol];
}

int solution(vector<vector<int>> data, int col, int row_begin, int row_end) {
    int answer = -1;
    staticCol = col - 1;
    
    sort(data.begin(), data.end(), compare);
    
    for(int i = row_begin; i <= row_end; i++)
    {
        int S = 0;
        for(int num : data[i])
        {
            S += (num % data[i][col - 1]);
        }
        
        answer = answer == -1 ? S : answer ^ S;
    }
    return answer;
}

col이 0부터가 아니라 1부텅였네요. 그래서 -1씩 해주었습니다.

실행결과 : 7


오늘 자 마지막 시도 코드

#include <string>
#include <vector>
#include <algorithm>
#include <bitset>
using namespace std;

int staticCol;
bool compare(vector<int>& first, vector<int>& second)
{
    return first[staticCol] == second[staticCol] ? first[0] > second[0]
        : first[staticCol] < second[staticCol];
}

int solution(vector<vector<int>> data, int col, int row_begin, int row_end) {
    int answer = -1;
    staticCol = col - 1;
    
    sort(data.begin(), data.end(), compare);
    
    for(int i = row_begin; i <= row_end; i++)
    {
        int S = 0;
        for(int num : data[i])
        {
            S += (num % (i + 1));
        }
        
        answer = answer == -1 ? S : answer ^ S;
    }
    return answer;
}

실행 결과 : 2로 입구컷입니다.


[TIL] Enemy 전투 시스템 — GAS 연동, DataTable 초기화, 컴포넌트 설계

Today I Learned | Unreal Engine 5 | GAS | DataTable | StateTree | C++


1부. GAS HandleGameplayEvent — EventData 설계 함정

HandleGameplayEvent란?

ASC->HandleGameplayEvent(Tag, &EventData)는 해당 Tag를 Listen하는 GA를
K2_ActivateAbilityFromEvent로 트리거한다.
TryActivateAbilityByClass와 달리 EventData를 함께 전달하므로
GA Blueprint에서 이벤트 데이터를 직접 활용할 수 있다.

EventData.Instigator 미설정 함정

// ❌ Instigator 미설정
FGameplayEventData EventData;
EventData.EventTag = RetrieveGameplayTags::GameplayEvent_Enemy_Attack;
EventData.Target   = Target;
// EventData.Instigator → 기본값 nullptr

// ✅ 수정
EventData.Instigator = GetOwner();

GA Blueprint에서 Instigator를 Cast할 때 nullptr이면:

K2_ActivateAbilityFromEvent
  → Cast(EventData.Instigator → Pawn)  ← nullptr → CastFailed
  → CastFailed 핀 미연결 → 실행 중단
  → AIMoveTo, PlayMontageAndWait 미실행

에러 로그 없이 GA가 조용히 중단된다. 디버깅이 매우 어렵다.

방어 패턴

  • GA Blueprint에서 EventData 필드를 Cast할 때는 CastFailed 핀에 반드시 EndAbility 연결
  • C++에서 EventData 구성 시 Instigator / Target / EventTag 3개를 체크리스트로 확인

2부. DataTable 기반 컴포넌트 초기화 — 이중 간접 참조 구조

초기화 흐름

BP Class Defaults
  └─ MonsterDataRowName = "Zombie_Basic"  ← 여기가 None이면 전체 실패
       └─ MonsterDataTable->FindRow(MonsterDataRowName)
            └─ Row->PatternSlots (패턴 Row 이름 목록)
                 └─ EnemyCombatComponent->Initialize(PatternTable, PatternSlots)
  • MonsterDataRow: 몬스터 메타 (PatternSlots, DropRow 등 키 묶음)
  • MonsterPatternRow: 실제 패턴 데이터 (ActivationRange, Cooldown, Hitbox 등)

실제 발생한 버그

BP_Test_EnemyCharacter의 Class Defaults에서 MonsterDataRowName = None.

void ARetrieveEnemyCharacter::InitializeComponents()
{
    if (!MonsterDataTable || MonsterDataRowName.IsNone())
    {
        UE_LOG(LogTemp, Warning, TEXT("설정 없음"));
        return;  // ← 조기 return
    }
    // EnemyCombatComponent, DropComponent 초기화 전혀 안 됨
}

결과: PatternSlots 비어 있음 → FindBestPattern 항상 nullptr → 모든 공격 불가.
증상이 "공격이 안 된다"로만 나타나서 원인을 찾는 데 시간이 걸렸다.

디버깅 체크리스트

항목확인 위치
MonsterDataRowNameBP Class Defaults
MonsterDataTableBP Class Defaults
PatternSlots 채워짐 여부InitializeComponents 로그
PatternTable 유효 여부EnemyCombatComponent::Initialize 진입 확인

3부. 전투 컴포넌트 책임 분리 — 기본 공격 vs 패턴 공격

문제: 역할 혼재

RequestPatternByPriority는 DT에서 패턴을 탐색하고
쿨다운을 관리하는 패턴(특수) 공격 로직이다.
일반 공격도 이 경로로 처리하려다 책임이 뒤섞였다.

분리 설계

EnemyCombatComponent
├── RequestBasicAttack(Target)        ← 기본 공격: 탐색 없음, 즉시 발동
├── RequestPatternByPriority(Target)  ← 패턴 공격: DT 탐색, 쿨다운 관리
└── ActivateHitbox() / DeactivateHitbox()  ← 공용 (ANS에서 호출)

RequestBasicAttack 초안

bool UEnemyCombatComponent::RequestBasicAttack(AActor* Target)
{
    if (!Target || !PatternTable || BasicAttackRowName.IsNone()) return false;

    const FMonsterPatternRow* Row =
        PatternTable->FindRow<FMonsterPatternRow>(BasicAttackRowName, TEXT(""));
    if (!Row || Row->HitboxBoneName.IsNone()) return false;

    ActivePatternRowName = BasicAttackRowName;  // ActivateHitbox()가 이 값을 사용

    FGameplayEventData EventData;
    EventData.EventTag   = RetrieveGameplayTags::GameplayEvent_Enemy_Attack;
    EventData.Target     = Target;
    EventData.Instigator = GetOwner();

    return ASC->HandleGameplayEvent(..., &EventData) > 0;
}

ActivePatternRowName의 역할

ActivePatternRowName은 공격 요청 시점에 설정되어,
ANS(AnimNotifyState)가 ActivateHitbox()를 호출할 때 올바른 Row를 찾는 연결고리다.

RequestBasicAttack() / RequestPatternByPriority()
  → ActivePatternRowName 설정
       ↓
     ANS_AttackWindow::NotifyBegin
       → EnemyCombatComponent->ActivateHitbox()
            → PatternTable->FindRow(ActivePatternRowName)
                 → 히트박스 위치·크기 적용

이 값이 None이거나 잘못 설정되면 히트박스 전체가 작동하지 않는다.


비교 정리

항목기본 공격 (RequestBasicAttack)패턴 공격 (RequestPatternByPriority)
DT 탐색고정 Row (BasicAttackRowName)거리·우선순위·쿨다운 기반 탐색
쿨다운 관리없음 (GA 측에서 처리)CooldownExpiry 맵으로 관리
발동 조건없음ActivationRange, IsCooldownReady
ActivePatternRowName 설정BasicAttackRowName 고정FindBestPattern 내부에서 설정

핵심 요약

  • HandleGameplayEventEventData.Instigator를 설정하지 않으면
    GA Blueprint에서 Cast가 실패해 에러 없이 조용히 중단된다.
  • DataTable 기반 초기화는 BP Class Defaults → DataTable Row → 컴포넌트 3단계이므로
    어느 한 단계라도 None이면 전체가 실패한다.
  • 기본 공격과 패턴 공격은 책임이 다르다:
    기본 공격은 즉시 발동, 패턴 공격은 탐색과 쿨다운 관리를 포함한다.
  • ActivePatternRowName은 공격 요청 → ANS → 히트박스를 이어주는 핵심 연결고리다.
    이 값이 잘못되면 히트박스 전체가 작동하지 않는다.
  • AI가 생성한 리팩토링 코드에는 중복 순회나 잘못된 필드 할당 같은 미묘한 버그가
    섞일 수 있다. 반드시 변경 diff를 직접 검토할 것.

Retrieve — 7th Team2 Final Project | 2026-05-22

profile
게임 개발 지망생입니다.

0개의 댓글