十三、RPGPlayerControllerBase.h/cpp & InventorySystem

PlayController是每个游戏都必须有的。在ActionRPG中,RPGPlayerControllerBase负责背包系统,即背包数据以及交互方法。


十三、RPGPlayerControllerBase.h/cpp & InventorySystem_第1张图片
背包:3个物品槽,1个技能槽,1个药水槽

Data

背包中的数据有:
*TMap InventoryData : Key:物品类型 , Value: 物品在背包中的数据
*TMap SlottedItems : Key:物品槽, Value:物品类型

十三、RPGPlayerControllerBase.h/cpp & InventorySystem_第2张图片


Methods

除了数据外,剩下的就是一堆和数据交互的方法:add、remove、get、set、save、load,和各种delegate:用来将消息传递到外部系统。直接上代码(a lot of code TT,尽量加注释):

头文件:


#pragma once

#include "ActionRPG.h"
#include "GameFramework/PlayerController.h"
#include "RPGInventoryInterface.h"
#include "RPGPlayerControllerBase.generated.h"

/** Base class for PlayerController, should be blueprinted */
UCLASS()
class ACTIONRPG_API ARPGPlayerControllerBase : public APlayerController, public IRPGInventoryInterface
{
    GENERATED_BODY()

public:
    // Constructor and overrides
    ARPGPlayerControllerBase() {}
    virtual void BeginPlay() override;

// 数据
// -------------------------------------------------------------------------------------------------------------
    /** Map of all items owned by this player, from definition to data */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Inventory)
    TMap InventoryData;

    /** Map of slot, from type/num to item, initialized from ItemSlotsPerType on RPGGameInstanceBase */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Inventory)
    TMap SlottedItems;
// -------------------------------------------------------------------------------------------------------------

// 委托和蓝图事件(用于消息传递)
// -------------------------------------------------------------------------------------------------------------
    /** Delegate called when an inventory item has been added or removed */
    UPROPERTY(BlueprintAssignable, Category = Inventory)
    FOnInventoryItemChanged OnInventoryItemChanged;

    /** Native version above, called before BP delegate */
    FOnInventoryItemChangedNative OnInventoryItemChangedNative;

    /** Delegate called when an inventory slot has changed */
    UPROPERTY(BlueprintAssignable, Category = Inventory)
    FOnSlottedItemChanged OnSlottedItemChanged;

    /** Called after the inventory was changed and we notified all delegates */
    UFUNCTION(BlueprintImplementableEvent, Category = Inventory)
    void InventoryItemChanged(bool bAdded, URPGItem* Item);

    /** Called after an item was equipped and we notified all delegates */
    UFUNCTION(BlueprintImplementableEvent, Category = Inventory)
    void SlottedItemChanged(FRPGItemSlot ItemSlot, URPGItem* Item);

    /** Native version above, called before BP delegate */
    FOnSlottedItemChangedNative OnSlottedItemChangedNative;

    /** Delegate called when the inventory has been loaded/reloaded */
    UPROPERTY(BlueprintAssignable, Category = Inventory)
    FOnInventoryLoaded OnInventoryLoaded;

    /** Native version above, called before BP delegate */
    FOnInventoryLoadedNative OnInventoryLoadedNative;
// -------------------------------------------------------------------------------------------------------------

//背包操作
// -------------------------------------------------------------------------------------------------------------
    /** Adds a new inventory item, will add it to an empty slot if possible. If the item supports count you can add more than one count. It will also update the level when adding if required */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    bool AddInventoryItem(URPGItem* NewItem, int32 ItemCount = 1, int32 ItemLevel = 1, bool bAutoSlot = true);

    /** Remove an inventory item, will also remove from slots. A remove count of <= 0 means to remove all copies */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    bool RemoveInventoryItem(URPGItem* RemovedItem, int32 RemoveCount = 1);

    /** Returns all inventory items of a given type. If none is passed as type it will return all */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    void GetInventoryItems(TArray& Items, FPrimaryAssetType ItemType);

    /** Returns number of instances of this item found in the inventory. This uses count from GetItemData */
    UFUNCTION(BlueprintPure, Category = Inventory)
    int32 GetInventoryItemCount(URPGItem* Item) const;

    /** Returns the item data associated with an item. Returns false if none found */
    UFUNCTION(BlueprintPure, Category = Inventory)
    bool GetInventoryItemData(URPGItem* Item, FRPGItemData& ItemData) const;

    /** Sets slot to item, will remove from other slots if necessary. If passing null this will empty the slot */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    bool SetSlottedItem(FRPGItemSlot ItemSlot, URPGItem* Item);

    /** Returns item in slot, or null if empty */
    UFUNCTION(BlueprintPure, Category = Inventory)
    URPGItem* GetSlottedItem(FRPGItemSlot ItemSlot) const;

    /** Returns all slotted items of a given type. If none is passed as type it will return all */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    void GetSlottedItems(TArray& Items, FPrimaryAssetType ItemType, bool bOutputEmptyIndexes);

    /** Fills in any empty slots with items in inventory */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    void FillEmptySlots();
