十三、RPGPlayerControllerBase.h/cpp & InventorySystem

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


背包:3个物品槽,1个技能槽,1个药水槽

Data

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


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<URPGItem*, FRPGItemData> InventoryData;

    /** Map of slot, from type/num to item, initialized from ItemSlotsPerType on RPGGameInstanceBase */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Inventory)
    TMap<FRPGItemSlot, URPGItem*> 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<URPGItem*>& 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<URPGItem*>& 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<URPGItem*, FRPGItemData>& GetInventoryDataMap() const override
    {
        return InventoryData;
    }
    virtual const TMap<FRPGItemSlot, URPGItem*>& 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<FRPGItemSlot, URPGItem*>& 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<URPGItem*>& Items, FPrimaryAssetType ItemType)
{
    for (const TPair<URPGItem*, FRPGItemData>& 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<FRPGItemSlot, URPGItem*>& 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<URPGItem*>& Items, FPrimaryAssetType ItemType, bool bOutputEmptyIndexes)
{
    for (TPair<FRPGItemSlot, URPGItem*>& Pair : SlottedItems)
    {
        if (Pair.Key.ItemType == ItemType || !ItemType.IsValid())
        {
            Items.Add(Pair.Value);
        }
    }
}
/** 自动填满空的slot*/
void ARPGPlayerControllerBase::FillEmptySlots()
{
    bool bShouldSave = false;
    for (const TPair<URPGItem*, FRPGItemData>& Pair : InventoryData)
    {
        bShouldSave |= FillEmptySlotWithItem(Pair.Key);
    }

    if (bShouldSave)
    {
        SaveInventory();
    }
}
/** 保存背包数据*/
bool ARPGPlayerControllerBase::SaveInventory()
{
    UWorld* World = GetWorld();
    URPGGameInstanceBase* GameInstance = World ? World->GetGameInstance<URPGGameInstanceBase>() : 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<URPGItem*, FRPGItemData>& ItemPair : InventoryData)
        {
            FPrimaryAssetId AssetId;

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

        for (const TPair<FRPGItemSlot, URPGItem*>& 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<URPGGameInstanceBase>() : nullptr;

    if (!GameInstance)
    {
        return false;
    }

    for (const TPair<FPrimaryAssetType, int32>& 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<FPrimaryAssetId, FRPGItemData>& ItemPair : CurrentSaveGame->InventoryData)
        {
            URPGItem* LoadedItem = AssetManager.ForceLoadItem(ItemPair.Key);

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

        for (const TPair<FRPGItemSlot, FPrimaryAssetId>& 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<FRPGItemSlot, URPGItem*>& 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();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351