Timothy Marks Memoji

Timothy Marks

@imothee

Hi, I'm Tim, an engineer, founder, leader and hacker. Here you'll find my articles, apps, projects and open source stuff

BP_Player_Controller_Level

Serfs Up 02 - Inventory

Building an Inventory in Unreal Part One - Containers, Interactables and Item Actors

Inventory Part One - Containers, Interactables and Item Actors

Almost every game will end up dealing with some kind of inventory or container functionality alongside interactable items in the game. These inventory systems may range just a set of booleans for each item you've picked up to a slot system (Valheim etc), grid system (Diablo etc) or a more traditional weight based inventory (Skyrim, Witcher 3 etc).

In this tutorial we will focus on the weight based system as it fits the Serfs Up theme but this base pattern can be fairly easily extended to suit your individual needs.

Step 1 - Define the Interfaces

If we think about mapping out how a container works in the real world we'd probably end up with something like this, which to your probable shock will end up becoming the interface definition for a container :)

  • AddItem(Item)
  • AddItems(Items)
  • RemoveItem(Item, Amount)
  • RemoveItems(Items)
  • GetAllItems()
  • GetItemMap() // return the itemname -> item for all the items in the container
  • Clear() // remove all items
  • GetWeight() // Get the weight of all the items in the container

Then if we think about the items inside the container, we need some way to pick them up or interact with them when they exist in the real world (think Skyrim or Fallout where you loot everything not nailed down).

  • GetName() // We probably want the name of the item at some point to show in the UI or our inventory
  • GetInteractionText() // So we can show what kind of interaction is possible
  • IsTurnedOn() // In case the item has an on/off state (like a torch or an Ipod)
  • Interact(bool Completed) // Completed here is whether the keypress has started or completed. A press and hold might be different than a completed press for example.

Lastly, we need to have an interface attached to one of our player objects to return the inventory.

  • GetInventoryContainer()

Step 2 - Define the Types

We need to be able to define and store all the details associated with an "Item" that can go in a container. To that end we will need to define a number of Enums and Structs that make up the composition of currency and items.

Source/SurfsUp/SurfsUpTypes.h

We need to first define the Enums that will represent options within our Item structs.

// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
// Let's make Currencies more generic so if we have Gold and Gems in the future we can support it
UENUM()
enum class ECurrencyType : uint8
{
  Gold
};

// Here's all the places you can equip items in our game, you can easily add and remove what you need
UENUM()
enum class EEquipmentSlot : uint8
{
  None,
  OneHandItem,
  TwoHandItem,
  Head,
  Torso,
  Legs,
  Hands,
  Feet,
  Bag
};

// Here's all the different equipment types
UENUM()
enum class EEquipmentType : uint8
{
  None,
  // Tools
  Broom,
  Axe,
  // Wearables
  Clothing,
};

UENUM()
enum class EItemType : uint8
{
  Equipment,
  Consumable,
  Resource,
  Literary,
  Quest,
  Other
};

Now that we have our Enums setup we can create the different Item structs to store a representation of an item in our game.

// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
// This represents an amount of a currency type
// You can store this in a container, on the player or in a bank for example
USTRUCT(BlueprintType)
struct FCurrencyValue
{
  GENERATED_USTRUCT_BODY()

  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  ECurrencyType Currency;

  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  float Value;
};

// This represents the cost of an item, uses an array in case you can purchase using multiple currencies
USTRUCT(BlueprintType)
struct FCurrencyCost
{
  GENERATED_USTRUCT_BODY()

  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  TArray<FCurrencyValue> CostCurrencies;
};

// This is the base representation of an item
// Does not include any of the implemented data (like an amount, durability, carges etc)
USTRUCT(BlueprintType)
struct FItemBase
{
  GENERATED_USTRUCT_BODY()

  /* FItemBase */
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  FString Name;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  FString Description;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  EItemType ItemType;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  EEquipmentType EquipmentType;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  EEquipmentSlot EquipmentSlot;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  bool Stackable;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  float Weight;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  UTexture2D *Icon;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  UClass *ItemClass;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  TArray<UStaticMesh *> StaticMeshes;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  TArray<USkeletalMesh *> SkeletalMeshes;
};

