
오늘은 프로젝트에 NPC 상점 시스템을 구현하고, NpcWorldSubsystem / NpcShopComponent / PlayerController / Shop UI 위젯 사이의 흐름을 점검하는 작업을 진행하였습니다.

FCMShopItemContentFName ItemID : 아이템 식별용 IDint32 BuyPrice : 구매 가격int32 SellPrice : 판매 가격int32 Quantity : 수량 정보FText ItemName : 표시용 이름UTexture2D* ItemIcon : 아이콘 이미지UDataTable 기반으로 상점 아이템 목록을 관리하도록 설계ACMNpcBase 에 TObjectPtr<UDataTable> ShopItemDataTable, FName NpcId 를 추가하여 NPC 단위로 상점 구성을 다르게 가져갈 수 있도록 준비
NPC 캐싱 구조 설계 (UCMNpcWorldSubsystem)
FCMNpcCacheEntryFName NpcIdTWeakObjectPtr<ACMNpcBase> NpcActorTMap<FName, FCMNpcCacheEntry> NpcCacheMap 에 NPC를 ID 기반으로 캐싱RegisterNpc(const FName& NpcId, ACMNpcBase* NpcActor)NpcCacheMap.FindOrAdd(NpcId) 를 이용해 캐시 엔트리 생성/조회 후 값 설정UnregisterNpc(const FName& NpcId)GetNpcById(const FName& NpcId) constGetAllNpcEntries(TArray<FCMNpcCacheEntry>& OutEntries) constNPC에서 Subsystem 등록 흐름
ACMNpcBaseFName NpcId 를 에디터에서 설정 가능하도록 노출UCMNpcWorldSubsystem 을 가져와 RegisterNpc(NpcId, this) 를 호출하도록 구현UnregisterNpc(NpcId) 로 정리NPC 상점 컴포넌트 (UCMNpcShopComponent)
UCMNpcComponentBase 를 상속TArray<FCMShopItemContent> ShopItemContents 를 내부에 보관virtual void PerformAction() override;void SetShopItemContents(const TArray<FCMShopItemContent>& InItems);void GetShopItemContents(TArray<FCMShopItemContent>& OutItems) const;PerformAction() 에서의 역할ACMNpcBase) 로부터 NpcId 를 가져옴ACMPlayerController)를 찾아 RequestOpenShopUI(NpcId) 호출PlayerController의 상점 UI 흐름 (ACMPlayerController)
TSubclassOf<UCMShopWidget> ShopWidgetClass; : 에디터에서 상점 메인 위젯 클래스 지정UCMShopWidget* ShopWidgetInstance; : 런타임 인스턴스 보관UFUNCTION(BlueprintCallable, Category = "Shop|UI") void RequestOpenShopUI(const FName& NpcId);Server_RequestShopData(NpcId) 호출UFUNCTION(Server, Reliable, WithValidation) void Server_RequestShopData(const FName& NpcId);UCMNpcWorldSubsystem 에서 GetNpcById(NpcId) 로 NPC 조회UCMNpcShopComponent 를 찾아 GetShopItemContents() 로 상점 아이템 배열 획득CreateShopWidget(ShopItems) 를 바로 호출하여 서버/클라 겸용 컨트롤러에서 UI 생성Client_ReceiveShopDataAndOpen(ShopItems) 클라 RPC 호출UFUNCTION(Client, Reliable) void Client_ReceiveShopDataAndOpen(const TArray<FCMShopItemContent>& ShopItems);CreateShopWidget(ShopItems); 호출void CreateShopWidget(const TArray<FCMShopItemContent>& InShopItems);IsLocalController() 확인 후SetShopItems() / BuildShopList() 만 호출하여 갱신UIManagerComponent->PushWidget(ShopWidgetClass) 로 위젯을 스택에 올린 뒤, 데이터 주입 및 리스트 빌드Shop 위젯 구현 (UCMShopWidget)
UVerticalBox* ShopItemListBox; (BindWidget)TSubclassOf<UCMShopContentElementWidget> ShopItemWidgetClass;TArray<FCMShopItemContent> ShopItems;void SetShopItems(const TArray<FCMShopItemContent>& InItems);void BuildShopList();BuildShopList() 동작 개요ShopItems를 순회하면서 ShopItemWidgetClass 로 UCMShopContentElementWidget 생성UCMShopWidget) 의 HandleElementBuyRequested, HandleElementSellRequested 와 연결개별 상점 아이템 위젯 (UCMShopContentElementWidget)
FCMShopItemContent 를 시각화Host(리슨 서버) / 클라이언트 동작 차이 정리
UCMNpcShopComponent::PerformAction() → ACMPlayerController::RequestOpenShopUI(NpcId) 호출RequestOpenShopUI → Server_RequestShopData 가 같은 프로세스의 서버 컨텍스트에서 실행NpcWorldSubsystem 으로 상점 데이터 조회 후 CreateShopWidget 을 직접 호출RequestOpenShopUI → Server RPC 로 서버에 상점 데이터 요청Client_ReceiveShopDataAndOpen 으로 돌려줌CreateShopWidget 을 호출해 UIManager로 Push디버깅용 로그 추가
CreateShopWidgetShopItems.Num() 과 첫 번째 아이템의 ItemID, ItemName, BuyPrice, SellPrice, Quantity 를 로그로 출력Server_RequestShopDataNpcId 와 NPC/ShopComponent 유효성 체크 로그 출력UCMNpcShopComponent::PerformActionNpcId 가 유효한지 확인하는 로그 출력오늘은 NPC 상점 시스템의 전체 흐름을 정리하고, 특히 NpcWorldSubsystem 기반의 NPC 캐싱과 UCMNpcShopComponent → ACMPlayerController → UCMShopWidget 으로 이어지는 상점 UI 생성 파이프라인을 점검하였습니다. Host(리슨 서버) 환경과 순수 클라이언트 환경에서의 동작 차이를 고려하여 Server RPC 및 Client RPC 분기를 설계한 덕분에, 네트워크 환경에 따라 상점 UI가 일관되게 동작하도록 만들 수 있었습니다. 앞으로는 이 구조 위에 실제 구매/판매 서버 검증 로직과 인벤토리, 골드 연동 로직을 추가하여 완성도 높은 상점 시스템으로 발전시켜 나가고자 합니다.