저장형 아이템 

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

+ Recent posts