// Here we have an instanced Item which extends FItemBase but adds instance details
// We could have embedded FItemBase instead and has a property FItembase ItemBase
// But it feels cleaner to not have it nested in my opinion
// Also note the GUID, this will be generated per instance of the Item in the world.
// The GUID lets us identify a specific copy of the Item in our inventory
USTRUCT(BlueprintType)
struct FItem : public FItemBase
{
  GENERATED_USTRUCT_BODY()

  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  uint8 Amount;
  UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
  FGuid Guid;

  FItem()
  {
    Guid = FGuid::NewGuid();
  }

  FItem(FItemBase ItemBase, uint8 ItemAmount)
  {
    Name = ItemBase.Name;
    Description = ItemBase.Description;
    ItemType = ItemBase.ItemType;
    EquipmentType = ItemBase.EquipmentType;
    EquipmentSlot = ItemBase.EquipmentSlot;
    Stackable = ItemBase.Stackable;
    Weight = ItemBase.Weight;
    Icon = ItemBase.Icon;
    ItemClass = ItemBase.ItemClass;
    StaticMeshes = ItemBase.StaticMeshes;
    SkeletalMeshes = ItemBase.SkeletalMeshes;
    Amount = ItemAmount;
    Guid = FGuid::NewGuid();
  }

  bool operator==(const FItem &S) const
  {
    return Guid == S.Guid;
  }
};

So the next question is, how do we represent all the Item data in our app? There's an awesome [article](Data-driven Design in Unreal · ben🌱ui) talking about the different possibilities and honestly they're all good and bad. So since there isn't one ideal method this tutorial will use DataTables since they are the most common and therefore easiest to find demo's, tutorials and details on.

To use a DataTable we need to create the FTableRowBase definition so we can start adding all the item content.

// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
USTRUCT(BlueprintType)
struct FItemInfo : public FTableRowBase
{
  GENERATED_BODY()

  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  FItemBase Item;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
  FCurrencyCost CurrencyCost;
};

Once you have the types defined, go ahead and hit compile and we'll be able to start adding some dummy data to our DataTable.

Step 3 - Create some DataTable Data

Create a new DataTable DT_Items under SerfsUps/DataTables and for the Row Structure choose ItemInfo (from SerfsUpTypes.h). If you can't find the Row type, make sure your code successfully compiled.

Open the DataTable and you'll see a pretty self-explanatory window. The top section is your rows of data and clicking on one populate the row editor at the bottom. Let's hit +Add and add two items to our table.

Segue ideally you'd have some meshes you could use to setup the base items here. I will be using the Advanced Village Pack if you want to grab it, install it and follow along

The first row we'll add will be for a Rusty Axe. You can set the Row Name by double clicking on the name in the row. The rest of the data is fairly simple, the only thing we haven't set yet is the Item Class (since we haven't created a class yet). The Static mesh points to the SM_Axe from the asset pack and for now I'm using the UE logo as a placeholder icon.

For the second row let's add something stackable, in this case let's make a Tomato that will stack in the inventory. Same structure as before but in this case it's stackable and a consumable item (we'll get to consumables later).

Now we have two items setup and ready to be added to our game! So let's make an ItemActor class so we can add these items to our level!

Step 4 - Interfacing

Now we get to create our three interfaces. This will define how the rest of the game interacts with Items and Containers (whether your inventory or a loot chest).

Public/Interfaces/ContainerInterface.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SerfsUp/SerfsUpTypes.h"
#include "ContainerInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UContainerInterface : public UInterface
{
  GENERATED_BODY()
};

/**
 *
 */
class SERFSUP_API IContainerInterface
{
  GENERATED_BODY()

public:
  // Add Items
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  void AddItem(FItem Item);
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  void AddItems(const TArray<FItem> &Items);

  // Remove Items
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  bool RemoveItem(FItem Item);

  // Get Items
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  TMap<FString, uint8> GetItemMap();
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  TArray<FItem> GetAllItems();
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  TArray<FItem> GetItems(EItemType ItemType);

  // Reset Inventory
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Items)
  void ClearItems();

  // Weight
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = State)
  float GetWeight();
};
Public/Interfaces/InteractableInterface.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InteractableInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UInteractableInterface : public UInterface
{
  GENERATED_BODY()
};

/**
 *
 */
