260305 TIL - 챕터3 D-Day

2026. 3. 5. 23:42언리얼 7기 본캠프

빨리 과제를 마감해야하는 상황상 코드카타는 1문제만 풀고 넘어감. 사실 1문제가 엄청 어려워서;; 2문제 푼거랑 다름없는 수준의 시간을 씀

 

 

//수식 최대화 문제. 이게 진짜 2레벨 문제인가? 싶을정도로 복잡하고 어려웠음
//공백 없이 들어온 수식을 숫자와 기호로 파싱해서 나누고, 연산자의 우선순위를 경우의 수로 잡아서
//처리해야 하는데, 이때 도움이 되는 next_permutation이라는 기능을 사용해서
//우선순위에 따른 계산을 하고 가장 높은 값을 answer에 저장하는 식으로 코드를 구현함
//상상도 못한 문제인데, 앞으로 이거보다 어려운게 많을거라고 생각하면 약간 막막하기도 하지만
//목표를 더 높게 잡은 만큼 마음의 각오를 단단히 하고 준비를 해야겠다는 생각이 들었음

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

long long solution(string expression) {
    long long answer = 0;
    vector<long long> numbers;
    vector<char> operators;
    string tempnum = "";
    for(int i = 0; i < expression.size(); i++)
    {
        if (expression[i] == '*' || expression[i] == '-' || expression[i] == '+')
        {
            numbers.push_back(stoll(tempnum));
            tempnum = "";
            operators.push_back(expression[i]);
        }
        else tempnum += expression[i];
    }
    numbers.push_back(stoll(tempnum));
    
    vector<char> prio = {'+', '*', '-'};
    sort(prio.begin(), prio.end());
    
    do{
        vector<long long> tempnums = numbers;
        vector<char> tempoper = operators;
        for(int i = 0; i < prio.size(); i++)
        {
            char optofind = prio[i];
            for(int j = 0; j < tempoper.size();)
            {
                if (tempoper[j] == optofind)
                {
                    long long calc;
                    if (optofind == '+') calc = tempnums[j] + tempnums[j + 1];
                    else if (optofind == '-') calc = tempnums[j] - tempnums[j + 1];
                    else if (optofind == '*') calc = tempnums[j] * tempnums[j + 1];
                    tempnums[j] = calc;
                    tempoper.erase(tempoper.begin() + j);
                    tempnums.erase(tempnums.begin() + j + 1);
                }
                else j++;
            }
        }
        answer = max(answer, llabs(tempnums[0]));
    }while (next_permutation(prio.begin(), prio.end()));
    
    return answer;
}

 

밤새서 혼자 디버깅을 마치고 영상도 찍고 하고 아침에 다같이 모여서 피드백 받고 PDF를 하면서 느낀거지만, '팀 프로젝트'인데 너무 나 혼자서 하려는 생각이 너무 많았음. 스스로의 한계를 인정하고 에고를 줄인 다음에 다같이 디버그를 한다거나 하는 프로세스가 필요했는데, 일정 관리도 제대로 못했고 막바지에 머리가 하얘질정도로 '완성해야한다' 는 강박에 시달리는 바람에 어떻게든 완성해보려고 스스로를 혹사했고, 이런점은 추후 팀프로젝트에서 반드시 고치고 '이런 버그들이 있으니 다같이 고쳐보자'는 감각으로 다같이 작업을 해야지 능률 떨어지게 혼자 하는것은 너무 해롭다고 판단함

 

특히 발표자료를 너무 급하게 준비해서... 다른 조 발표와 비교하면 명백히 전달하고자 하는 바가 잘 전달되지 않은 느낌이 있었음. 결국 플레이 영상만 틀어놓은 느낌으로 준비가 되었고 이는 좀 안타까운 부분으로 남게 됨

 

마지막 디버그를 진행하며 가장 신경썼던 부분은, 레벨 전환이나 재시작 누를 시 크래시가 계속 나는 부분이었는데,

단순히 널포인터 참조 오류로만 나와서 '이게 뭐지' 싶은게 너무 많았음. 그러다가 널포인터가 AI모듈에서 발생하고, '팅기는 타이밍이 랜덤하다(누르자마자 팅김, 누르고 몇초 뒤에 팅김, 누르고 몇초+@ 뒤에 팅김 등)' 는 점에서 착안하여, 혹시 내가 차마 찾지 못한 타이머가 있나 해서 AI 관련 코드를 전부 뜯어봄. 분명히 AI 소멸자에 타이머를 클린하는 코드를 넣었는데도 타이머 관련 문제가 나오는것 같아서 전부 뜯어봐야 겠다는 생각을 들었음

 

