## 前言 ActionRPG在RPGType.h定义了各个类型,在RPGControllerBase类中定义背包逻辑。存档逻辑写在RPGGameInstance与RPGSaveGame中。 个人建议:如果你打算制作个人项目,这里的代码基本不用改,直接就可以用。当然最好还是将共用部分封装成插件,这样后续Ue4项目也可以使用。这也是官方GameplayAbility视频教程中所建议的。 ## 物品 ### RPGItem.h FPrimaryAssetType ItemType:物品类型。其中这些类型都定义在URPGAssetManager类中
其他还有物品名称、描述、图标、价格、最大数量、最高等级、附带的Ability等级变量。 TSubclassOf GrantedAbility:物品附带的Ability的对象 FString GetIdentifierString():返回主资源的Id字符串 FPrimaryAssetId GetPrimaryAssetId():返回主资源的Id ### RPGType.h #### RPGItemSlot FRPGItemSlot位于RPGType.h中:比如黑魂里的快捷物品栏、装备武器栏。你可以给Slot指定类型来进行区分。因为他通常表现为放置特定类型物品的格子,所以之后我就将其翻译成物品格子了。 FPrimaryAssetType ItemType:物品类型 int32 SlotNumber:该格子所在容器的索引值 #### FRPGItemData int32 ItemCount:背包中的物品数量 int32 ItemLevel:物品等级 UpdateItemData函数:物品发生变动时的容错处理。 #### 委托 ``` //背包物品发生变化时 FOnInventoryItemChanged, bool, bAdded, URPGItem*, Item FOnInventoryItemChangedNative, bool, URPGItem* //背包物品格子的信息发生改变 FOnSlottedItemChanged, FRPGItemSlot, ItemSlot, URPGItem*, Item FOnSlottedItemChangedNative, FRPGItemSlot, URPGItem* //打包背包系统加载Asset与数据时 FOnInventoryLoaded FOnInventoryLoadedNative //加载游戏存档 FOnSaveGameLoaded, URPGSaveGame*, SaveGame FOnSaveGameLoadedNative, URPGSaveGame* ``` ### RPGPlayerControllerBase.h 个人不太喜欢把背包逻辑都写在控制类中,还是倾向于封装成一个Component。所以本人通过继承UActorComponent来移植Inventory方面的代码。 #### 变量 ``` /** Map of all items owned by this player, from definition to data */ //玩家所拥有物品的Map TMap InventoryData; /** Map of slot, from type/num to item, initialized from ItemSlotsPerType on RPGGameInstanceBase */ //物品格子Map,保存物品的类型与数量数据,使用RPGGameInstanceBase中的ItemSlotsPerType Map初始化。 TMap SlottedItems; ``` #### 函数 ``` //当背包发生变化时,会调用这个函数,并且通知所有委托 void InventoryItemChanged(bool bAdded, URPGItem* Item); /** Called after an item was equipped and we notified all delegates */ //当物品被装备时,会调用这个函数,并且通知所有委托 void SlottedItemChanged(FRPGItemSlot ItemSlot, URPGItem* Item); //往背包中增加物品 bool AddInventoryItem(URPGItem* NewItem, int32 ItemCount = 1, int32 ItemLevel = 1, bool bAutoSlot = true); //从背包中移除物品 bool RemoveInventoryItem(URPGItem* RemovedItem, int32 RemoveCount = 1); //返回背包中指定类型的所有物品,如果没有指定类型将会返回所有物品 void GetInventoryItems(TArray& Items, FPrimaryAssetType ItemType); //返回这个背包实例的物品数 int32 GetInventoryItemCount(URPGItem* Item) const; /** Returns the item data associated with an item. Returns false if none found */ //返回指定物品的数据 bool GetInventoryItemData(URPGItem* Item, FRPGItemData& ItemData) const; //将物品移动到指定物品格子 bool SetSlottedItem(FRPGItemSlot ItemSlot, URPGItem* Item); /** Returns item in slot, or null if empty */ //返回指定物品格子中的物品指针 URPGItem* GetSlottedItem(FRPGItemSlot ItemSlot) const; /** Returns all slotted items of a given type. If none is passed as type it will return all */ //返回物品格子中指定类型的物品,如果没有指定类型,将会返回所有物品 void GetSlottedItems(TArray& Items, FPrimaryAssetType ItemType, bool bOutputEmptyIndexes); //填充空的物品格子 void FillEmptySlots(); //保存背包数据 bool SaveInventory(); //载入背包数据 bool LoadInventory(); ``` #### 委托 因为本人制作的Demo为魂like游戏,所以Token类型的Item会作为“魂”来定义。魂并不会影响UI显示,所以本人的项目里会多加一个委托:FOnInventoryItemChanged OnInventoryTokenItemChanged; 并且在通知函数FillEmptySlotWithItem中进行判断,如果是Token类型就只会对OnInventoryTokenItemChanged进行多播。 ``` //背包物品增加或者减少 FOnInventoryItemChanged OnInventoryItemChanged; //本地版本,在BP委托之前执行 FOnInventoryItemChangedNative OnInventoryItemChangedNative; //当物品格子发生改变时 FOnSlottedItemChanged OnSlottedItemChanged; //本地版本 FOnSlottedItemChangedNative OnSlottedItemChangedNative; //背包数据载入时调用委托 FOnInventoryLoaded OnInventoryLoaded; //本地版本 FOnInventoryLoadedNative OnInventoryLoadedNative; ``` #### 接口 ``` // 实现IRPGInventoryInterface virtual const TMap& GetInventoryDataMap() const override { return InventoryData; } virtual const TMap& GetSlottedItemMap() const override { return SlottedItems; } virtual FOnInventoryItemChangedNative& GetInventoryItemChangedDelegate() override { return OnInventoryItemChangedNative; } virtual FOnSlottedItemChangedNative& GetSlottedItemChangedDelegate() override { return OnSlottedItemChangedNative; } virtual FOnInventoryLoadedNative& GetInventoryLoadedDelegate() override { return OnInventoryLoadedNative; } ``` #### 通知 ``` /** 给物品格子指定物品,如果返回True则代表成功指定物品 */ bool FillEmptySlotWithItem(URPGItem *NewItem); /** 物品系统数据更新回调函数 */ void NotifyInventoryItemChanged(bool bAdded, URPGItem *Item); void NotifySlottedItemChanged(FRPGItemSlot ItemSlot, URPGItem *Item); void NotifyInventoryLoaded(); /** Called when a global save game as been loaded */ void HandleSaveGameLoaded(URPGSaveGame* NewSaveGame); ``` ## 存档 ### RPGSaveGame.h 它本质上就是个用于存储游戏数据的存储对象。 ``` /** 两个存储物品信息的map */ TMap InventoryData; TMap SlottedItems; /** 用户独立id */ FString UserId; protected: /** 存储废弃物品,这些物品将不会被保存 */ TArray InventoryItems_DEPRECATED; /** 最后的保存版本 */ int32 SavedDataVersion; ``` ### RPGGameInstanceBase.h 主要的存档逻辑都写在这里,最主要的就是HandleSaveGameLoaded、WriteSaveGame、LoadOrCreateSaveGame、HandleAsyncSave函数。 #### 变量 ``` /** 给新玩家的默认物品 */ TMap DefaultInventory; /** 每个物品格子数目 */ TMap ItemSlotsPerType; /** 存档槽名称 */ FString SaveSlot; /** 系统指定的用户序号 */ int32 SaveUserIndex; /** 当前的存档对象 */ URPGSaveGame* CurrentSaveGame; /** 是否需要保存 */ bool bSavingEnabled; /** 是否处于保存中 */ bool bCurrentlySaving; /** 在保存过程中是否出现了新的保存请求*/ bool bPendingSaveRequested; ``` #### 函数 ``` /** 增加新玩家的默认物品 */ void AddDefaultInventory(URPGSaveGame* SaveGame, bool bRemoveExtra = false); /** 判断物品格子是否有效 */ bool IsValidItemSlot(FRPGItemSlot ItemSlot) const; /** 返回当前的存档对象 */ URPGSaveGame* GetCurrentSaveGame(); /** 设置当前是否可以保存 */ void SetSavingEnabled(bool bEnabled); /** 载入或者创建存档对象 */ bool LoadOrCreateSaveGame(); /** 存档的载入流程控制(如果存档类型符合则,加载物品,不符合创建新的存档对象并且加入默认物品) */ bool HandleSaveGameLoaded(USaveGame* SaveGameObject); /** Gets the save game slot and user index used for inventory saving, ready to pass to GameplayStatics save functions */ /** 取得存档槽名称与用户序号 */ void GetSaveSlotInfo(FString& SlotName, int32& UserIndex) const; /** Writes the current save game object to disk. The save to disk happens in a background thread*/ /** 将存档写入磁盘,此为异步操作 */ bool WriteSaveGame(); /** 将存档数据清空 */ void ResetSaveGame(); /** 当异步存储进行时调用(为了处理当存档过程中有了新的存档需要的情况) */ virtual void HandleAsyncSave(const FString& SlotName, const int32 UserIndex, bool bSuccess); ``` #### 委托 ``` /** 当存档载入或者重置时 */ UPROPERTY(BlueprintAssignable, Category = Inventory) FOnSaveGameLoaded OnSaveGameLoaded; /** 本地版本 */ FOnSaveGameLoadedNative OnSaveGameLoadedNative; ``` ## 备注 这个游戏中ItemSlot的设置与魂系列游戏不太一样。所以这里建议删除所有自动填充ItemSlot的逻辑,重写SetSlottedItem、FillEmptySlots、FillEmptySlotWithItem等函数。 ActionRPG首先会在继承RPGGameInstanceBase的BP_GameInstace中初始化默认存档数据,以及加载各个Slot类型的物品Asset。 ARPGPlayerControllerBase::FillEmptySlotWithItem(URPGItem* NewItem) { ## UMG逻辑 该物品系统是把 消耗品、装备、技能物品都做在一起的。 主要的Asset位于Blueprints-WidgetBP-Inventory 对于背包系统我们只需要看: ### WB_Equipment #### 设计界面 将5个WB_EquipmentSlot拖到界面中,并且将其中3个的ItemType设置为Weapon,MinEquippedItems为1。最后按顺序将SlotNumber设置为0~2。 另外两个将ItemType分别设置为Skill与Potion。 #### Construct 取得所有的WB_EquipmentSlot对象,并以此构造EquipmentListSlot TArray变量 ### WB_EquipmentSlot 主要的逻辑都写在这里 #### Construct 执行刷新RefreshItem事件=》绑定OnSlottedItemChanged事件=》设置控件上Label显示(显示为ItemType) #### RefreshItem 从控制类中执行GetSlottedItem,获取Slot信息来初始化Item变量=》再以这个信息设置Brush=》并根据Item是否获取成功来设置PotionNumberLabel是否可见。=》设置PotionNumberLabel文字 ## 本人设计思路 ### ItemType CanBeUseItemType 可使用物体 SkillItemType 技能 TokenItemType 货币 EquipmentItemType 装备 KeyItemType 钥匙 RubbishItemType 垃圾