class SERFSUP_API IInteractableInterface
{
  GENERATED_BODY()

public:
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
  FString GetName();

  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
  FString GetInteractionText();

  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
  bool IsTurnedOn();

  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
  void Interact(bool Completed, APlayerController *PlayerController);
};
Public/Interfaces/PlayerInterface.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SerfsUp/SerfsUpTypes.h"
#include "Components/ContainerComponent.h"
#include "PlayerInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UPlayerInterface : public UInterface
{
  GENERATED_BODY()
};

/**
 *
 */
class SERFSUP_API IPlayerInterface
{
  GENERATED_BODY()

public:
  /* Inventory */
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Inventory)
  UContainerComponent *GetInventoryContainer();
};

Step 5 - Trust me, I'm an Actor

In an Unreal level you'll mostly be dealing with one of two primitives, a static mesh and an Actor.

The major difference here is that actors can tick, have logic embedded in them and do "stuff". The majority of levels will likely contain mainly static meshes - think walls, floors, rocks, pathways, fences etc. They just exist (hence static) and are just there for aesthetics, level design or collision testing. Actors on the other hand have a state and are tracked and can change by the game engine.

We could add our axe and tomato to the level as just static meshes but then we couldn't pick them up, interact with them or do anything fun. So we have to make an Item Actor class that lets us doing Item like things.

There are two main ways we can create an actor to represent our base items, creating a Blueprint per item Actor (ie. BP_Rusty_Axe, BP_Tomato) or having a single shared blueprint that sets up the right model during construction. If you went the one BP per item route, you could make a base class that contains some of the shared logic or copy it into each one. Going the single Blueprint route doesn't stop you from extending it and creating a custom BP if you needed to add item specific logic so let's go that route and create our ItemActor.

By now you should know the drill but we're going to create the ItemActor class that extends from an Actor. This ItemActor will implement the InteractableInterface defined above so that the player can interact with it (and pick it up and put it in our inventory like the loot hoarder we are).

Public/Interactable/ItemActor.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interfaces/InteractableInterface.h"
#include "SerfsUp/SerfsUpTypes.h"
#include "ItemActor.generated.h"

UCLASS()
class SERFSUP_API AItemActor : public AActor, public IInteractableInterface
{
  GENERATED_BODY()

  // Sets default values for this actor's properties
  AItemActor();

  virtual void OnConstruction(const FTransform &Transform) override;

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:
  // Called every frame
  virtual void Tick(float DeltaTime) override;

protected:
  UPROPERTY(VisibleAnywhere, Category = "Mesh")
  // Stores the reference to the Mesh that the actor will load
  UStaticMeshComponent *MeshComp;

public:
  // ItemData needs to be set to load the item, this points to a row in DT_Items
  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
  FDataTableRowHandle ItemHandle;
  // If the item is stackable, this is the amount of them
  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
  uint8 Amount;
  // Loaded from the DT_Items row, holds the ItemInfo for the item
  UPROPERTY(BlueprintReadOnly, Category = Item)
  FItemInfo ItemInfo;

  // Mesh
  /* Public accessor to the mesh component. With FORCEINLINE we are allowed to define the function in the header, use this only for simple accessors! */
  FORCEINLINE UStaticMeshComponent *GetMeshComponent() const
  {
    return MeshComp;
  }

public:
  // InteractableInterface
  FString GetName_Implementation() override;
  FString GetInteractionText_Implementation() override;
  bool IsTurnedOn_Implementation() override;
  void Interact_Implementation(bool Completed, APlayerController *PlayerController) override;
};
Private/Interactable/ItemActor.cpp
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#include "Interactable/ItemActor.h"
#include "Components/StaticMeshComponent.h"
#include "Interfaces/ContainerInterface.h"
#include "Interfaces/PlayerInterface.h"

// Sets default values
AItemActor::AItemActor()
{
  // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
  RootComponent = MeshComp;
}