타이머 발견!

 

그 결과, 무작위로 Patrol하는 Behavior Tree 내에서 타이머를 따로 사용하는걸 발견하고, 이 부분을 과감히 지워버린 뒤

Behavior Tree 내에서 자체적으로 대기하는 Wait Idle을 Sequence로 연결하여 동일한 기능을 하면서 레벨 전환하면서 Destroy될때 타이머 관련 오류가 나오지 않게 수정하는데 성공함

 

 

Wait Idle을 넣어서 수정한 Behavior Tree 코드

 

이걸로 랜덤하게 나오는 크래시는 나오지 않게 되었음. 잘됐구나 잘됐어~

 

시연 영상 첨부하고 오늘 TIL은 마무리

https://www.youtube.com/watch?v=G0jVC4iVxc8

 

 

 

 

이하는 팀원분들의 KPT 회고록 첨부함

 

김상민

  Keep

  • 클래스 작성 시 최대한 결합도를 낮추기 위한 방법이 무엇일까 고민했던 점. 컴포넌트를 활용, 기능별로 분리한 점.
  • 애니메이션 몽타주, 노티파이, IK Rig 등 애니메이션 전반을 공부하고, GASP의 모션 매칭이나 Layered bone 등의 기술을 탐구하고 적용하려 한 점.

  Problem

  • 리타겟팅 시 세밀한 조정을 하지 못하여 그 뒤에 붙이려고 했던 애니메이션들도 전부 조금씩 하자가 생겼던 점. 준비한 메쉬가 리타겟팅이 정확히 되지 않는다면, 직접 본을 매칭해서 IK Retargeter 설정을 해주는 공부도 해보고 싶다.
  • 처음에 해야할 일의 양을 너무 방대하게 잡아 프로젝트 막바지엔 구조를 고려하지 못하고 급하게 할 수밖에 없었던 점. 다시 한번 시간을 잘 분배하는 것이 양질의 프로젝트를 완성하는데 가장 중요한 점이라는것을 깨닫는다.

  Try

  • 절차적 애니메이션을 적용해 보고싶다. 벽을 짚을 때 Search - Approach - Rise - Touch - Reset으로 이어지는 구조라던가, 캐릭터가 멈추거나 멀어질 때 동작을 취소하는 트리거 로직을 다양하게 구현한다던가 하는 방식을 분석해보고 싶다.
  • Motion Warping을 적용, 다음번엔 파쿠르도 더 정확하게 만들어 보고 싶다.
  • ‘좋은 구조란 무엇일까’를 계속 생각하면서 코딩을 해야겠다 느낀다. 협업 시에 결합도가 낮은 코드를 작성해서 서로의 작업을 몰라도 돌아갈 수 있는 구조를 만드는게 중요하다..!

  Feel

AAA급 게임들이라 불리는 게임들의 애니메이션은 정교한 작업들이 쌓인 결과물임을 다시금 느낀다.
애니메이터가 괜히 존재하는것이 아니다..
그렇지만 그 벽을 넘어보고 싶다는 생각이 계속 들었다.
프로그래밍과 애니메이션을 둘 다 잘 이해하는 개발자가 되고 싶다.


박민재

  Keep

이번 챕터를 진행하면서 웨이브 시스템을 퀘스트 조건에따라서 발생시키는 로직을
짠것은 정말 잘했다.
기술적으로 Delegate에대한 이해도가 많이 올라갔고 해당 구조는 다음 팀플에도
유용할것이라고 생각한다.

  Problem

문제점 및 어려웠던점 : Core쪽 역할을 맡으면서 만들어야하는부분이나 필요한부분을 팀원분들이
요청하였을때 바로바로 어떻게 만들어야한다 라기보단 이걸 어떻게 만들어야하지 라는생각과
도저히 내 스스로 못하겠다라는 생각에 많이 잠겼고 그만큼작업이 느려졌던것 같다.해결방안 : 내가 구현하려는 로직을 좀더 자세하게 파악할 수 있는능력을 기르는것이 필요하다고 느꼈고,
필요한것을 정확하게 정리해서 팀원에게 자세히 요청할 수 있는 능력을 키워야한다고 느꼇다.

  Try

