UPROPERTY(ReplicatedUsing=OnRep_EquippedParts)
TMap<ENSPartSlot, FNSPartData> EquippedParts;
기존의 하나의 파츠만 보유하던 구조에서 여러 파츠를 소유할 수 있는 구조로 확장하는 중
TMap으로 장착된 파츠들을 저장하려고 했는데 아래 에러가 발생했다.
NSPartEquipComponent.h(74): Error : Replicated maps are not supported.
UE의 프로퍼티 리플리케이션은 TMap을 지원하지 않는다. (포인터, Map, TSet 모두 델타 직렬화 불가)
해결: TMap에서 슬롯을 구조체 필드로 옮기고, TArray로 전환해 배열이 복제되도록 변경.
USTRUCT()
struct FNSPartData {
ENSPartSlot Slot; // 배열 내 슬롯 식별자로 사용
...
};
UPROPERTY(ReplicatedUsing=OnRep_EquippedParts)
TArray<FNSPartData> EquippedParts;
| 메서드 | 의미 | 로드 유발 |
|---|---|---|
IsNull() | 경로 자체가 비어있음 (아무것도 안 가리킴) | X |
IsValid() | 지금 메모리에 로드되어 있고 유효 | X |
IsPending() | 경로는 있는데 아직 로드 안 됨 | X |
.Get() | 로드돼 있으면 포인터, 아니면 nullptr | X |
핵심: 경로가 있는지 체크할 때는 IsNull(), 메모리에 올라와 있는지 체크할 때는 IsValid().
IsValid()를 guard clause에 쓰면 경로는 있지만 아직 로드 안 된 경우에도 nullptr을 반환하는 버그가 생긴다.
실수 예시:
// 잘못된 코드 — Definition이 로드돼 있으면 nullptr 반환해버림
if (Part.DefinitionPtr.IsValid())
return nullptr;
// 올바른 코드 — 경로 자체가 없을 때만 bail
if (Part.DefinitionPtr.IsNull())
return nullptr;
.Get()은 로드를 절대 유발하지 않는다. 지금 메모리에 올라와 있으면 포인터, 아니면 그냥 nullptr.
동기 로드는 .LoadSynchronous(), 비동기 로드는 RequestAsyncLoad가 별도로 존재한다.
NSDataSubsystem이 번들 단위로 PrimaryAsset을 미리 로드해두기 때문에
.Get()이 정상 반환되는 것 — 번들 로드 전이라면 .Get()은 nullptr을 반환하고 조용히 실패한다.
FGameplayAbilitySpec Spec(Loaded, 1, INDEX_NONE, this);
| 인자 | 설명 |
|---|---|
Ability | 부여할 GA 클래스 |
Level | 어빌리티 레벨 (스케일링/레벨 기반 효과에 사용) |
InputID | INDEX_NONE = 입력 바인딩 없음 (패시브/내부 트리거용) |
SourceObject | 이 어빌리티를 부여한 주체. 파츠 컴포넌트라면 this |
SourceObject에 this를 넘기면 나중에 "어느 파츠가 준 GA인지" 추적 및 조건 판정에 사용할 수 있다.
ASC->GiveAbility(Spec)의 반환 핸들(FGameplayAbilitySpecHandle)을 컴포넌트가 직접 보관해
파츠 제거 시 ClearAbility(Handle)로 정확히 그 GA만 회수하는 GAS 정석 패턴.
FNSPartData* FindPart(ENSPartSlot Slot); // 수정 가능 포인터
const FNSPartData* FindPart(ENSPartSlot Slot) const; // 읽기 전용 포인터
C++은 호출하는 객체가 const인지 아닌지로 어느 쪽을 쓸지 자동 선택한다.
const 멤버 함수 안에서는 non-const 버전을 호출할 수 없으므로 (const 위반)
외부에서 const UNSPartEquipComponent&로 참조할 경우를 위해 두 버전 모두 필요하다.
UE 멀티플레이에서 Pawn은 사망 시 파괴되지만 PlayerState는 인런 내내 살아있다.
파츠/증강처럼 사망 후에도 유지되어야 하는 데이터는 PlayerState(또는 PlayerState에 붙은 컴포넌트)에 두는 것이 정석.
| 위치 | 사망 시 | 용도 |
|---|---|---|
| Pawn/Character | 파괴됨 | 비주얼, 입력, 물리 |
| PlayerState | 유지됨 | 파츠, 증강, 스탯, 진행도 |
ASC도 PlayerState에 두면 GE/GA가 사망 후에도 유지되고,
부활 시 InitAbilityActorInfo(PS, newPawn)으로 Avatar만 교체하면 기존 효과가 새 Pawn에 자동 적용된다.
World에 스폰된 액터(예: 드랍 파츠)는 owning connection이 없다.
클라이언트가 이 액터에 직접 Server RPC를 보내려 해도 라우팅이 안 된다.
해결: 플레이어 측(Pawn/PlayerController)에서 Server RPC를 받아,
서버에서 trace 등으로 대상 액터를 찾아 해당 로직을 직접 호출하는 패턴을 사용.
// 드랍 액터에 ServerRPC X
// 플레이어 컨트롤러에서:
void AMyPlayerController::ServerPickup_Implementation(ANSDroppedPart* Target) {
Target->TryPickup(GetPawn());
}
id별 최종 닉네임 매핑을 만들어두고, 메시지 순서를 따로 저장한 뒤 마지막에 합치는 방식.
unordered_map<string, string> matchMap;
vector<pair<string, string>> message;
for (const string& rec : record) {
string command, uid, nickname;
// flag로 직접 파싱하는 방식 → 어색하고 비효율적
}
개선: stringstream으로 공백 기준 자동 분리.
for (const string& rec : record) {
stringstream ss(rec);
string command, uid, nickname;
ss >> command >> uid >> nickname;
if (command == "Enter" || command == "Leave")
message.push_back({uid, command});
if (command != "Leave")
matchMap[uid] = nickname;
}
stringstream은 공백/개행 기준으로 토큰을 자동 분리해준다.
직접 flag를 관리하는 파싱보다 짧고 의도가 명확하다.