void AItemActor::OnConstruction(const FTransform &Transform)
{
  // This is the OnConstruction event of the blueprint. This needs to be where you load the mesh if you don't want weird behavior in the editor
  // Get the ItemInfo from DT_Items based on the ItemHandle
  FItemInfo *Info = ItemHandle.GetRow<FItemInfo>(FString("Item Info"));
  if (Info)
  {
    ItemInfo = *Info;
    // If we have static meshes in the ItemInfo we set the mesh to be the first one
    if (!ItemInfo.Item.StaticMeshes.IsEmpty())
    {
      MeshComp->SetStaticMesh(ItemInfo.Item.StaticMeshes[0]);
    }
  }
  else
  {
    UE_LOG(LogTemp, Warning, TEXT("No ItemInfo %s"), Info);
  }
}

// Called when the game starts or when spawned
void AItemActor::BeginPlay()
{
  Super::BeginPlay();
}

// Called every frame
void AItemActor::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);
}

/* UsableItemInterface */
FString AItemActor::GetName_Implementation()
{
  // Return the item name from the ItemInfo
  return ItemInfo.Item.Name;
}

FString AItemActor::GetInteractionText_Implementation()
{
  // Default to take for most items
  return FString("Take");
}

bool AItemActor::IsTurnedOn_Implementation()
{
  // Generic items can't be turned on/off
  return false;
}

void AItemActor::Interact_Implementation(bool Completed, APlayerController *PlayerController)
{
  if (!Completed)
  {
    return;
  }

  // Get the InventoryContainer from the Player
  UContainerComponent *InventoryContainer = IPlayerInterface::Execute_GetInventoryContainer(PlayerController);

  if (!InventoryContainer)
  {
    return;
  }

  FItem Item = FItem(ItemInfo.Item, Amount);

  // Add the item to our InventoryContainer
  IContainerInterface::Execute_AddItem(InventoryContainer, Item);
  // Destroy the item for now (since we picked it up)
  // If you wanted the item to respawn we could add a respawnable flag and hide the actor instead
  Destroy();
}

Let's save and compile and test things out! To make this work we need to add an ItemActor to our level and setup the item details.

In Place Actors (Window/Place Actors if it's not visible) search for ItemActor and drag it into our level. By default no mesh will load but we can fix that by scrolling down through the details of the object until we get to the Item group/

Under Item Handle, select the DT_Items Data Table and select either row name and amount 1. If you did it correctly you'll see the ItemActor render with the Axe mesh. Axe-cellant.

If you want to see something cool, change the row to the tomato and you'll immediately see the ItemActor re-render with the Tomato mesh. Every time the item details change, the OnConstruction event runs and switches out the mesh.

Step 6 - The Container Component

So to store items we need a container, this container can then represent your inventory, the loot in a chest, storage in your wardrobe or any collection of items in your game. This could be a slot based container (where each item or stack must sit in a specific slot) or a weight system (like this one) or really any way of storing and getting items.

Public/Components/ContainerComponent.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Interfaces/ContainerInterface.h"
#include "ContainerComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnItemAdded, FItem, ItemAdded);

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class SERFSUP_API UContainerComponent : public UActorComponent, public IContainerInterface
{
  GENERATED_BODY()

public:
  // Sets default values for this component's properties
  UContainerComponent();

  FOnItemAdded OnItemAdded;

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:
  // Called every frame
  virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;

public:
  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Settings)
  bool StoreCurrencyAsItem;
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Item)
  TArray<FItem> Items;
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Item)
  TArray<FCurrencyValue> Currency;

public:
  /* ContainerInterface */
  void AddItem_Implementation(FItem Item) override;
  bool RemoveItem_Implementation(FItem Item) override;
  TArray<FItem> GetAllItems_Implementation() override;
  TArray<FItem> GetItems_Implementation(EItemType ItemType) override;
  void ClearItems_Implementation() override;
  float GetWeight_Implementation() override;
};
Private/Components/ContainerComponent.cpp
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#include "Components/ContainerComponent.h"
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"

// Sets default values for this component's properties
UContainerComponent::UContainerComponent()
{
  // Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
  // off to improve performance if you don't need them.
  PrimaryComponentTick.bCanEverTick = true;
}

// Called when the game starts
void UContainerComponent::BeginPlay()
{
  Super::BeginPlay();
}

// Called every frame
void UContainerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
  Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
}

// ContainerInterface
void UContainerComponent::AddItem_Implementation(FItem Item)
{
  UE_LOG(LogTemp, Warning, TEXT("Adding Item to Inventory %s"), *Item.Name);

  // Let's keep it super simple for now, we're not going to check for currency or stacking items, just add whatever we pickup
  Items.Add(Item);

  OnItemAdded.Broadcast(Item);
}