// -------------------------------------------------------------------------------------------------------------

//保存和加载背包数据
// -------------------------------------------------------------------------------------------------------------
    /** Manually save the inventory, this is called from add/remove functions automatically */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    bool SaveInventory();

    /** Loads inventory from save game on game instance, this will replace arrays */
    UFUNCTION(BlueprintCallable, Category = Inventory)
    bool LoadInventory();
// -------------------------------------------------------------------------------------------------------------


// 接口实现
// -------------------------------------------------------------------------------------------------------------
    // Implement 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;
    }
// -------------------------------------------------------------------------------------------------------------


// 内部方法
// -------------------------------------------------------------------------------------------------------------
protected:
    /** Auto slots a specific item, returns true if anything changed */
    bool FillEmptySlotWithItem(URPGItem* NewItem);

    /** Calls the inventory update callbacks */
    void NotifyInventoryItemChanged(bool bAdded, URPGItem* Item);
    void NotifySlottedItemChanged(FRPGItemSlot ItemSlot, URPGItem* Item);
    void NotifyInventoryLoaded();
// -------------------------------------------------------------------------------------------------------------
};

源文件:

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

#include "RPGPlayerControllerBase.h"
#include "RPGCharacterBase.h"
#include "RPGGameInstanceBase.h"
#include "RPGSaveGame.h"
#include "Items/RPGItem.h"

/** 往背包里添加物品,
1) 如果是全新的物品,且auto slot为true,则添加到空的道具槽中
2) 如果道具支持叠加,则该道具持有数+1
3) 会更新道具等级
4) 如果添加道具成功,则保存背包数据
*/
bool ARPGPlayerControllerBase::AddInventoryItem(URPGItem* NewItem, int32 ItemCount, int32 ItemLevel, bool bAutoSlot)
{
    bool bChanged = false;
    if (!NewItem)
    {
        UE_LOG(LogActionRPG, Warning, TEXT("AddInventoryItem: Failed trying to add null item!"));
        return false;
    }

    if (ItemCount <= 0 || ItemLevel <= 0)
    {
        UE_LOG(LogActionRPG, Warning, TEXT("AddInventoryItem: Failed trying to add item %s with negative count or level!"), *NewItem->GetName());
        return false;
    }

    // Find current item data, which may be empty
    FRPGItemData OldData;
    GetInventoryItemData(NewItem, OldData);

    // Find modified data
    FRPGItemData NewData = OldData;
    NewData.UpdateItemData(FRPGItemData(ItemCount, ItemLevel), NewItem->MaxCount, NewItem->MaxLevel);

    if (OldData != NewData)
    {
        // If data changed, need to update storage and call callback
        InventoryData.Add(NewItem, NewData);
        NotifyInventoryItemChanged(true, NewItem);
        bChanged = true;
    }

    if (bAutoSlot)
    {
        // Slot item if required
        bChanged |= FillEmptySlotWithItem(NewItem);
    }

    if (bChanged)
    {
        // If anything changed, write to save game
        SaveInventory();
        return true;
    }
    return false;
}
/** 移除道具
*/
bool ARPGPlayerControllerBase::RemoveInventoryItem(URPGItem* RemovedItem, int32 RemoveCount)
{
    if (!RemovedItem)
    {
        UE_LOG(LogActionRPG, Warning, TEXT("RemoveInventoryItem: Failed trying to remove null item!"));
        return false;
    }

    // Find current item data, which may be empty
    FRPGItemData NewData;
    GetInventoryItemData(RemovedItem, NewData);

    if (!NewData.IsValid())
    {
        // Wasn't found
        return false;
    }

    // If RemoveCount <= 0, delete all
      // 计算移除后的数量
    if (RemoveCount <= 0)
    {
        NewData.ItemCount = 0;
    }
    else
    {
        NewData.ItemCount -= RemoveCount;
    }

    if (NewData.ItemCount > 0)
    {
        // Update data with new count
//注意: TMap的Key是唯一的,因此,此处的add实际上是替换掉过去的Value
        InventoryData.Add(RemovedItem, NewData);
    }
    else
    {
        // Remove item entirely, make sure it is unslotted
        InventoryData.Remove(RemovedItem);

        for (TPair& Pair : SlottedItems)
        {
            if (Pair.Value == RemovedItem)
            {
                Pair.Value = nullptr;
                NotifySlottedItemChanged(Pair.Key, Pair.Value);
            }
        }
    }

    // If we got this far, there is a change so notify and save
    NotifyInventoryItemChanged(false, RemovedItem);

    SaveInventory();
    return true;
}
/** 返回背包中所有给定type类型的item*/
void ARPGPlayerControllerBase::GetInventoryItems(TArray& Items, FPrimaryAssetType ItemType)
{
    for (const TPair& Pair : InventoryData)
    {
        if (Pair.Key)
        {
            FPrimaryAssetId AssetId = Pair.Key->GetPrimaryAssetId();

            // Filters based on item type
            if (AssetId.PrimaryAssetType == ItemType || !ItemType.IsValid())
            {
                Items.Add(Pair.Key);
            }
        }   
    }
}