다음 챕터에서는 이번에 확실히 깨달은 문제점을 보완하고, 이번에 부족했던 역할을 다시 맡아서
성장한 모습을 프로젝트에 담고싶다. 그러기위해서는 잘못됬다고 생각했던 학습법을 바로잡고
밀도있는 집중력을 키울것이다.

  Feel

지난 팀프로젝트보다 훨씬 많이 부족함을 느꼈고 좀 더 간절하게 공부해야한다는 것을 깨달았다.


정찬호

  Keep

1. DataAsset 사용
DataAsset은 공유 자원이라 인스턴스가 생기지 않아 메모리가 절약됩니다.
코드로 일일이 변수를 선언해서 관리하는 것보다 DataAsset에 정의해두고 가져와 사용한 수정이 확실히 편했습니다.
- Enemy 종류에 따른 데이터를 DataAsset으로 분리
- 특수 공격 데이터를 DataAsset으로 분리

 

2. AIPerception 사용
단순히 EnemyCharacer가 PlayerCharacter를 생성 시부터 가지고 일정 거리에 도달 시 추적을 시작하는 것이 아닌 AIPerception으로 시야 비슷하게 플레이어를 감지하고 추적하게 만드니 좀 더 현실감이 있는 것 같아서 좋았습니다.
하지만 세부 수치 조정을 진행하지 않아 인식이 부자연스러운 부분이 있어 아쉬운 결과가 되었습니다.

 

3. 오후 데일리 스크럼
모든 팀원이 시간이 될 때 작업 내용을 공유하는 시간을 가졌습니다. 보통 오후 2시였죠.
이게 은근히 괜찮았습니다.
오전에는 머리가 덜 깨어 있어서 오늘 할일, 어제 했던 일을 잘 설명하지 못했던 것 같은데, 오후에는 그동안 작업을 하면서 뭘 했는 지 복기하고 앞으로 뭘 할 지도 어느 정도 보이는 상태가 되어있어서 수월하게 이야기할 수 있었던 것 같습니다.

  Problem

1. 애니메이션 오류
RootMotion 관련
특수 공격 중 점프 후 내리 찍어 공격하는 애니메이션이 존재했습니다.
이 애니메이션 몽타주 재생 후 캐릭터가 미끄러지듯 움직이는 현상이 발생했습니다.
몽타주로 인해 그려지는 모습과 실제 물리적인 위치가 달라 이를 맞추기 위해 발생하는 문제라고 합니다.

시도 1
AnimInstance의 RootMotion을 키고 CharacterMovementComponent의 MoveMode를 FlyMode로 전환했다가 애니메이션 종료 시 되돌리는 방법을 시도했습니다.
애니메이션 종료 후 미끄러지듯 움직이는 것이 고쳐지지 않았습니다.
오히려 FlyMode 때문에 중력이 적용되지 못해서 공중에서 몽타주가 정지해 버립니다.

시도 2
FlyMode 없이 에니메이션 에셋에서 Root 모션과 관련 옵션을 수정해 보았습니다.
위치 이동 없이 제자리에서 뛰어올랐다 내려치는 동작을 취하는 것이 너무 부자연스러웠습니다.

시도 3(해결)
AN_Jump 노티파이를 추가해 애니메이션에서 점프를 하는 지점에서 LaunchCharacter를 해주기로 했습니다.
이제 몽타주와 메쉬의 위치가 어긋나지 않고 겉보기에도 부자연스럽지 않습니다.애니메이션 전환 관련
Enemy가 Dead 상태가 되어 모든 로직이 종료되었을 텐데 죽는 애니메이션이 종료된 후 Idle 애니메이션으로 전환되는 오류가 발생했었습니다.
이는 BlendOut 타임 설정과 Dead 처리 종료 전용 노티파이의 위치가 겹쳐서 처리되는 중에 전환이 발생하게 된 것이었습니다.
그 둘을 수정하니 정상적으로 Dead 처리가 되었습니다.

 