bool UContainerComponent::RemoveItem_Implementation(FItem Item)
{
  // Try to remove the item from the array
  int32 Removed = Items.Remove(Item);
  if (Removed > 0)
  {
    return true;
  }
  return false;
}

TArray<FItem> UContainerComponent::GetAllItems_Implementation()
{
  return Items;
}

TArray<FItem> UContainerComponent::GetItems_Implementation(EItemType ItemType)
{
  return Items.FilterByPredicate([&ItemType](const FItem &I)
                                 { return I.ItemType == ItemType; });
}

void UContainerComponent::ClearItems_Implementation()
{
  Items.Empty();
  return;
}

float UContainerComponent::GetWeight_Implementation()
{
  float Weight = 0;
  for (int i = 0; i < Items.Num(); i++)
  {
    Weight += Items[i].Weight * Items[i].Amount;
  }
  return Weight;
}

Step 7 - The PlayerInterface

We could store the inventory on the PlayerCharacter but if we change characters or kill the actor and respawn one we would lose all the items in the inventory. There are two strategies I would recommend depending on the complexity of your Game/Player. The first is to have all the components and the interface mounted to the PlayerController directly. The second is to have a PlayerManagerComponent that stores and isolates the Player content and functions which is cleaner if you want to detach/attach or move the manager around and have the PlayerInterface mounted to the PlayerController and proxy through to the PlayerManager. For simplicity and cleanliness let's just put all this logic in the PlayerController for now.

Let's start by generating a new C++ class that extends PlayerController and call it SerfsUpPlayerController.

Public/Player/SerfsUpPlayerController.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "Components/ContainerComponent.h"
#include "Interfaces/PlayerInterface.h"
#include "SerfsUpPlayerController.generated.h"

/**
 *
 */
UCLASS()
class SERFSUP_API ASerfsUpPlayerController : public APlayerController, public IPlayerInterface
{
    GENERATED_BODY()

    // Constructor
    ASerfsUpPlayerController();

public:
    /* Container Component */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Container)
    class UContainerComponent *InventoryContainer;

public:
    /* Player Interface */
    UContainerComponent *GetInventoryContainer_Implementation() override;
};
Private/Player/SerfsUpPlayerController.cpp
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#include "Player/SerfsUpPlayerController.h"

ASerfsUpPlayerController::ASerfsUpPlayerController()
{
  // Setup the Inventory
  InventoryContainer = CreateDefaultSubobject<UContainerComponent>(TEXT("ContainerComp"));
}

/* Player Interface */
UContainerComponent *ASerfsUpPlayerController::GetInventoryContainer_Implementation()
{
  return InventoryContainer;
}

Since we will need to pass through a lot of input assets and settings to our PlayerController we also want to make an empty Blueprint for the PlayerController so lets create SerfsUp/BP_SerfsUpPlayerController and extend the SerfsUpPlayerController class. If you can't find the class remember to compile your C++ code and check for any errors.

Once you have your blueprint created we need to set our BP_SerfsUpPlayerController as the default PlayerController in our GameMode so let's open

Private/Game/SerfsUpGameMode.cpp

To this we're going to add the following code to the constructor.

AThreadsGameMode::AThreadsGameMode()
{
...

    // Setup the PlayerController
    static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerBPClass(TEXT("/Game/SerfsUp/Core/BP_SerfsUpPlayerController"));
    if (PlayerControllerBPClass.Class != NULL)
    {
        PlayerControllerClass = PlayerControllerBPClass.Class;
    }

...
}

Interlude

So if you hit compile and save on the C++ code and your Blueprint and run your game you should now see our new classes being loaded into the Level.

It won't do anything just yet but we've done a bunch of C++ code to setup our ability to start writing actual game code.

So to recap, so far we've create

  • 3 interfaces for a player, a container and an interactable object
  • Created all our Item and Currency types
  • Created a DataTable holding two items for now
  • Created our ItemActor that we could add to our level and set the Item type
  • Setup a PlayerController and set it as the default PlayerController for our GameMode

Pretty impressive progress so far!

Step 8 - Interacting

