이전에 푼 적 있는 문제입니다.
이전 코드
#include <string>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm>
using namespace std;
struct stage
{
int floor;
int cnt;
bool operator<(const stage s) const
{
return this->cnt<s.cnt;
}
};
int solution(int storey) {
int answer = numeric_limits<int>::max();
priority_queue<stage> q;
stage s;
s.floor=storey;
s.cnt=0;
q.push(s);
while(!q.empty())
{
stage s=q.top();
q.pop();
stage newS;
if(s.floor<10)
{
answer=min(answer,s.cnt+min(s.floor,10-s.floor+1));
}
else
{
if(s.floor%10==0)
{
newS.floor=s.floor/10;
newS.cnt=s.cnt;
q.push(newS);
}
else
{
newS.floor=s.floor/10;
newS.cnt=s.cnt+s.floor%10;
q.push(newS);
newS.floor=s.floor/10+1;
newS.cnt=s.cnt+(10-s.floor%10);
q.push(newS);
}
}
}
return answer;
}
되도록 이전 코드를 보지 않고 진행하겠습니다.
10의 c 승으로 이동할 수 있는 버튼들이 있고 목표는 0층으로 이동하면서 최소한의 횟수로 버튼을 누르는 것이네요. ( C >= 0 제한)
1의 자리부터 점차 올라가게 하는 것이 좋겠네요. 아닌가 예외처리할 게 많은가?
BFS나 DFS로 쭉 파고 들어가는 방법도 있긴 한데..
예를 들어 85면 +1 - 5번, +10 - 1번, -100 1번으로 7번이 최적해일 텐데
75면 +1 - 5번, +10 - 2번, -100 1번으로 8번,
65면 +1 - 5번, +10 - 3번, -100 1번으로 9번
55면 +1 - 5번, +10 - 4번, -100 1번과 -1 - 5번, -5 - 5번으로 10번
45면 -1 - 5번, -40 - 4번으로 8번
음 그냥 5일 때는 다음수가 5이상이면 올리는 것이 좋은 것 같네요.
6이상은 그냥 올리는 것이 낳고요.
일단 복잡한 식 없이 간닥하게 풀어봤습니다.
정답률은 76.9가 나왔네요.
1차 코드
#include <string>
#include <vector>
using namespace std;
int solution(int storey) {
int answer = 0;
int maxDividor = 1;
int dividor = 1;
while(maxDividor < storey)
{
maxDividor *= 10;
}
while(dividor <= maxDividor)
{
int nowNum = storey / dividor;
nowNum %= 10;
int nextNum = storey / (dividor * 10);
if(nowNum > 5
|| (nowNum == 5 && nextNum >= 5))
{
answer += (10 - nowNum);
storey += (10 - nowNum) * dividor;
}
else
{
answer += nowNum;
storey -= nowNum * dividor;
}
dividor *= 10;
}
if(storey != 0)
{
answer++;
}
return answer;
}
아니 이걸 nextNum에서 한자리만 남기는 연산을 빼먹고 있었네요.
2차 코드
#include <string>
#include <vector>
using namespace std;
int solution(int storey) {
int answer = 0;
int maxDividor = 1;
int dividor = 1;
while(maxDividor < storey)
{
maxDividor *= 10;
}
while(dividor <= maxDividor)
{
int nowNum = storey / dividor;
nowNum %= 10;
int nextNum = storey / (dividor * 10);
nextNum %= 10;
if(nowNum > 5
|| (nowNum == 5 && nextNum >= 5))
{
answer += (10 - nowNum);
storey += (10 - nowNum) * dividor;
}
else
{
answer += nowNum;
storey -= nowNum * dividor;
}
dividor *= 10;
}
if(storey != 0)
{
answer++;
}
return answer;
}
테스트를 원활히하기 위해 에디터 옵션 중 해상도를 반타작 내는 기능을 튜터님에게 안내 받았습니다.
Lumen과 Virtual Shadow Maps가 RTX 2060에서 에디터 프레임을 깎아먹는 1, 2위입니다. 수업/튜터링용 프로젝트면 Project Settings → Rendering에서:
Dynamic Global Illumination: Lumen → None 또는 Screen Space
Reflection Method: Lumen → Screen Space
Shadow Map Method: Virtual Shadow Maps → Shadow Maps
Hardware Ray Tracing: 끄기 (소프트웨어 Lumen이 그나마 가벼움)
이거 셋만 꺼도 뷰포트 FPS가 두 배 가까이 뜁다고 합니다.
Editor Preferences에서:
Use Less CPU when in Background: ON (이미 알고 계실 거지만 PIE 디버깅 때 끄셔야 한다는 점)
Real-Time 뷰포트는 필요할 때만 (단축키 Ctrl+R)
Engine Scalability Settings → Medium 또는 Low (에디터 뷰포트만 영향, 게임 빌드와 별개)
뷰포트 띄워놓고 작업할 때:
r.ScreenPercentage 75
r.Streaming.PoolSize 2000
r.VolumetricFog 0
r.Fog 0
QA 문서의 4번째 챕터는 "디버깅 및 수정 내역서 작성하기"였습니다.
기존에 버그 리포트 보드에 원인 분석과 수정 내용을 정리했기에 그것을 옮겨 작성했습니다.
수정 내역서에서 추가로 소요 시간을 요구하고 있었습니다. Claude에 각 버그에 대한 채팅 기록 시간을 가지고 소요 시간을 유추해달라 요청했기 때문에 부정확한 시간을 작성하게 되었습니다.
| 버그ID | 소요시간 | 심각도 | LLM 활용 | 난이도 | 리그레션 | 리테스트 결과 | 수정 결과 | 버그 제목 | LLM 활용 내용 | 수정 내용 | 원인 분석 | 어디까지 파악했나요 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AI-002 | 1시간 20분 | Minor | 미활용 | 낮음 | 없음 | 통과 | 수정 완료 | AI 캐릭터 이동 시 발자국 사운드 미재생 | AO_AN_AIFootstep.cpp L38, L58 두 곳의 VolumeMultiplier 인자를 0.f → VolumeMultiplier로 변경 Notify() 내 SpawnSoundAtLocation·PlaySoundAtLocation 두 호출 모두 VolumeMultiplier 자리에 0.f가 하드코딩되어 항상 볼륨 0으로 재생됨 | AO_AN_AIFootstep.cpp의 Notify 함수를 직접 확인하여 볼륨 파라미터 오기입 2곳까지 독립적으로 파악 및 수정 |
AI의 소리를 재생하는 코드를 찾기 위해 버그 힌트의 Notify로 검색했습니다. 알맞는 클래스를 찾지 못하고 AI 폴더를 열고 AI 휘하의 폴더를 열었보았습니다.
그리고 Tast 아래에서 2개의 문제 호출문 중 하나를 찾았습니다.
AO_STTask_PlaySoundInterval.cpp - 155번째 줄
InstanceData.AudioComponent->Play(0.f);
재생 값이 0으로 되어있어서 아무런 소리가 나질 않았던 것으로 보입니다.
아니었군요. 이 값은 StartTime이었습니다.
다시 파일들을 확인하여 한 Notify를 발견했습니다.
버그의 원인으로 짐작되는 코드도 발견했습니다.
AO_AN_AIFootstep.cpp - 31번째 줄 ~ 60번째 줄
if (AttenuationSettings)
{
UAudioComponent* AudioComp = UGameplayStatics::SpawnSoundAtLocation(
Owner,
Sound,
Owner->GetActorLocation(),
FRotator::ZeroRotator,
0.f,
PitchMultiplier,
0.0f,
AttenuationSettings
);
// MaxDuration이 설정되어 있으면 해당 시간 후 자동 중지
if (AudioComp && MaxDuration > 0.0f)
{
AudioComp->StopDelayed(MaxDuration);
}
}
else
{
// Attenuation 없으면 기본 PlaySoundAtLocation 사용
UGameplayStatics::PlaySoundAtLocation(
Owner,
Sound,
Owner->GetActorLocation(),
FRotator::ZeroRotator,
0.f,
PitchMultiplier
);
현재 첫번째와 세번째 0.f로 되어 있는 곳은 VolumMultiplier의 값이 들어가야 하는 자리입니다.
즉, 지금 코드로는 무조건 소리의 크기가 0이 된다는 것입니다.
AO_AN_AIFootstep.cpp - 31번째 줄 ~ 60번째 줄
if (AttenuationSettings)
{
UAudioComponent* AudioComp = UGameplayStatics::SpawnSoundAtLocation(
Owner,
Sound,
Owner->GetActorLocation(),
FRotator::ZeroRotator,
VolumeMultiplier,
PitchMultiplier,
0.0f,
AttenuationSettings
);
// MaxDuration이 설정되어 있으면 해당 시간 후 자동 중지
if (AudioComp && MaxDuration > 0.0f)
{
AudioComp->StopDelayed(MaxDuration);
}
}
else
{
// Attenuation 없으면 기본 PlaySoundAtLocation 사용
UGameplayStatics::PlaySoundAtLocation(
Owner,
Sound,
Owner->GetActorLocation(),
FRotator::ZeroRotator,
VolumeMultiplier,
PitchMultiplier
);
인클루드는 선언은 다음의 순서로 작성해보자
1. CoreMinimal
2. Engine에서 사용하는 것
3. 우리가 만든 클래스
4. .generated.h
함수 선언은 생성자가 맨위에
그 다음에는 Replicate 정의를 위해 GetLifetimeReplicteProps
BeginPlay, Tack, SetUpPlayerInputComponent등은 신 버전으로 들어오며 Public으로 변경됨.
가상함수가 그 다음으로 온다. 보통 재정의를 하기 위해?
빡빡한 팀은 문단 마다 자리를 수를 맞추기 위해 글자수가 짧은 함수 옆에 공백을 추가해서 () 위치를 통일하기도 한다.
필수는 아니다. 팀 코드 컨밴션 분위기를 보고 따라하자.
저는 InputAction이 10개가 넘어가면 TMap으로 만들어서 보관하기도 합니다. 매크로로 묶어도 되고요.
매크로로 묶거나 컨테이너로 묶으면 사용하기 복잡합니다. 프로젝트가 거의 완성 단계여서 코드를 수정할 일이 거의 없어지면, 코드를 정리하는 시간이 주어집니다. 그 때 코드 컨밴션에 맞춰서 묶어주거나 하면 됩니다.
TObjectPtr은 자동으로 nullptr로 초기화를 해주니 굳이 초기화 코드를 작성하지 않아도 됩니다.
초기화는 되도록 생성자에서 해주는 것이 추천됩니다.
형변환으로 나온 것을 받을 경우 const를 쓰는 것을 고려해보자. 함수 호출이 안 되어서 버그가 날 수 있으니 조심할 것
클래스를 직접 상속하는 BP가 없다면 abstract을 붙이는 것도 좋습니다.
상속 클래스를 고를 때 아예 나오지 않습니다.
특정 설정 값들(카메라 관련 속성값들)을 선언해서 사용할 거면 구조체를 만들거나 UObject를 상속시킨 클래스로 빼내서 묶어주는 것이 좋다.
변수 순서나 함수 순서 등이 햇갈리면 중요도 순서로 나열하면 좋다. Character.h 같은 엔진에서 제공하는 기본 클래스를 참고하거나, AI와 대화하듯이 이야기를 나눠보는 것이 좋다.
IsValid는 유효한 척 하는 놈들(쓰레기 값, GC로 제거 대기)을 걸러내는 역할을 한다.
컴포넌트 같이 생성자 단계에서 생성하는 경우는 간단히 null 체크로도 충분할 것이다.
DeveloperSettings
ProjectSettings를 보면 BP 꽂아넣을 수 있게 되어 있습니다.
한 번 공부해보자.
어떤 것인가?
어떨 때 써야 하는가?
어떤 장점과 어떤 단점이 있나?
어떨 때 지양해야 하는 가?
OnRep_의 로직을 서버에서도 사용해야 한다면, 아예 해당 로직을 별개의 함수로 분리해서 사용하는 것이 안전합니다
OnRep는 어디까지나 클라이언트에서만 호출이 된다는 개념이 존재합니다.
분리된 함수를 OnRep에서 호출하는 식으로 분리하세요.
데이터 에셋이나 리소스 같은 것을 넘길 때는 포인터 보다는 값, 서버와 클라이언트가 동일한 에셋을 가진다면 인덱스를 넘기는 식이 더 좋다.
캐릭터 자체가 필요한 상황 같은 경우에는 포인터를 넘겨도 상관없다.
이전에 풀었던 문제를 이전 코드를 보지 않고 다시 풀었다.
핵심 아이디어는 각 자릿수를 독립적으로 처리하는 것이다. 현재 자릿수(nowNum)가 5 초과이거나, 5이면서 바로 위 자릿수(nextNum)가 5 이상이면 올림 처리가 유리하고, 그렇지 않으면 내림 처리가 유리하다.
1차 시도에서 nextNum에 % 10을 빠뜨려 정답률 76.9%가 나왔다. 한 자릿수만 추출해야 하는데 전체 몫을 그대로 넘긴 것이 원인이었다.
// 버그 — % 10 누락
int nextNum = storey / (dividor * 10);
// 수정
int nextNum = storey / (dividor * 10);
nextNum %= 10;
BFS로 접근했던 이전 코드보다 직관적이고 깔끔하게 정리됐다. 자릿수 기반 그리디 접근이 이 문제에선 더 적합하다.
이전 세션까지 수정한 버그 10개(UI-001, CHAR-001~004, INV-001, COMBAT-001~002, TRAIN-001, AI-001)에 대한 수정 내역서 초안을 완성했다. 항목은 버그ID, 소요시간, 심각도, LLM 활용, 난이도, 리그레션, 리테스트 결과, 수정 결과, 버그 제목, LLM 활용 내용, 수정 내용, 원인 분석, 어디까지 파악했나요.
채팅 기록 타임스탬프(.jsonl)를 PowerShell로 파싱해서 버그별 실제 소요 시간을 측정했다. 가장 오래 걸린 건 AI-001(3시간 15분)이었는데, 이 중 LLM과의 분석 시간은 약 22분이고 나머지는 직접 수정·검증이었다.
튜터님이 제공한 버그 5개의 내역서 초안을 작성했다. 이번에는 내가 직접 찾는 것을 원칙으로 했다.
AI-002 — AI 캐릭터 발자국 사운드 미재생
AO_AN_AIFootstep.cpp의 Notify() 함수를 확인했더니 SpawnSoundAtLocation과 PlaySoundAtLocation 두 호출 모두 VolumeMultiplier 인자 자리에 0.f가 하드코딩되어 있었다.
// 버그 — 볼륨 0 고정
UGameplayStatics::SpawnSoundAtLocation(
Owner, Sound, Owner->GetActorLocation(),
FRotator::ZeroRotator,
0.f, // ← VolumeMultiplier 자리
PitchMultiplier, ...
);
// 수정
VolumeMultiplier, // ← 멤버 변수로 교체 (기본값 1.0f)
Attenuation 사용 여부에 따라 분기가 나뉘어 있어 두 곳 모두 수정해야 했다. 볼륨이 0이면 애니메이션 노티파이가 정상 발동되어도 소리가 들리지 않으니, 이런 버그는 재생 자체가 안 된다고 착각하기 쉽다.
나머지 4개(INTER-001, REP-01, CAN-001, MC-02)는 직접 파악 중이다.
튜터님의 코드 리뷰에서 배운 내용들을 정리한다.
1. CoreMinimal.h
2. 엔진 클래스 (UObject, AActor 등)
3. 직접 만든 클래스
4. .generated.h
각 그룹 내에서는 알파벳 순으로 정렬.
생성자 → GetLifetimeReplicatedProps → BeginPlay / Tick / SetupPlayerInputComponent 등 → 가상 함수(override) 순서가 일반적이다. 최신 엔진에선 BeginPlay 등이 public으로 이동했다.
팀에 따라서는 괄호 위치를 맞추기 위해 짧은 함수명 옆에 공백을 추가하기도 한다. 필수는 아니고 팀 컨벤션을 따르면 된다.
TObjectPtr은 자동으로 nullptr 초기화되므로 별도 초기화 코드가 불필요하다.const를 붙이는 것을 고려하자. 함수 호출 제한으로 버그가 날 수 있으니 주의.abstract를 붙이면 BP 상속 후보 목록에서 아예 제외된다.InputAction이 많아지면 TMap이나 매크로로 묶을 수 있지만, 프로젝트 완성 단계에 정리하는 것이 현실적이다.IsValid()는 쓰레기 값이나 GC 대기 중인 객체를 걸러낸다. 생성자에서 생성한 컴포넌트는 단순 nullptr 체크로 충분하다.UObject 상속 클래스로 분리하면 좋다.
OnRep_의 로직을 서버에서도 사용해야 한다면, 해당 로직을 별개의 함수로 분리하고OnRep_에서 그 함수를 호출하는 구조로 만드는 것이 안전하다.
OnRep는 어디까지나 클라이언트에서만 자동 호출된다. 서버에서 같은 로직이 필요하면 직접 호출해야 하는데, 이때 OnRep_ 함수를 서버에서 직접 호출하면 의미가 불명확해진다. 별도 함수로 분리하고 양쪽에서 호출하는 것이 명확하다. 오늘 REP-01 버그를 보면서 이 패턴이 지켜지지 않았을 때 어떤 문제가 생기는지 직접 확인했다.
ProjectSettings에서 BP를 꽂아넣을 수 있는 DeveloperSettings 기능이 있다. 아래 질문들을 기준으로 나중에 정리해볼 것이다.
오늘은 QA 마무리 작업과 코드 리뷰가 겹쳤다. 코드 리뷰에서 들은 OnRep 분리 패턴이 REP-01 버그와 바로 연결됐다. 오늘 직접 수정한 AI-002는 볼륨 파라미터 하나였지만, 재생이 아예 안 되는 것처럼 보이는 증상이라 원인 찾는 게 생각보다 까다로울 수도 있었다. 힌트 없이 파라미터 순서까지 직접 확인한 것은 나름대로 성취감이 있었다.
내일 마감이라 남은 버그 4개(INTER-001, REP-01, CAN-001, MC-02)를 마저 수정해야 한다.