2. DataAsset 이해도 부족
DataAsset은 공유 자산이기에 인스턴스가 생기지 않습니다.
초기에 이를 몰라 DataAsset에 변경 수치를 집어 넣는 바람에 SpecialAttack(특수 공격) 기능이 먹통이 되어버리는 문제가 발생했었습니다.변동 수치를 포함한 SpecialAttack 전체를 관리하는 Component를 별개로 생성하여 문제를 해결했습니다.

 

3. BT노드 종류
C++ BT 노드를 만들고 제일 헤맸던 것이 BP로 만들어지지 않았다는 것이었습니다.
C++로 기본 틀을 짜고 BP에서 세세한 수치나 특정 액터 전용 로직을 추가할 생각이었는데 BP 부모 클래스 지정하는 부분에서 검색이 되질 않아서 AI에게 물어보고 구글링을 하면서 헤맸습니다.나중에 애초부터 BT 노드 클래스에는 C++로만 생성가능한 것과 BP의 부모로서 만드는 것이 구별되어 있다는 것을 알게 되었고,
그냥 C++ 그대로 가져다 사용하기로 결정했습니다.

 

4. UFUNCTION() 매크로 & 가리기 & 정적 바인딩의 콜라보
부모클래스에서 일반으로 선언된 함수와 자식에서 virtual로 선언된 동명의 함수가 있을 때,
자식 클래스 타입 ->함수명으로 호출하면 무엇이 호출되는가?
부모 클래스의 일반 함수가 호출되는 것을 직접 경험했습니다. AI에서는 'UFUNCTION() 매크로로 인해 부모의 것을 가리키는 가능성'과 '컴파일 단계에서 해당 함수가 부모 클래스의 것이라고 결정했을 가능성'을 제시했습니다.부모 클래스에서  virtual을 붙이는 것으로 해결을 했습니다.

 

5. 레벨 전환에 Timer는 극독이다
최종 머지 이후 팀원분께서 레벨 전환 시에 충돌이 발생한다는 말씀을 해주셨습니다.
원인을 찾아보니 제가 범인이었습니다.
AI 이동 Task에서 Timer를 사용하고 있었는데 이것 때문에 로직이 제대로 종료되지 않아 문제가 발생했던 겁니다.
이전에도 비슷한 문제가 있어 EndPlay를 추가하는 것으로 해결했는데 또 비슷한 문제가 발생해서 면목이 없었습니다.

다행히 팀원분께서 무사히 수정해 주셨습니다.

  Try

1. AIPerception 듣기 감지
AIPerception에 Hearing을 추가했었는데 정작 사용하지는 않았습니다.
진짜 소리 발생을 감지하는게 아니라 소리 이벤트를 발생시키고 그것을 감지하는 방식이어서 사용하려면 소리를 내야할 액터를 맡은 분들에게 안내를 드렸어야 했지만, 시각만으로도 충분하다는 생각도 있었고 다른 작업 내용도 많아 어느새 잊어버렸습니다.
회고하면서 생각해보니 Enemy 유인용 미끼나 공격 소음으로 인한 추적 등을 구현할 수 있었을 것 같아 아쉬움을 느끼고 있습니다.
다음번에는 한 번 시도를 해봐야겠습니다.

 

2. 세세한 수치 설정
기능이나 오류의 해결에만 몰두하다 보니 기존에 만들어둔 기능을 충분히 테스트하지 못했습니다.
당장 EnemyTypeData(Enemy 종류에 따른 데이터 값을 저장하는 DataAsset)도 제대로 수정하지 못하고 한 Enemy의 것을 복제한 뒤 대충 수치랑 몽타주만 변경한 것이 다였습니다.
다음에는 직접 테스트를 진행하며 최대 체력, 공격력, 방어력, 및 그 계산 공식 등을 작성하는 밸런싱 작업을 진행해 볼 것입니다.

 

3. VFX & SFX
기능 구현에만 급급하다 보니 파티클과 사운드 효과가 적절히 들어가지 못했습니다.
공격 시에 팔 주위에 트레일을 넣는 다거나, 피격 시에 피 파티클이 튀게 만든다거나
사망 시에는 검은 연기 같은 것이 피어오르면서 서서히 매쉬가 투명해진다거나 등등
생각나는 연출은 많았지만 작업 난이도를 오판하는 바람에 시간 배분에 실패하였고 결국 추가되지 못하였습니다.

 