There's probably many many of ways to do this (like everything I've run into while learning game design) but one of the patterns that really resonated with me was creating an InteractionComponent inside of the PlayerController. We'll also need to setup some Input actions and bind them in the PlayerController as well.

Public/Components/InteractionComponent.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionComponent.generated.h"

// This is our event we broadcast when the actor we're focused on changes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractableActorChanged, AActor *, Actor);

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class SERFSUP_API UInteractionComponent : public UActorComponent
{
  GENERATED_BODY()

public:
  // Sets default values for this component's properties
  UInteractionComponent();

  FOnInteractableActorChanged OnInteractableActorChanged;

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:
  // Called every frame
  virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;

public:
  // This is how close we need to be to be able to interact
  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Settings)
  float MaxInteractionDistance;
  // This lets our player call interact on our focused actor
  UFUNCTION()
  void Interact(bool Completed, APlayerController *PlayerController);

private:
  AActor *GetActorInView() const;
  AActor *FocusedInteractableActor;
};
Private/Components/InteractionComponent.cpp
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/

#include "Components/InteractionComponent.h"
#include "Interfaces/InteractableInterface.h"

// Sets default values for this component's properties
UInteractionComponent::UInteractionComponent()
{
  // Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
  // off to improve performance if you don't need them.
  PrimaryComponentTick.bCanEverTick = true;

  MaxInteractionDistance = 500;
}

// Called when the game starts
void UInteractionComponent::BeginPlay()
{
  Super::BeginPlay();
}

// Called every frame
void UInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
  Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

  AActor *Actor = GetActorInView();

  if (FocusedInteractableActor != Actor)
  {
    // Focused object has updated
    if (Actor && Actor->Implements<UInteractableInterface>())
    {
      // New focus is interactable
      FocusedInteractableActor = Actor;
    }
    else
    {
      // Focus is not interactor so set to null
      FocusedInteractableActor = nullptr;
    }
    // Broadcast the updated actor
    OnInteractableActorChanged.Broadcast(FocusedInteractableActor);
  }
}

/*
 * IInteractableInterface
 */
void UInteractionComponent::Interact(bool Completed, APlayerController *PlayerController)
{
  if (FocusedInteractableActor && FocusedInteractableActor->Implements<UInteractableInterface>())
  {
    // Pass the interact call through to the interactable item to handle
    IInteractableInterface::Execute_Interact(FocusedInteractableActor, Completed, PlayerController);
  }
}

// Internal
AActor *UInteractionComponent::GetActorInView() const
{
  FVector CamLoc;
  FRotator CamRot;

  APlayerController *Controller = Cast<APlayerController>(GetOwner());

  if (Controller == nullptr)
    return nullptr;

  APawn *PawnOwner = Controller->GetPawnOrSpectator();

  if (PawnOwner == nullptr)
    return nullptr;

  // Get the viewpoint
  Controller->GetPlayerViewPoint(CamLoc, CamRot);
  const FVector TraceStart = CamLoc;
  const FVector Direction = CamRot.Vector();
  const FVector TraceEnd = TraceStart + (Direction * MaxInteractionDistance);

  FCollisionQueryParams TraceParams(TEXT("TraceUsableActor"), true, PawnOwner);
  TraceParams.bReturnPhysicalMaterial = false;

  /* Not tracing complex uses the rough collision instead making tiny objects easier to select. */
  TraceParams.bTraceComplex = false;

  // Run a trace from our viewpoint to the trace end at our max interaction distance and see if we hit an actor
  FHitResult Hit(ForceInit);
  GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, TraceParams);

  return Hit.GetActor();
}

Now we need to mount the ActorComponent on our SerfsUpPlayerController

public:
    /* Interaction Component */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Interaction)
    class UInteractionComponent *InteractionComp;
ASerfsUpPlayerController::ASerfsUpPlayerController()
{
  ...
  // Setup the InteractionComponent
  InteractionComp = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComp"));
}

If we wanted to test that this worked, we could add some simple debug logging to our InteractionComponent

// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
if (Actor && Actor->Implements<UInteractableInterface>())
{
  // New focus is interactable
  FocusedInteractableActor = Actor;
  UE_LOG(LogTemp, Warning, TEXT("Interactable found %s %s"), *IInteractableInterface::Execute_GetInteractionText(Actor), *IInteractableInterface::Execute_GetName(Actor));
}