bool ARPGPlayerControllerBase::SetSlottedItem(FRPGItemSlot ItemSlot, URPGItem* Item)
{
    // Iterate entire inventory because we need to remove from old slot
    bool bFound = false;
    for (TPair& Pair : SlottedItems)
    {
//将道具添加到指定slot
        if (Pair.Key == ItemSlot)
        {
            // Add to new slot
            bFound = true;
            Pair.Value = Item;
            NotifySlottedItemChanged(Pair.Key, Pair.Value);
        }
//如果再别slot发现了该道具,则从slot中移除
        else if (Item != nullptr && Pair.Value == Item)
        {
            // If this item was found in another slot, remove it
            Pair.Value = nullptr;
            NotifySlottedItemChanged(Pair.Key, Pair.Value);
        }
    }
//如果找到slot,保存改变后的背包数据
    if (bFound)
    {
        SaveInventory();
        return true;
    }

    return false;
}
/** 获得道具的数量*/
int32 ARPGPlayerControllerBase::GetInventoryItemCount(URPGItem* Item) const
{
    const FRPGItemData* FoundItem = InventoryData.Find(Item);

    if (FoundItem)
    {
        return FoundItem->ItemCount;
    }
    return 0;
}
/** 获得道具的背包数据*/
bool ARPGPlayerControllerBase::GetInventoryItemData(URPGItem* Item, FRPGItemData& ItemData) const
{
    const FRPGItemData* FoundItem = InventoryData.Find(Item);

    if (FoundItem)
    {
        ItemData = *FoundItem;
        return true;
    }
    ItemData = FRPGItemData(0, 0);
    return false;
}
/** 获得道具槽内的道具*/
URPGItem* ARPGPlayerControllerBase::GetSlottedItem(FRPGItemSlot ItemSlot) const
{
    URPGItem* const* FoundItem = SlottedItems.Find(ItemSlot);

    if (FoundItem)
    {
        return *FoundItem;
    }
    return nullptr;
}
/** 返回给定type的所有slot中的道具*/
void ARPGPlayerControllerBase::GetSlottedItems(TArray& Items, FPrimaryAssetType ItemType, bool bOutputEmptyIndexes)
{
    for (TPair& Pair : SlottedItems)
    {
        if (Pair.Key.ItemType == ItemType || !ItemType.IsValid())
        {
            Items.Add(Pair.Value);
        }
    }
}
/** 自动填满空的slot*/
void ARPGPlayerControllerBase::FillEmptySlots()
{
    bool bShouldSave = false;
    for (const TPair& Pair : InventoryData)
    {
        bShouldSave |= FillEmptySlotWithItem(Pair.Key);
    }

    if (bShouldSave)
    {
        SaveInventory();
    }
}
/** 保存背包数据*/
bool ARPGPlayerControllerBase::SaveInventory()
{
    UWorld* World = GetWorld();
    URPGGameInstanceBase* GameInstance = World ? World->GetGameInstance() : nullptr;

    if (!GameInstance)
    {
        return false;
    }

    URPGSaveGame* CurrentSaveGame = GameInstance->GetCurrentSaveGame();
    if (CurrentSaveGame)
    {
        // Reset cached data in save game before writing to it
        CurrentSaveGame->InventoryData.Reset();
        CurrentSaveGame->SlottedItems.Reset();

        for (const TPair& ItemPair : InventoryData)
        {
            FPrimaryAssetId AssetId;

            if (ItemPair.Key)
            {
                AssetId = ItemPair.Key->GetPrimaryAssetId();
                CurrentSaveGame->InventoryData.Add(AssetId, ItemPair.Value);
            }
        }

        for (const TPair& SlotPair : SlottedItems)
        {
            FPrimaryAssetId AssetId;

            if (SlotPair.Value)
            {
                AssetId = SlotPair.Value->GetPrimaryAssetId();
            }
            CurrentSaveGame->SlottedItems.Add(SlotPair.Key, AssetId);
        }

        // Now that cache is updated, write to disk
        GameInstance->WriteSaveGame();
        return true;
    }
    return false;
}
/** 加载背包数据*/
bool ARPGPlayerControllerBase::LoadInventory()
{
    InventoryData.Reset();
    SlottedItems.Reset();

    // Fill in slots from game instance
    UWorld* World = GetWorld();
    URPGGameInstanceBase* GameInstance = World ? World->GetGameInstance() : nullptr;

    if (!GameInstance)
    {
        return false;
    }

    for (const TPair& Pair : GameInstance->ItemSlotsPerType)
    {
        for (int32 SlotNumber = 0; SlotNumber < Pair.Value; SlotNumber++)
        {
            SlottedItems.Add(FRPGItemSlot(Pair.Key, SlotNumber), nullptr);
        }
    }

    URPGSaveGame* CurrentSaveGame = GameInstance->GetCurrentSaveGame();
    URPGAssetManager& AssetManager = URPGAssetManager::Get();
    if (CurrentSaveGame)
    {
        // Copy from save game into controller data
        bool bFoundAnySlots = false;
        for (const TPair& ItemPair : CurrentSaveGame->InventoryData)
        {
            URPGItem* LoadedItem = AssetManager.ForceLoadItem(ItemPair.Key);

            if (LoadedItem != nullptr)
            {
                InventoryData.Add(LoadedItem, ItemPair.Value);
            }
        }

        for (const TPair& SlotPair : CurrentSaveGame->SlottedItems)
        {
            if (SlotPair.Value.IsValid())
            {
                URPGItem* LoadedItem = AssetManager.ForceLoadItem(SlotPair.Value);
                if (GameInstance->IsValidItemSlot(SlotPair.Key) && LoadedItem)
                {
                    SlottedItems.Add(SlotPair.Key, LoadedItem);
                    bFoundAnySlots = true;
                }
            }
        }

        if (!bFoundAnySlots)
        {
            // Auto slot items as no slots were saved
            FillEmptySlots();
        }

        NotifyInventoryLoaded();

        return true;
    }

    // Load failed but we reset inventory, so need to notify UI
    NotifyInventoryLoaded();

    return false;
}