4. AI 점프 이동
AI에게 특정 지점에서 떨어져서 이동하거나 점프해서 이동할 수 있음을 알리는 액터가 존재합니다.
바로 NavLink인데 이에 대해 제대로 학습하지 못하였고 점프 관련 로직을 추가하기도 애매해서 사용하길 포기했습니다.
다음에는 NavLink를 사용해 AI가 좀 더 생동감 있게 움직일 수 있도록 만들고 싶습니다.

  Feel

모든 프로젝트는 진행할 때마다 어렵게 느껴지는 것 같습니다.
'이거다!'라는 생각으로 만들었던 것이 다르게 동작해 갈아엎기도 하고, 이전에 만든 것과 새로 만든 것이 충돌해서 하나를 아예 없애버리기도 하는 등 나름 설계를 하고 시작해도 예상치 못한 일이 계속해서 발생했죠.
진행하면서 힘들고 스트레스를 받아도 끝나고 나면 '좀 더 잘할 수 있었을 텐데' 라는 생각이 들어 아쉬우면서 즐겁기도 합니다.
이번 프로젝트에서도 저는 팀원분들의 도움을 받았고 다음에도 받을 것입니다. 하지만 받은 만큼 도움을 줄 수 있는 사람이 되어가는 것 같아 스스로가 좀 더 나아갔다는 생각을 합니다.
다음에는 스스로가 좀 더 나은 설계와 일정 조정을 할 수 있기를 기대합니다.


주철민

  Keep

  • 인벤토리의 그리드 시스템과 드래그앤 드롭, 무기의 부착물 장착 시스템은 원하는대로 구현이 잘 되었고, 추후 비슷한 프로젝트를 진행하게 된다면 이걸 꼭 응용해보고 싶음
  • 커밋을 나눠쓰는 습관을 기를 수 있었고 이후 깃을 사용하는 과정에서도 이를 좀 더 세분화해서 할 수 있으면 좋을것 같음
  • 그냥 C++가 아닌 Unreal C++의 코드를 읽고 해석하는 눈이 많이 늘었으며, 이는 더욱 발달시켜 가져갈 수 있었으면 함

  Problem

  • 목표를 크게 잡는건 좋았지만, 그 목표의 한계를 느끼고 전환하는 시점이 너무 늦었음. 일정에 휘말려 해야할 일을 제대로 파악하지 못하게 됨
  • 일정 관리를 체계적으로 하지 못해서 남은 기간동안 어떤 일을 얼마나 할 수 있을지에 대한 인사이트가 부족했음
  • 게임을 작동할 수 있게끔 고쳐보려는건 좋았지만, 너무 혼자서 많은 일을 하려고 시도한 것 같음

  Try

  • 일정을 더더욱 체계적으로 잡고, 변동사항이 있을것 같으면 일찍이 팀원분들과 소통을 시도해서 가능한 업무의 한계점을 정확히 이른시간에 도출해낼 것을 시도하기
  • 너무 혼자 하려는 생각은 줄이고, 최대한 팀원들과 찾은 내용들을 공유하며 같이 수정할 수 있게 시도해보기
  • Git 사용법을 조금 더 명확하게 익히고, 커밋이나 머지 과정에서 조금 더 효율적으로 할 수 있는 방법 알아보면서 시도해보기

  Feel

  • 시간이 부족하다고 느꼈지만 다르게 말하면 아직 그만한 속도로 일을 처리할 능력까지 올라가진 못했다는 뜻이니 더더욱 갈고닦아야 할 것임
  • 단순히 C++로 협업하는게 아닌 Unreal 상에서 협업하는것은 천지차이라는걸 느꼈고, 더더욱 많은 상호 소통이 있어야 코드 보완도 잘 되고 충돌도 덜 될것이라고 생각함

조수경

  Keep

팀워크가 좋았다.

  Problem

시간 조절을 하지 못해 기획의 일부를 포기할 수 밖에 없었음.

다음부터는 시간 분배를 세세하게 할 필요가 있어보임.

  Try

더 다양한  기능을 구현해 보았으면 좋았을 것 같음.

미리 하고 싶은거 생각해 놓고 구현할 수 있을 만큼의 실력 올리기!

  Feel

다양하고 더 직관적으로 UI을 만드는 것은 생각보다 복잡하면서 어려웠고

게임을 개인으로 만들 때와 팀으로 만들 때 어마어마한 차이가 있어보임.