Now if we save and compile and run the game, if we walk over and kinda stare in the direction of the Axe we should get a log entry showing us we found the Axe interactable.

Step 9 - Binding the Interact Input Key

This is the last step for this part of the tutorial, binding through the Interact Input key so we can pick up the items in our level. For this action since we'll be using the keybind for interactions in the world and inside our inventory/containers we should put the input into the PlayerController so it's not localized to a specific character. This lets us disable input on the Character while we're in menu's/inventory screens but still intercept the keypress.

Note since this is an Unreal 5.1 tutorial I will be using AdvancedInputMapping for handling inputs. If you are using an older version of Unreal please note you will need to pipe the inputs through the Project settings and handle the conditional status of which input context we want yourself.

Let's start by creating a new Folder under SerfsUp called Input and creating an Input Action named IA_Interact. We don't need to open or change anything for this input (since it defaults to a digital boolean) but if you want to open it and get an idea of what we could do feel free.

Then we'll create an Input Mapping Context called IMC_Character. Inside of IMC_Character we want to add a mapping for IA_Interact and lets set the keyboard key to "E".

To bind the new Character Input Mapping Context and Inputs to our player controller, we need to add a BeginPlay and SetupPlayerInputComponent function to our SerfsUpPlayerController.

Public/Player/SerfsUpPlayerController.h
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
#include "InputActionValue.h"

protected:
    // ~Overrides: APlayerController
    virtual void SetupInputComponent() override;

    // To add mapping context
    virtual void BeginPlay();

public:
    /** MappingContext */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    class UInputMappingContext *CharacterMappingContext;

    /** Interact Input Action */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    class UInputAction *InteractAction;

protected:
    /** Called for interaction input */
    void OnInteractActionStart(const FInputActionValue &Value);
    void OnInteractActionEnd(const FInputActionValue &Value);
Private/Player/SerfsUpPlayerController.cpp
// This work is licensed under a Creative Commons Attribution 4.0 International License https://creativecommons.org/licenses/by/4.0/
#include "Components/InputComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

void ASerfsUpPlayerController::BeginPlay()
{
  // Call the base class
  Super::BeginPlay();

  // Add The Character Mapping Context
  if (UEnhancedInputLocalPlayerSubsystem *Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
  {
    Subsystem->AddMappingContext(CharacterMappingContext, 0);
  }
}

//////////////////////////////////////////////////////////////////////////
// Input

void ASerfsUpPlayerController::SetupInputComponent()
{
  Super::SetupInputComponent();
  // Set up action bindings
  if (UEnhancedInputComponent *EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent))
  {
    // Bind interaction start and end to two local functions that we can proxy through to the Interaction Component
    EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ASerfsUpPlayerController::OnInteractActionStart);
    EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Completed, this, &ASerfsUpPlayerController::OnInteractActionEnd);
  }
}

void ASerfsUpPlayerController::OnInteractActionStart(const FInputActionValue &Value)
{
  // Tell the InteractionComp to interact with completed false
  // Because this is in the CharacterMappingContext we know it will only be called when the character has input
  // so we don't need to check if we're in a menu or inventory screen
  InteractionComp->Interact(false, this);
}

void ASerfsUpPlayerController::OnInteractActionEnd(const FInputActionValue &Value)
{
  // Tell the InteractionComp to interact with completed false
  // Because this is in the CharacterMappingContext we know it will only be called when the character has input
  // so we don't need to check if we're in a menu or inventory screen
  InteractionComp->Interact(true, this);
}

Save and compile then the last thing we need to do is set the two variables for CharacterMappingContext and InteractAction in the BP_SerfsUpPlayerController.

Fin

If you run your game now and walk up to the axe and kinda look around (we don't have a hud yet so it's hard to aim) and press E on the axe you'll get the following logged and the actor will be removed from our level.

LogTemp: Warning: Interactable found Take Tool_Axe
LogTemp: Warning: Adding Item to Inventory Tool_Axe

This shows you can now pickup and store items (it will be in your inventory container) which completes part 1 of setting up an inventory system. In the next tutorial we'll work on the UI that lets us see and interact with our inventory and the world outside of log files.

December 30, 2022

0

🕛 24

© 2022 - 2023, Built by @imothee