저장형 아이템
playerstate에 옮길까 고민했지만 하이더가 잡히면 아웃이라 재스폰이 없으니 인벤토리를 컴포넌트에 둠
인벤토리 저장 구조 슬롯 변수를 택한 이유
처음엔 흔한 RPG 인벤토리 예제처럼 TMap<아이템종류, 개수> 구조를 떠올렸는데 인벤토리 개수를 본인 UI에 그려야 하는데 그러려면 개수가 서버에서 클라이언트로 복제되어야 함
근데 언리얼의 복제 시스템은 TMap과 TSet을 지원하지 않는다고 함
복제하려면 더 복잡한 방식을 구현해야할 것 같은데 아직 공부하지 않아서 이해하기 어려웠음
저장 방식은 슬롯 변수로
변수를 Replication 시키고 그걸로 UI (인벤토리창) 만들 때 읽게 하는 방식
- HeldItemRow 삭제 -> InvisibilityCount, ThrowItemCount 두 개로
- 최대치 상수 MaxInvisibility = 1, MaxThrowItem = 3 추가
- Server_UseHeldItem() -> Server_UseItem(EFDItemEffect Which)로 변경 (뭘 쓸지 지정)
- Client_NotifyInventoryFull(EFDItemEffect Which) 추가 (가득 참 알림)
- OnRep 콜백 정리 (두 카운트가 갱신될 때 UI 신호)
// FDItemInventoryComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Item/FDItemTypes.h"
#include "FDItemInventoryComponent.generated.h"
UCLASS(ClassGroup = (Item), meta = (BlueprintSpawnableComponent))
class FUNNYORDIE_API UFDItemInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UFDItemInventoryComponent();
// 뽑은 결과를 넘겨줄 때 호출
// bStoredOnPickup에 따라 보관하거나 즉시 발동
void ReceiveDrawnItem(FName ItemRow);
// 보관 아이템 사용 요청 - 어떤 종류를 쓸지 지정
UFUNCTION(Server, Reliable)
void Server_UseItem(EFDItemEffect Which);
// 보관 한도 초과로 획득 실패 시 그 클라이언트에게만 알림 (UI 경고 문구용)
UFUNCTION(Client, Reliable)
void Client_NotifyInventoryFull(EFDItemEffect Which);
// UI가 읽어갈 수 있게 개수 조회 함수 (BlueprintPure - WBP에서 바인딩)
UFUNCTION(BlueprintPure, Category = "Item")
int32 GetInvisibilityCount() const { return InvisibilityCount; }
UFUNCTION(BlueprintPure, Category = "Item")
int32 GetThrowItemCount() const { return ThrowItemCount; }
protected:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 투명화 보관 개수 - UI 표시용으로 복제
UPROPERTY(ReplicatedUsing = OnRep_Inventory)
int32 InvisibilityCount = 0;
// 투사체 보관 개수 - UI 표시용으로 복제
UPROPERTY(ReplicatedUsing = OnRep_Inventory)
int32 ThrowItemCount = 0;
// 인벤토리 UI를 다시 그리게 하려고 만든 함수
UFUNCTION()
void OnRep_Inventory();
// 아이템 데이터 테이블 (에디터에서 DT_ItemData 할당)
UPROPERTY(EditDefaultsOnly, Category = "Item")
class UDataTable* ItemDataTable;
private:
// 실제 효과 실행 (서버 전용)
void ExecuteEffect(const FFDItemData& Data);
// 보관 한도 상수
static constexpr int32 MaxInvisibility = 1;
static constexpr int32 MaxThrowItem = 3;
FTimerHandle EffectExpireTimerHandle;
};
// FDItemInventoryComponent.cpp
#include "Item/FDItemInventoryComponent.h"
#include "Character/FDHiderCharacter.h"
#include "Net/UnrealNetwork.h"
#include "Engine/DataTable.h"
UFDItemInventoryComponent::UFDItemInventoryComponent()
{
PrimaryComponentTick.bCanEverTick = false;
// 복제해야 하니 컴포넌트 자체 복제를 켬
SetIsReplicatedByDefault(true);
}
void UFDItemInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UFDItemInventoryComponent, InvisibilityCount);
DOREPLIFETIME(UFDItemInventoryComponent, ThrowItemCount);
}
void UFDItemInventoryComponent::ReceiveDrawnItem(FName ItemRow)
{
// 서버만
if (!GetOwner() || !GetOwner()->HasAuthority()) return;
if (!ItemDataTable) { return;}
const FFDItemData* Data = ItemDataTable->FindRow<FFDItemData>(ItemRow, TEXT("뽑힌 아이템 조회"));
if (!Data) return;
// 즉시발동형은 저장하지 않고 바로 실행
if (!Data->bStoredOnPickup)
{
ExecuteEffect(*Data);
return;
}
// 나머지는 전부 보관형
// 종류별 슬롯에 개수 누적 (한도 넘으면 획득 실패 + 알림)
switch (Data->Effect)
{
case EFDItemEffect::Invisibility:
if (InvisibilityCount >= MaxInvisibility)
{
// 이미 가득 참 획득 실패
Client_NotifyInventoryFull(EFDItemEffect::Invisibility);
return;
}
++InvisibilityCount;
OnRep_Inventory(); // 서버 수동 호출 (UI 갱신하는 함수)
break;
case EFDItemEffect::TaggerStun:
if (ThrowItemCount >= MaxThrowItem)
{
Client_NotifyInventoryFull(EFDItemEffect::TaggerStun);
return;
}
++ThrowItemCount;
OnRep_Inventory();
break;
default:
break;
}
}
void UFDItemInventoryComponent::Server_UseItem_Implementation(EFDItemEffect Which)
{
if (!ItemDataTable) return;
switch (Which)
{
case EFDItemEffect::Invisibility:
if (InvisibilityCount <= 0) return;
// 소모 먼저 (중복 사용 방지)
--InvisibilityCount;
OnRep_Inventory();
// 투명화 효과 실행
if (const FFDItemData* Data = ItemDataTable->FindRow<FFDItemData>(TEXT("Invisibility"), TEXT("투명화 사용")))
{
ExecuteEffect(*Data);
}
break;
case EFDItemEffect::TaggerStun:
if (ThrowItemCount <= 0) return;
--ThrowItemCount;
OnRep_Inventory();
if (const FFDItemData* Data = ItemDataTable->FindRow<FFDItemData>(TEXT("TaggerStun"), TEXT("투사체 사용")))
{
ExecuteEffect(*Data);
}
break;
default:
break;
}
}
void UFDItemInventoryComponent::Client_NotifyInventoryFull_Implementation(EFDItemEffect Which)
{
// UI에 "최대 n개까지만 보관할 수 있습니다" 경고 문구 만들어서 여기에 띄우기
}
void UFDItemInventoryComponent::OnRep_Inventory()
{
// 인벤토리 UI 여기에서 갱신 (WBP 연동)
}
void UFDItemInventoryComponent::ExecuteEffect(const FFDItemData& Data)
{
UE_LOG(LogTemp, Warning, TEXT("효과 발동"));
}
Z키는 Server_UseItem(Invisibility), X키는 Server_UseItem(TaggerStun)을 부르게 나중에 맵핑할 거임
1) 즉시발동형은 switch 위에서 먼저 걸러냄 (early return)
if (!Data->bStoredOnPickup) { ExecuteEffect; return; }로 즉시발동형을 맨 위에서 처리하고 끝
그래야 아래 switch는 보관형만 온다고 가정하고 깔끔하게 슬롯 분배만 하면 됨
2) Server_UseItem에서 Row 이름을 하드코딩 (TEXT("Invisibility"))
딱히 변수명 바꿀 것 같지 않아서 그냥 직접 넣음
한도 초과 알림을 복제 변수가 아닌 RPC를 쓴 이유
보관 한도(투명화 1개, 투사체 3개)를 초과해 아이템을 획득하려 하면 획득에 실패하면서 경고 문구를 띄우게 함
이때 가득 참을 알리는 데 복제 변수가 아니라 Client_ RPC를 사용
한도 초과는 개수가 3에서 3으로 그대로이므로 복제 변수로는 변화가 감지되지 않아 UI가 실패를 알 수 없음
반면 RPC는 순간의 사건을 특정 클라이언트에게 직접 전달하는 데 적합하니까 RPC 사용
알게된 것
캡슐화 = 데이터를 안에 감춰두고, 정해진 통로(함수)로만 다루게 하는 것
- public : 아무나 접근 가능 (통로로 쓸 함수들: IsInvincible(), Server_UseItem())
- protected : 이 클래스와 자식만 (감출 데이터: bIsInvincible, InvisibilityCount)
- private : 오직 이 클래스만 (내부 헬퍼: ExecuteEffect(), GetBalanceSettings())
변수를 감추고 함수들만 열어두면 바깥에서 할 수 있는 건
int32 n = Inventory->GetInvisibilityCount(); // 읽기는 됨
Inventory->Server_UseItem(EFDItemEffect::Invisibility); // 써줘라고 요청은 됨
Inventory->InvisibilityCount = 99; // 이건 불가능 (변수가 protected니까)
개수를 바꾸는 유일한 통로가 Server_UseItem, ReceiveDrawnItem 같은 함수뿐이면 그 함수 안에 규칙을 심어둘 수 있음
void Server_UseItem(EFDItemEffect Which)
{
// 이 통로를 지나가는 모든 사용 요청은 이 규칙을 반드시 거침
if (InvisibilityCount <= 0) return; // 없으면 못 씀
InvisibilityCount--; // 딱 1개만 감소
...
}
개수가 0이면 안 줄어들고,한 번에 1개씩만 줄어듦
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| 투사체 생성 (0) | 2026.07.10 |
|---|---|
| 투명화 아이템 사용 기능 구현 (Z키) (0) | 2026.07.10 |
| 아이템 데이터 정의 (구조체) (0) | 2026.07.09 |
| 인트로 화면 구성 (0) | 2026.07.07 |
| 에셋 (0) | 2026.07.07 |