//内部函数
//------------------------------------------------------------------------------------------------------------
bool ARPGPlayerControllerBase::FillEmptySlotWithItem(URPGItem* NewItem)
{
    // Look for an empty item slot to fill with this item
    FPrimaryAssetType NewItemType = NewItem->GetPrimaryAssetId().PrimaryAssetType;
    FRPGItemSlot EmptySlot;
    for (TPair& Pair : SlottedItems)
    {
        if (Pair.Key.ItemType == NewItemType)
        {
            if (Pair.Value == NewItem)
            {
                // Item is already slotted
                return false;
            }
            else if (Pair.Value == nullptr && (!EmptySlot.IsValid() || EmptySlot.SlotNumber > Pair.Key.SlotNumber))
            {
                // We found an empty slot worth filling
                EmptySlot = Pair.Key;
            }
        }
    }

    if (EmptySlot.IsValid())
    {
        SlottedItems[EmptySlot] = NewItem;
        NotifySlottedItemChanged(EmptySlot, NewItem);
        return true;
    }

    return false;
}

void ARPGPlayerControllerBase::NotifyInventoryItemChanged(bool bAdded, URPGItem* Item)
{
    // Notify native before blueprint
    OnInventoryItemChangedNative.Broadcast(bAdded, Item);
    OnInventoryItemChanged.Broadcast(bAdded, Item);

    // Call BP update event
    InventoryItemChanged(bAdded, Item);
}

void ARPGPlayerControllerBase::NotifySlottedItemChanged(FRPGItemSlot ItemSlot, URPGItem* Item)
{
    // Notify native before blueprint
    OnSlottedItemChangedNative.Broadcast(ItemSlot, Item);
    OnSlottedItemChanged.Broadcast(ItemSlot, Item);

    // Call BP update event
    SlottedItemChanged(ItemSlot, Item);
}

void ARPGPlayerControllerBase::NotifyInventoryLoaded()
{
    // Notify native before blueprint
    OnInventoryLoadedNative.Broadcast();
    OnInventoryLoaded.Broadcast();
}
//----------------------------------------------------------------------------------------------------------------------
void ARPGPlayerControllerBase::BeginPlay()
{
    // Load inventory off save game before starting play
    LoadInventory();

    Super::BeginPlay();
}

你可能感兴趣的:(十三、RPGPlayerControllerBase.h/cpp & InventorySystem)