이전에 푼 적 있는 문제였나 봅니다. 기록이 남아 있네요.
이전 코드
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
void split(vector<string> &out, string str, char delimeter)
{
string splited="";
for(int i=0;i<str.length();i++)
{
if(str[i]!=delimeter)
{
splited+=str[i];
}
else
{
out.push_back(splited);
splited="";
}
}
if(splited!="")
{
out.push_back(splited);
}
}
int str_to_int(string str)
{
int result=0;
for(int i=0;i<str.length();i++)
{
result*=10;
result+=(str[i]-'0');
}
return result;
}
int solution(vector<vector<string>> book_time) {
int answer = 0;
int room_cnt=0;
vector<int> start_time;
vector<int> end_time;
vector<string> splited_str;
for(int i=0;i<book_time.size();i++)
{
splited_str.clear();
split(splited_str,book_time[i][0],':');
start_time.push_back(str_to_int(splited_str[0])*60
+str_to_int(splited_str[1]));
splited_str.clear();
split(splited_str,book_time[i][1],':');
end_time.push_back(str_to_int(splited_str[0])*60
+str_to_int(splited_str[1])+10);
}
sort(start_time.begin(),start_time.end());
sort(end_time.begin(),end_time.end());
int start_idx=0,end_idx=0;
while(start_idx!=start_time.size())
{
if(start_idx<=start_time.size()-1)
{
if(start_time[start_idx]<end_time[end_idx])
{
room_cnt++;
start_idx++;
answer=max(answer,room_cnt);
}
else if(start_time[start_idx]==end_time[end_idx])
{
start_idx++;
end_idx++;
}
else
{
end_idx++;
room_cnt--;
}
}
}
return answer;
}
최대한 안 보고 풀어 봅시다.
호텔 객실 사용을 최소화하면 예약 손님을 받기, 한 번 사용한 객실은 퇴실 시간 + 10분 동안 사용이 불가
예약 시각은 문자열(book_time) 형태로 제공
book_time : ["HH:MM", "HH:MM"], [대실 시작 시각, 대실 종료 시각]
전부 분으로 바꿔서 priority_queue에 집어넣고 사용중인 방의 수룰 기록해서 최대 값을 반환하면 될 것 같네요.
book_time을 분으로 변경하는 함수를 먼저 만들고, 그 다음에 처리 로직을 작성해야겠어요.
#include <string>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
void splitString(vector<string>& outVector, const string& toSplit, char delimiter)
{
string curStr = "";
for(char ch : toSplit)
{
if(ch != delimiter)
{
curStr += ch;
}
else
{
if(curStr != "")
{
outVector.push_back(curStr);
curStr = "";
}
}
}
if(curStr != "")
{
outVector.push_back(curStr);
}
}
int book_timeToMin(const string& book_time)
{
vector<string> splited;
splitString(splited, book_time, ':');
return stoi(splited[0]) * 60 + stoi(splited[1]);
}
int solution(vector<vector<string>> book_time) {
int answer = 0;
int curInCnt = 0;
priority_queue<int, vector<int>, greater<int>> inTimes;
priority_queue<int, vector<int>, greater<int>> outTimes;
for(vector<string>& timeRow : book_time)
{
int startTime = book_timeToMin(timeRow[0]);
int endTime = book_timeToMin(timeRow[1]) + 10;
inTimes.push(startTime);
outTimes.push(endTime);
}
while(!inTimes.empty())
{
int startTime = inTimes.top();
inTimes.pop();
while(!outTimes.empty())
{
if(outTimes.top() <= startTime)
{
outTimes.pop();
curInCnt--;
}
else
{
break;
}
}
curInCnt++;
if(curInCnt > answer)
{
answer = curInCnt;
}
}
return answer;
}
점프키 = 파쿠르 키인 것을 뒤늦게 확인하고 점프 테스트를 한 결과, 점프가 정상적으로 동작하지 않는 다는 것을 학인했습니다.
메시지 로그에서 "로컬 플레이어 컨트롤러만 위젯에 할당할 수 있습니다. BP_PlayerController_MainMenu_C_1 : 로컬 플레이어 컨트롤러가 아닙니다."라는 에러 로그가 출력되었습니다. 한 번 주의 깊게 확인해 볼 필요가 있습니다.
일단 싱글 플레이 테스트를 먼저 진행하겠습니다.
체크리스트 보드에 버그 리포트를 작성하고 있었네요.
체크리스트 보드에는 체크 리스트들을 적어 넣는 거였군요. 이런..
대대적으로 수정하고 있습니다.
불행히도 노션 페이지를 Claude로 수정하는 것은 못했지만 붙여넣기 양식에 맞춰서 채크 리스트를 출력하는 것은 가능했습니다.
붙여넣기 양식에 맞춰서 출력된 테스트 케이스의 일부
P1-High 싱글플레이 UI LLM 생성 ⏭️ 보류 S-030 : 체력 피해 → AO_HealthWidget 실시간 감소 + 색상 변화 1) AI 공격으로 피해 수신 2) AO_HealthWidget 즉시 감소 확인 3) 체력 30% 이하 시 색상 변화 확인 OnAttributeChanged로 위젯 즉시 갱신, 체력 비율 기반 색상 피드백
P3-Low 싱글플레이 UI LLM 생성 S-040 : 세션 목록(AO_LobbyListWidget) 페이지네이션 — 5개씩 표시 1) 세션 검색 실행 2) 목록 5개 표시 확인 3) 다음 페이지 버튼으로 나머지 확인 페이지당 5개씩 표시, 이전/다음 정상, 마지막 페이지 다음 버튼 비활성
P3-Low 싱글플레이 UI LLM 생성 S-039 : 설정 초기화(SetToDefaults) → 전 설정값 기본값 복원 1) 그래픽/오디오/해상도 임의 변경 2) SetToDefaults 호출 3) 모든 값 기본값 확인 4) ApplyAndSaveAllSettings 저장 확인 모든 설정 기본값으로 복원, 저장 후 재시작 시 기본값 유지
P3-Low 싱글플레이 UI LLM 생성 S-038 : 게임 설정 — 그래픽/해상도/음량 변경 후 저장 1) 그래픽 품질·해상도·마스터 볼륨 변경 2) 저장 후 재시작 3) 설정값 유지 확인 변경 설정이 재시작 후에도 유지, AO_DelegateManager 설정 이벤트 정상 브로드캐스트
P2-Medium 싱글플레이 UI LLM 생성 S-037 : 오디오 타입별 볼륨 독립 조절 (Master/SFX/Voice/Ambient) 1) Master 0 → 전체 무음 확인 2) SFX만 0 → 효과음만 소거 3) Voice만 0 → 음성만 소거 4) 저장 후 재시작 유지 확인 각 오디오 타입 독립 조절, 저장 후 재시작 시 유지, 타입 간 간섭 없음
P2-Medium 싱글플레이 UI LLM 생성 S-036 : 일시정지 메뉴 — 설정 탭 변경 + 메인 메뉴 나가기 1) ESC 입력 2) 일시정지 메뉴 표시 확인 3) 설정 탭 변경 4) 메인 메뉴로 나가기 일시정지 시 게임 멈춤, 설정 적용, 메인 메뉴 복귀 시 세션 정상 종료
P2-Medium 싱글플레이 UI LLM 생성 S-035 : 인벤토리 UI 아이템 정보 정확성 (이름/수량/아이콘) 1) 인벤토리 UI 열기 2) 슬롯 이름·수량·아이콘 확인 3) DataTable 정보와 대조 DataTable 기반 정보와 UI 표시 일치, 수량 오차 없음
P2-Medium 싱글플레이 UI LLM 생성 S-034 : 인벤토리 슬롯 스크롤 입력 → 하이라이트 즉시 변경 + 랩어라운드 1) 마우스 휠 UP/DOWN 입력 2) ServerSetSelectedSlot 호출 확인 3) 하이라이트 이동 확인 4) 양 끝 랩어라운드 확인 입력마다 하이라이트 즉시 변경, 양단 랩어라운드 정상
P1-High 싱글플레이 UI LLM 생성 S-033 : 인터랙션 오브젝트 접근/이탈 → AO_InteractionWidget 팝업/소멸 1) 범위 진입 → 위젯 팝업 확인 2) 텍스트 정확성 확인 3) 이탈 → 소멸 확인 4) 비활성 인터랙션 미표시 확인 진입 즉시 FAO_InteractionMessage 브로드캐스트로 위젯 표시, 이탈 시 즉시 소멸
P2-Medium 싱글플레이 UI LLM 생성 S-032 : 스태미나 25% 잠금 → AO_StaminaWidget 경고 상태 시각 구분 1) 스프린트로 스태미나 25% 도달 2) 색상 변화 확인 3) 회복 시 정상 색상 복귀 확인 25% 잠금 시 색상 변화, 스프린트 불가 시각 피드백, 회복 후 복귀
P1-High 싱글플레이 UI LLM 생성 S-031 : 스프린트 입력 → AO_StaminaWidget 스태미나 바 실시간 감소 + 회복 1) 스프린트 키 홀드 2) AO_StaminaWidget 감소 확인 3) 해제 → 회복 증가 확인 스프린트 중 부드럽게 감소, 해제 후 증가, 25% 이하 경고 색상 전환
테스트 도중 튜토리얼 맵에서 버그가 발생했습니다.
튜토리얼 진행은 기차 안으로 들어가는 것을 지시했으나 기차는 문으로 막혀있고 아무런 상호작용도 나타나지 않았습니다.
확인 결과 기차 문에 상호작용하는 부분에 버그가 존재했습니다.
// 현재 — 상호작용 완전 차단
bool AAO_TrainDoor::CanInteraction(const FAO_InteractionQuery& InteractionQuery) const
{
return false;
}
// 수정 후 — 부모 클래스 정상 로직 사용
bool AAO_TrainDoor::CanInteraction(const FAO_InteractionQuery& InteractionQuery) const
{
return Super::CanInteraction(InteractionQuery);
}
테스트를 진행해보니 기존 싱글 플레이 전용 테스트 케이스를 멀티플레이로 변경해야 함을 확인했습니다.
퍼즐, 공격, 체력 등의 요소는 튜토리얼 맵에서 확인이 불가능하여 본 맵에 들어가야 하는데 이 경우, 세션 생성 등의 작업을 거치고 들어가야 하니 멀티 플레이(1인)의 상황이 됩니다.
메타휴먼 커스터 마이징 기능이 정상적으로 동작하지 않는 버그를 픽스하기
출력된 로그
LogMutable: Started Update Skeletal Mesh Async. CustomizableObject=CO_Character_Anka Instance=CustomizableObjectInstance_17, Frame=57654
LogKSH: UAO_DummyCustomComponent::OnMeshUpdateFinished : Mesh update finished
LogElectraPlayer: [2] Looping (61) from 26.517 to 0.000
LogMutable: Started Update Skeletal Mesh Async. CustomizableObject=CO_Character_Anka Instance=CustomizableObjectInstance_17, Frame=57805
LogMutable: Started Update Skeletal Mesh Async. CustomizableObject=CO_Character_Anka Instance=CustomizableObjectInstance_17, Frame=57811
LogMutable: Disabling "Started Update Skeletal Mesh Async" log during 5.000000 seconds due to spam
LogViewport: Display: Viewport MouseLockMode Changed, DoNotLock -> LockOnCapture
LogViewport: Display: Viewport MouseCaptureMode Changed, CaptureDuringMouseDown -> CapturePermanently
LogBlueprintUserMessages: [WBP_RevivalChip] Server: 0
LogMutable: Started Update Skeletal Mesh Async → Mutable 커스터마이징 갱신 요청 정상 발생
LogKSH: OnMeshUpdateFinished → 메시 업데이트 콜백 정상 수신
WBP_RevivalChip Server: 0 → 커스터마이징과 무관, 부활 칩 UI 관련 로그
저장 버튼에 바인딩된 로직에는 로그가 없는 것인지 아니면 아예 바인딩에 문제가 있는 건지 확인할 필요가 있어 보입니다.
OnRep 함수는 없으니 여기서 종료되는 것 같습니다.
구현이 아직인 것인지, 아니면 중간에 빠진 코드가 있는 건지를 알아봐야겠네요.
관련 코드를 Claude에게 분석을 맡기겠습니다.
1차 프롬포트
저장 버튼에 바인딩된 로직에는 로그가 없는 것인지 아니면 아예 바인딩에 문제가 있는 건지 확인할 필요가 있어 보입니다.
1. Save 버튼 클릭
2. AO_DummyCustinmComponent에서 SaveCustomizingData()를 호출
3. SaveCustomizingData() 내부에서 PlayerState->ServerRPC_SetCharacterCustomizingData(CustomizingData) 호출
4. CharacterCustomizingData를 커스터 마이징 UI에서 수정한 데이터로 갱신
OnRep 함수는 없으니 여기서 종료되는 것 같습니다.
구현이 안 된 기능인 것인지, 아니면 구현되었으나 호출이 되지 않는 것인지 모르겠습니다.
캐릭터 메쉬를 갱신하는 코드, 커스터 마이징 정보를 저장하고 불러오는 코드 등의 관련 코드를 분석해서 해당 기능의 현재 구현되어 있으나 일부 호출 코드가 빠진 것인지, 아니면 추가 구현이 필요한 상황인지 확인을 부탁드립니다.
응답은 다음과 같습니다.
구현 현황 분석
핵심 결론: "구현됨, 단 Save 시 실제 캐릭터 메쉬 갱신 호출 누락"
[Save 버튼 클릭] (Blueprint에서 바인딩)
↓
SaveCustomizingData() [DummyCustomComponent.cpp:36]
├─ Instance에서 현재 HairStyle/ClothType 옵션 읽기
├─ PlayerState->CharacterCustomizingData = CustomizingData (로컬 직접 할당)
└─ PlayerState->ServerRPC_SetCharacterCustomizingData(CustomizingData) 호출
↓
[Server] CharacterCustomizingData = CustomizingData (단순 할당, 끝)
→ Replicated 설정이므로 클라이언트에 복제됨
→ 단, OnRep 콜백 없음 → 아무 것도 자동 실행 안 됨
버그 핵심
SaveCustomizingData()는 PlayerState에 데이터 보존만 합니다. 실제 플레이어 캐릭터의 CustomizingComponent::CustomizingData는 건드리지 않습니다.
따라서 Save를 눌러도:
✅ PlayerState에는 저장됨 → 다음 레벨 로드 시 복원됨
❌ 현재 세션의 실제 캐릭터 메쉬는 변경 안 됨
실제 메쉬가 반영되는 시점은 BeginPlay에서 LoadCustomizingDataFromPlayerState()가 호출될 때뿐입니다.
제안된 수정 방안
SaveCustomizingData()에 한 줄 추가
void UAO_DummyCustomComponent::SaveCustomizingData()
{
// ... 기존 코드 ...
PlayerState->CharacterCustomizingData = CustomizingData;
PlayerState->ServerRPC_SetCharacterCustomizingData(CustomizingData);
// ✅ 추가 필요: 실제 플레이어 캐릭터 메쉬에도 즉시 반영
if (UAO_CustomizingComponent* CustomComp = Player->GetCustomizingComponent())
{
CustomComp->ServerRPC_ChangeCustomizing(CustomizingData);
}
}
기대 효과
1. PlayerState에 저장 (레벨 전환 후 복원용)
2. CustomizingComponent 업데이트 → OnRep_CustomizingData() → ApplyCustomizingData() → 실제 메쉬 갱신
그런데 저는 UAO_CustomizingComponent::ServerRPC_ChangeCustomizing(CustomizingData)를 PlayerState가 아니라 DummyCustomComponent에서 호출하는 지가 의아했습니다. 그래서 물어보았습니다.
2차 프롬포트
BP 상에서 바인딩이 되어 있는 것을 확인했습니다.
한 가지 의아한 점이 있습니다.
메쉬를 갱신하는 함수인 UAO_CustomizingComponent::ServerRPC_ChangeCustomizing(CustomizingData);를 PlayerState가 아닌 DummyCustomComponent에서 진행하는 이유가 있을까요?
응답 결과
좋은 질문입니다. 오히려 DummyCustomComponent에서 호출하는 것이 더 이상한 구조입니다.
왜 DummyCustomComponent에서 호출하면 안 좋은가
문제 1: RPC가 2번 발생
[Client: DummyCustomComponent::SaveCustomizingData()]
│
├─→ ServerRPC_SetCharacterCustomizingData() ← RPC ①
└─→ CustomizingComponent::ServerRPC_ChangeCustomizing() ← RPC ②
같은 데이터를 서버에 두 번 보내는 셈입니다. 불필요한 네트워크 비용이 발생하고, 두 RPC의 도달 순서가 보장되지 않습니다.
문제 2: 책임 분리 위반
DummyCustomComponent는 커스터마이징 메뉴의 프리뷰 전용 컴포넌트입니다. 실제 플레이어의 CustomizingComponent까지 직접 참조해서 조작하는 건 역할을 넘어서는 것입니다.
그러면 PlayerState에서 호출하는 것이 올바른 방법이었네요. 물어보길 잘했습니다.
이번에는 수정방안으로 제시한 코드가 좀 이상하네요.
// AO_PlayerState.cpp
void AAO_PlayerState::ServerRPC_SetCharacterCustomizingData_Implementation(
const FCustomizingData& NewCustomizingData)
{
// ① 레벨 전환 대비 저장
CharacterCustomizingData = NewCustomizingData;
// ② 이미 서버이므로 RPC 없이 직접 CustomizingComponent에 적용
if (APawn* Pawn = GetPawn())
{
if (UAO_CustomizingComponent* CustomComp =
Pawn->FindComponentByClass<UAO_CustomizingComponent>())
{
CustomComp->CustomizingData = NewCustomizingData; // 서버에서 직접 할당
CustomComp->ApplyCustomizingData(); // 서버 메쉬 갱신
// CustomizingData가 ReplicatedUsing이므로 클라이언트에 자동 복제
// → OnRep_CustomizingData() → ApplyCustomizingData() 자동 실행
}
}
}
커스텀 데이터를 받아서 적용하는 ServerRPC_ChangeCustomizing()가 있는데 굳이 비공개 멤버 변수에 접근하는 코드를 작성해 주었네요. 그냥 다음과 같이 수정했습니다.
void AAO_PlayerState::ServerRPC_SetCharacterCustomizingData_Implementation(const FCustomizingData& CustomizingData)
{
CharacterCustomizingData = CustomizingData;
if (APawn* Pawn = GetPawn())
{
if (UAO_CustomizingComponent* CustomComp =
Pawn->FindComponentByClass<UAO_CustomizingComponent>())
{
CustomComp->ServerRPC_ChangeCustomizing(CharacterCustomizingData);
}
}
}
노션 페이지의 테스트 케이스 작성이 끝난 뒤에 다음과 같이 적는 부분이 있었습니다.
검수 기준을 적는 부분에 다음과 같이 작성했습니다.
해당 부분을 작성하면서 체크 리스트 생성 당시에는 코드 이해가 부족해서 테스트 절차 그대로 실행이 가능한지 확신이 없었고, 기대 결과는 크게 생각하지 않았다는 것을 되돌아보게 되었습니다.
다음에 테스트 케이스나 체크 리스트를 검수하게 되는 일이 있다면 코드를 좀 더 읽어보며 이해를 한 뒤에 생성을 하고 기대 결과도 주의깊게 확인을 해야겠습니다.
최소한 튜토리얼까지는 플레이 한 뒤에 테스트 케이스를 생성해야 겠네요. 퍼즐이나 적대 AI는 튜토리얼에 없다는 것을 알지 못한 채 테스트 케이스를 생성했더니 죄다 싱글 플레이용 테스트 케이스로 작성이 되었습니다.
PCG 특강 - 윤상혁 튜터님
GetActorProperty
GetTextureData
고급 폴리지같은 느낌으로 이해했습니다.
다양한 것을 지원하고 있네요. 나중에 제대로 뜯어먹어봐야겠습니다.
김성훈님 Q:
pcg 넓게쓰면
월드파티션도 쓸것같은데
파티션 컬링이랑 디스턴스컬링이랑
어떤걸 주로쓸까요?
튜터님 :
저라면 파티션 컬리을 쓸 것 같습니다. 그리드 단위가 최적화가 잘 되고, 너무 큰 상태에서 자꾸 이어서 사용하면 PC 부하가 크기 필요한 부분만 켜서 테스트하고 나중에 필요한 부분만 켜서 사용하는 것이 좋습니다.
그래프 테스트부터 작게하고 늘리기
이전에 풀었던 기록이 남아 있는 문제였습니다. 그래서 최대한 안 보고 다시 풀어봤습니다.
"HH:MM" 형태의 문자열로 주어진다.시간을 전부 분 단위로 변환한 뒤 priority_queue(min-heap) 두 개로 체크인/체크아웃 시간을 관리하고, 동시에 사용 중인 방의 최대 수를 반환하는 방식으로 접근했습니다.
이전 코드는 직접 split, str_to_int 함수를 구현한 C 스타일이었는데, 이번에는 STL(stoi, range-based for)을 적극적으로 활용해서 훨씬 간결해졌습니다.
#include <string>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
void splitString(vector<string>& outVector, const string& toSplit, char delimiter)
{
string curStr = "";
for(char ch : toSplit)
{
if(ch != delimiter) curStr += ch;
else if(curStr != "") { outVector.push_back(curStr); curStr = ""; }
}
if(curStr != "") outVector.push_back(curStr);
}
int book_timeToMin(const string& book_time)
{
vector<string> splited;
splitString(splited, book_time, ':');
return stoi(splited[0]) * 60 + stoi(splited[1]);
}
int solution(vector<vector<string>> book_time) {
int answer = 0, curInCnt = 0;
priority_queue<int, vector<int>, greater<int>> inTimes, outTimes;
for(vector<string>& timeRow : book_time)
{
inTimes.push(book_timeToMin(timeRow[0]));
outTimes.push(book_timeToMin(timeRow[1]) + 10);
}
while(!inTimes.empty())
{
int startTime = inTimes.top(); inTimes.pop();
while(!outTimes.empty() && outTimes.top() <= startTime)
{
outTimes.pop(); curInCnt--;
}
curInCnt++;
answer = max(answer, curInCnt);
}
return answer;
}
테스트를 진행하면서 뒤늦게 깨달은 것이 있습니다. 퍼즐, 공격, 체력 등 본 게임 콘텐츠는 튜토리얼 맵에 없고, 본 맵에 들어가려면 세션을 생성해야 합니다. 즉, 혼자 방을 만들어 들어가도 멀티플레이(1인 호스트) 환경이 되는 것입니다.
그래서 기존에 싱글 플레이로 분류했던 테스트 케이스 중 19개(+ 이후 커스터마이징 1개 추가로 총 20개)를 호스트 단독으로 재분류했습니다.
처음부터 최소한 튜토리얼이라도 플레이해본 뒤 테스트 케이스를 만들었어야 했는데 아쉬운 부분입니다.
튜토리얼 맵에서 기차 안으로 들어가라는 지시가 있었는데, 기차 문 앞에서 아무런 상호작용 UI도 뜨지 않았습니다.
코드를 확인해보니 원인은 단순했습니다.
// 현재 — 상호작용 완전 차단
bool AAO_TrainDoor::CanInteraction(const FAO_InteractionQuery& InteractionQuery) const
{
return false;
}
// 수정 후 — 부모 클래스 정상 로직 사용
bool AAO_TrainDoor::CanInteraction(const FAO_InteractionQuery& InteractionQuery) const
{
return Super::CanInteraction(InteractionQuery);
}
CanInteraction()이 무조건 false를 반환하도록 하드코딩되어 있었습니다. 상호작용 감지 시스템이 이 함수를 호출해서 필터링하는 구조이기 때문에, 기차 문은 범위에 들어가도 UI 자체가 표시되지 않았던 것입니다.
커스터마이징 저장이 제대로 되지 않는 것 같아서 로그를 확인했습니다.
LogMutable: Started Update Skeletal Mesh Async. ...
LogKSH: UAO_DummyCustomComponent::OnMeshUpdateFinished : Mesh update finished
Mutable 갱신 요청과 콜백 수신은 정상이었습니다. 그런데 저장 버튼을 눌러도 관련 로그가 없어서 의심이 생겼습니다. BP에서 바인딩을 확인해보니 바인딩 자체는 되어 있었습니다.
Claude에게 코드 분석을 맡겼더니 핵심 문제를 찾아냈습니다.
문제 핵심: SaveCustomizingData()는 PlayerState에만 데이터를 저장하고, 실제 플레이어 캐릭터의 CustomizingComponent는 건드리지 않습니다.
Save 버튼 클릭
↓
SaveCustomizingData()
├─ PlayerState->CharacterCustomizingData = CustomizingData ← 저장됨
└─ PlayerState->ServerRPC_SetCharacterCustomizingData() ← 서버에 전달
↓
[Server] CharacterCustomizingData = CustomizingData ← 단순 할당, 끝
OnRep 없음 → 클라이언트 메쉬 갱신 안 됨
결국 저장은 되지만 현재 세션의 캐릭터 메쉬는 바뀌지 않고, 다음 레벨 로드 시에야 적용되는 구조였습니다.
처음에는 DummyCustomComponent에서 ServerRPC_ChangeCustomizing()을 추가로 호출하는 방안이 제안됐지만, 이는 RPC가 2번 발생하고 책임 분리도 어색한 구조였습니다.
올바른 위치는 PlayerState의 RPC 구현부 입니다. 이미 서버에 있으니 RPC 없이 직접 ServerRPC_ChangeCustomizing을 호출하면 됩니다.
void AAO_PlayerState::ServerRPC_SetCharacterCustomizingData_Implementation(
const FCustomizingData& CustomizingData)
{
CharacterCustomizingData = CustomizingData;
if (APawn* Pawn = GetPawn())
{
if (UAO_CustomizingComponent* CustomComp =
Pawn->FindComponentByClass<UAO_CustomizingComponent>())
{
CustomComp->ServerRPC_ChangeCustomizing(CharacterCustomizingData);
}
}
}
Claude가 제안한 코드에서 비공개 멤버 변수에 직접 접근하는 부분이 있어서 이미 존재하는 ServerRPC_ChangeCustomizing()을 활용하는 방향으로 직접 수정했습니다. 물어보길 잘했습니다.
노션 워크시트에 최종 검수 기준을 작성하면서 되돌아보게 됐습니다.
- 기본 요소(플레이어 입력 피드백, 게임 사이클, 네트워크 동기화)가 포함되어 있는가?
- 중복된 테스트 케이스가 없는가?
- 충분한 수의 테스트 케이스가 있는가?
체크리스트를 처음 만들 때는 코드 이해가 부족해서 테스트 절차대로 실행이 가능한지도 확신이 없었고, 기대 결과도 대충 작성했던 것 같습니다. 다음에 테스트 케이스를 만들 때는 코드를 충분히 읽어보고, 기대 결과도 꼼꼼히 작성해야겠습니다.
GetActorProperty는 그 액터에서 지정된 이름의 프로퍼티를 읽어온다.Use Absolute Transform을 켜야 월드 좌표에 맞게 배치된다. 주의!고급 폴리지와 비슷한 개념으로 이해했습니다. 지원하는 기능이 꽤 많아 보여서 나중에 제대로 뜯어봐야겠습니다.
Q (김성훈님): PCG를 넓게 쓰면 월드 파티션도 같이 쓸 것 같은데, 파티션 컬링과 디스턴스 컬링 중 어느 쪽을 주로 쓰나요?
A (튜터님): 파티션 컬링을 추천합니다. 그리드 단위 최적화가 잘 되어 있고, 넓은 맵에서 계속 이어서 사용하면 PC 부하가 커지기 때문에 필요한 부분만 켜서 테스트하는 것이 좋습니다. 그래프 테스트도 작은 범위부터 시작해서 점진적으로 늘리는 것을 권장합니다.