C++ Inventory
Contents
Overview
Original Author Denis "Heite92" Heitkamp
This wiki page will show how to implement a simple inventory system with C++ instead of blueprints. The initial work is based on the video tutorial series Crafting/Inventory System Tutorial in Unreal Engine by Reuben Ward and C++ 3rd Person Battery Collector Power Up Game by Lauren Ridge. Most games have some sort of inventory system and one or more pickup mechanisms. This tutorial covers the implementation of one inventory and two pickup systems. One of the pickup systems is based on sight. If the player is looking at a pickupable item and if it's in range the player can press a button to pick up the item. The other system doesn't require sight or input. The player automatically picks up everything close to him.
Systems like these are used in many games like Borderlands, Diablo, Skyrim or Witcher 3.
The core features of this system are:
- Extendible Inventory
- Weight or slot limit (optional)
- Extendible Item class
- Item weight
- Extendible automatic Pickup class (AutoPickup)
- Extendible Interactable class
- Extendible manual Pickup class (ManPickup)
- A database for every existing item
- A simple GUI for keyboard and mouse
It is not necessary to implement both the automatic and the manual Pickup class, each class works independently. This wiki article does not cover controls for gamepads, touchscreens and VR.
The following video shows the system in action.
<youtube>(https://youtu.be/hjhull5Rn5U)</youtube>
Preparation
The Third Person C++ Template is used. It should also work similarly with the First Person Template. The project name used in this tutorial is Inventory. The complete project is availabe on GitHub.
Source Code
After creating the project there will be the following C++ classes:
- <project_name>
- <project_name>Character
- <project_name>GameMode
The next step is to add the these C++ classes to the project. That needs to be done with the UE4 Editor.
- <project_name>Controller derived from APlayerController
- The player controller will store the inventory
- <project_name>GameState derived from AGameStateBase
- the game state will hold a database for all existing items
- Interactable derived from AActor
- Works as base class for every interactable actor
- InventoryItem derived from FTableRowBase
- It represents picked up items in the player’s inventory
- It is not possible to pick FTableRowBase as the parent class inside the editor. Pick something else and change it later.
- ManPickup derived from AInteractable
- ManPickup is short for manual pickup. A different name can be used, but the source code has to be refactored. Same applies to the other classes
- AutoPickup derived from AActor
- AutoPickup means automatic pickup
- MoneyAutoPickup derived from AAutoPickup
It is also possible to add a MoneyManPickup class which derives from ManPickup for large occurrences of money.
Here is a class diagram for the inventory system.
Character
First edit <project_name>Character.h.
InventoryCharacter.h
The character class needs functionality from Engine.h, change
#include "CoreMinimal.h"
to
#include "Engine.h"
The next source code is only needed for AutoPickups, it is a collision sphere which is needed to check for overlapping AutoPickups. Add the code snippet to the character class, directly after the UCameraComponent.
/** Collection sphere */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class USphereComponent* CollectionSphere;
The Tick method is required to collect any pickup. Place it in the public section of the class.
virtual void Tick(float DeltaTime) override;
The following methods are needed to collect the pickups. Add the ones needed to the protected section of the character.
Needed to collect AutoPickups:
/** Function to collect every AutoPickup in range. */
void CollectAutoPickups();
Needed to collect ManPickups:
/** Function to check for the closest Interactable in sight and in range. */
void CheckForInteractables();
The next step is to edit the cpp file of the character:
InventoryCharacter.cpp
Add these includes:
#include "Interactable.h"
#include "AutoPickup.h"
#include "InventoryItem.h"
Also add an include for the player controller, in this project that is InventoryController.
#include "InventoryController.h"
Add the initialization of the collection sphere to the constructor of the character. The sphere is used by the player to pickup AutoPickups. Every AutoPickup inside the sphere will be picked up automatically. Alter the sphere radius if needed.
// Create the collection sphere
CollectionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CollectionSphere"));
CollectionSphere->SetupAttachment(RootComponent);
CollectionSphere->SetSphereRadius(200.f);
Add the methods. They can be placed anywhere in the cpp file after the includes. Beginning with the Tick method.
void AInventoryCharacter::Tick(float Deltatime)
{
Super::Tick(Deltatime);
CollectAutoPickups();
CheckForInteractables();
}
The next method is CollectAutoPickups, it can be skipped if the project does not use AutoPickups.
void AInventoryCharacter::CollectAutoPickups()
{
// Get all overlapping Actors and store them in an array
TArray<AActor*> CollectedActors;
CollectionSphere->GetOverlappingActors(CollectedActors);
AInventoryController* IController = Cast<AInventoryController>(GetController());
// For each collected Actor
for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
{
// Cast the actor to AAutoPickup
AAutoPickup* const TestPickup = Cast<AAutoPickup>(CollectedActors[iCollected]);
// If the cast is successful and the pickup is valid and active
if (TestPickup && !TestPickup->IsPendingKill())
{
TestPickup->Collect(IController);
}
}
}
The last method left ist CheckForInteractables, it is only needed to collect ManPickups. The range value defines how close a player has to be to pick up the item.
void AInventoryCharacter::CheckForInteractables()
{
// Create a LineTrace to check for a hit
FHitResult HitResult;
int32 Range = 500;
FVector StartTrace = FollowCamera->GetComponentLocation();
FVector EndTrace = (FollowCamera->GetForwardVector() * Range) + StartTrace;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
AInventoryController* IController = Cast<AInventoryController>(GetController());
if (IController)
{
// Check if something is hit
if (GetWorld()->LineTraceSingleByChannel(HitResult, StartTrace, EndTrace, ECC_Visibility, QueryParams))
{
// Cast the actor to AInteractable
AInteractable* Interactable = Cast<AInteractable>(HitResult.GetActor());
// If the cast is successful
if (Interactable)
{
IController->CurrentInteractable = Interactable;
return;
}
}
IController->CurrentInteractable = nullptr;
}
}
Player Controller
The player controller will store the player’s inventory and money.
InventoryController.h
First include the Interactable and InventoryItem classes.
#include "Interactable.h"
#include "InventoryItem.h"
The player controller needs some methods and properties. These should be pretty self-explanatory. InventorySlotLimit, InventoryWeightLimit, GetInventoryWeight and Money aren't needed for the basic inventory system. Leave them for the additional functionality or remove them from the controller.
public:
AInventoryController();
UFUNCTION(BlueprintImplementableEvent)
void ReloadInventory();
UFUNCTION(BlueprintCallable, Category = "Utils")
int32 GetInventoryWeight();
UFUNCTION(BlueprintCallable, Category = "Utils")
bool AddItemToInventoryByID(FName ID);
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
class AInteractable* CurrentInteractable;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
TArray<FInventoryItem> Inventory;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
int32 Money;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 InventorySlotLimit;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 InventoryWeightLimit;
protected:
void Interact();
virtual void SetupInputComponent() override;
InventoryController.cpp
Include the created game state and character. Do not forget to refactor the filenames to the specific project name.
#include "InventoryGameState.h"
#include "InventoryCharacter.h"
Also add the definitions of the methods and the constructor to the cpp file.
AInventoryController::AInventoryController()
{
InventorySlotLimit = 50;
InventoryWeightLimit = 500;
}
int32 AInventoryController::GetInventoryWeight()
{
int32 Weight = 0;
for (auto& Item : Inventory)
{
Weight += Item.Weight;
}
return Weight;
}
bool AInventoryController::AddItemToInventoryByID(FName ID)
{
AInventoryGameState* GameState = Cast<AInventoryGameState>(GetWorld()->GetGameState());
UDataTable* ItemTable = GameState->GetItemDB();
FInventoryItem* ItemToAdd = ItemTable->FindRow<FInventoryItem>(ID, "");
if (ItemToAdd)
{
// If a Slot- or WeightLimit are not needed remove them in this line
if (Inventory.Num() < InventorySlotLimit && GetInventoryWeight() + ItemToAdd->Weight <= InventoryWeightLimit)
{
Inventory.Add(*ItemToAdd);
ReloadInventory();
return true;
}
}
return false;
}
void AInventoryController::SetupInputComponent()
{
Super::SetupInputComponent();
InputComponent->BindAction("Interact", IE_Pressed, this, &AInventoryController::Interact);
}
void AInventoryController::Interact()
{
if (CurrentInteractable)
{
CurrentInteractable->Interact(this);
}
}
Game State
The game state will hold the database for all existing items.
InventoryGameState.h
CoreMinimal.h does not cover everything needed by the game state class, change
#include "CoreMinimal.h"
to
#include "Engine.h"
The next step is to add the ItemDB as a property to the game state.
public:
AInventoryGameState();
UDataTable* GetItemDB() const;
protected:
UPROPERTY(EditDefaultsOnly)
class UDataTable* ItemDB;
InventoryGameState.cpp
The constructor loads the database and the get method returns it. The ItemDB can be created after the source code is compiled.
AInventoryGameState::AInventoryGameState()
{
static ConstructorHelpers::FObjectFinder<UDataTable> BP_ItemDB(TEXT("DataTable'/Game/Data/ItemDB.ItemDB'"));
ItemDB = BP_ItemDB.Object;
}
UDataTable* AInventoryGameState::GetItemDB() const
{
return ItemDB;
}
Inventory system
Most of the implementation of the inventory system can be copied. AInventoryController has to be changed at a few points to A<project_name>controller but that is it. Every parameter is exposed to blueprints and even the Collect and Interact methods can be overwritten by blueprints if wanted. This enables designers without C++ knowledge to implement functionality.
Interactable
The Interactable class serves as base class for every interactable actor in the game. In this tutorial the only interactable actors are manual pickup items. But it can also be used as base class for interactable chests, Non-player characters(NPCs) and much more.
Interactable.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.generated.h"
UCLASS()
class INVENTORY_API AInteractable : public AActor
{
GENERATED_BODY()
public:
AInteractable();
UFUNCTION(BlueprintNativeEvent)
void Interact(APlayerController* Controller);
virtual void Interact_Implementation(APlayerController* Controller);
UPROPERTY(EditDefaultsOnly)
FString Name;
UPROPERTY(EditDefaultsOnly)
FString Action;
UFUNCTION(BlueprintCallable, Category = "Pickup")
FString GetInteractText() const;
};
Interactable.cpp
#include "Interactable.h"
AInteractable::AInteractable()
{
Name = "Interactable";
Action = "interact";
}
void AInteractable::Interact_Implementation(APlayerController* Controller)
{
return;
}
FString AInteractable::GetInteractText() const
{
return FString::Printf(TEXT("%s: Press F to %s"), *Name, *Action);
}
Manual Pickup
The ManPickup class uses Interactable as base class. It is used to implement items which can be picked up manually by the player. The player has to be in pickup range, has to look directly at the item and he has to press a specific button to pick up the item. The button will be defined in the input section of this tutorial.
ManPickup.h
#pragma once
#include "CoreMinimal.h"
#include "Interactable.h"
#include "ManPickup.generated.h"
UCLASS()
class INVENTORY_API AManPickup : public AInteractable
{
GENERATED_BODY()
public:
AManPickup();
void Interact_Implementation(APlayerController* Controller) override;
protected:
UPROPERTY(EditAnywhere)
UStaticMeshComponent* PickupMesh;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemID;
};
ManPickup.cpp
#include "ManPickup.h"
#include "InventoryController.h"
AManPickup::AManPickup()
{
PickupMesh = CreateDefaultSubobject<UStaticMeshComponent>("PickupMesh");
RootComponent = Cast<USceneComponent>(PickupMesh);
ItemID = FName("No ID");
Super::Name = "Item";
Super::Action = "pickup";
}
void AManPickup::Interact_Implementation(APlayerController* Controller)
{
Super::Interact_Implementation(Controller);
AInventoryController* IController = Cast<AInventoryController>(Controller);
if(IController->AddItemToInventoryByID(ItemID))
Destroy();
}
Automatic Pickup
This class is used to implement items which are picked up automatically when the player is in pickup range to the actors.
AutoPickup.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AutoPickup.generated.h"
UCLASS()
class INVENTORY_API AAutoPickup : public AActor
{
GENERATED_BODY()
public:
AAutoPickup();
UFUNCTION(BlueprintNativeEvent)
void Collect(APlayerController* Controller);
virtual void Collect_Implementation(APlayerController* Controller);
FName GetItemID();
protected:
UPROPERTY(EditAnywhere)
UStaticMeshComponent* PickupMesh;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemID;
};
AutoPickup.cpp
#include "AutoPickup.h"
#include "InventoryController.h"
AAutoPickup::AAutoPickup()
{
PickupMesh = CreateDefaultSubobject<UStaticMeshComponent>("PickupMesh");
RootComponent = Cast<USceneComponent>(PickupMesh);
ItemID = FName("No ID");
}
void AAutoPickup::Collect_Implementation(APlayerController* Controller)
{
AInventoryController* IController = Cast<AInventoryController>(Controller);
if(IController->AddItemToInventoryByID(ItemID))
Destroy();
}
FName AAutoPickup::GetItemID()
{
return ItemID;
}
Money Automatic Pickup
The class MoneyAutoPickup is a special form of AutoPickup. It overrides the Collect method and it also adds the attribute Value. The player’s money is not stored in the inventory. So instead of calling AddItemToInventoryByID, which is done by AutoPickups, the money will be directly added to the money attribute.
MoneyAutoPickup.h
#pragma once
#include "CoreMinimal.h"
#include "AutoPickup.h"
#include "MoneyAutoPickup.generated.h"
UCLASS()
class INVENTORY_API AMoneyAutoPickup : public AAutoPickup
{
GENERATED_BODY()
public:
AMoneyAutoPickup();
void Collect_Implementation(APlayerController* Controller) override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Value;
};
MoneyAutoPickup.cpp
#include "MoneyAutoPickup.h"
#include "InventoryController.h"
AMoneyAutoPickup::AMoneyAutoPickup()
{
Super::ItemID = FName("money");
Value = 1;
}
void AMoneyAutoPickup::Collect_Implementation(APlayerController* Controller)
{
AInventoryController* IController = Cast<AInventoryController>(Controller);
IController->Money += Value;
Destroy();
}
Inventory Item
This class represents the picked up items in the player’s inventory. Depending on the needs a few adjustments can be done. If the weight attribute is not needed it can ne removed from the inventory item. Also if a rarity system (as seen in World of Warcraft, Borderlands, Diablo...) is wanted a property can be added for that.
InventoryItem.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/DataTable.h"
#include "InventoryItem.generated.h"
USTRUCT(BlueprintType)
struct FInventoryItem : public FTableRowBase
{
GENERATED_BODY()
public:
FInventoryItem();
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemID;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Weight;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Value;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* Thumbnail;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText Description;
bool operator==(const FInventoryItem& OtherItem) const
{
if (ItemID == OtherItem.ItemID)
return true;
return false;
}
};
InventoryItem.cpp
#include "InventoryItem.h"
FInventoryItem::FInventoryItem()
{
this->Name = FText::FromString("No Name");
this->Weight = 1;
this->Value = 1;
this->Description = FText::FromString("No Description");
}
At this point the project should be compiled. Otherwise it will not be possible to complete all left steps.
Input
A few additional keybindings are needed:
- Toggle the inventory
- Interact with interactables
- Delete items from the inventory
Open the project settings with Edit -> Project Settings... and navigate to Engine -> Input. There add mappings for Interact, OpenInventory and Remove as shown in the picture below.
Also it could be useful to add an additional mapping for RemoveMouseButton (Right Mouse Button) instead of defining the binding inside a blueprint.
Graphical User Interface
Interact Text Overlay
Create the folder UI inside Content and create a new User Interface -> Widget Blueprint. Name it WB_Ingame. The widget is required to show the player the interact text for the current Interactable, something like Teapot: Press f to pickup for a receivable teapot. First switch from Designer to Graph and create the function GetInteractText.
GetInteractText
Switch back to the designer and add a static text field to the middle of the screen. Set the text alignment to center and bind the text to GetInteractText.
Open the level blueprint as seen in the screenshot below.
Add the following to the level blueprint to show the interact text on the screen.
Inventory Slot Tooltip
Create the blueprint C_InventorySlot_Tooltip in the UI folder. First add a variable to store an inventory item.
The next step is to design the tooltip.
Bind all FText attributes of the item to the specific fields. To bind other attributes like int32 it is required to create functions. This is the case for value and weight, which are both int32.
GetValueText
GetWeightText
The final step is to add the binding to the value and weight field.
Inventory Slot
Create the blueprint C_InventorySlot in the UI folder. The same variable as used in the tooltip is necessary.
Design the inventory slot and bind the attributes of the inventory item. The design in this tutorial is a minimalistic design by showing only the thumbnail. The thumbnail is stored inside a size box to make sure every thumbnail has the same size.
Add the function GetToolTipWidget.
Also the function has to be bound to the canvas panel.
Add the RemoveItemFromInventory function.
Override the function OnMouseButtonDown with the following blueprint.
Inventory
Create the widget blueprint WB_Inventory.
Design the inventory. If it needs to be scrollable the inventory slot container has to be wrapped by a ScrollBox.
Add the LoadInventoryItems function.
GetMoneyText
GetWeightText
GetWeightLimitText
Assign the bindings in the designer to the weight, weight limit and money field.
The last step to finish the inventory is to add functionality to the event graph. On construct the system shows the mouse cursor and sets the input mode to game and UI. On destruct the mouse cursor is hidden and the input mode is set to game only. Also the onclick event on the close button removes the widget from the view.
Blueprints
First create the folder Content->Blueprints to hold the blueprints. Next create blueprints based on the player controller and the game mode. Go to the C++ classes and do a right click on <project_name>Controller. Select Create Blueprint class based on... and create BP_<project_name>Controller in the blueprints folder. Now do the same for <project_name>GameMode. The next step is to go to the project settings. Then go to Project->Maps&Modes and select the BP_<project_name>GameMode. Also set the following:
- Default Pawn Class = <project_name>Character
- Player Controller Class = BP_<project_name>Controller
- Game State Class = <project_name>GameState
These steps are necessary to tell the game to load the custom game mode, character, player ontroller and game state. Now open BP_<project_name>Controller in the blueprint editor. First add this variable to the blueprint to store the inventory widget.
Then add these blueprints to the event graph:
Assets, Pickups and Database
Create the folder Assets inside the Content folder. Then create the folders Materials, Models and Thumbnails inside it. Place everything need for the pickups in these folders.
The next step is to create the actual pickups. Create the folder Content->Blueprints->Pickups. Navigate to the C++ classes and do a right click on AutoPickup, ManPickup or MoneyAutoPickup and select Create Blueprint class based on... pick the Pickups folder as destination. Assign the static mesh, the itemID, the value (MoneyAutoPickup only), the name and the action (ManPickup only) in the blueprint.
At this point it is already possible to place the pickups in the map and the player can collect them. But the items will not be added to the inventory because they do not refer to items in the item database.
Create the folder Data inside Content and create a new Miscellaeneous->Data Table. Select Inventory Item as pick structure and name it ItemDB.
Add items to the database. The row name of every entry should be the same as the ItemID, the ItemID is the same assigned in the pickup blueprints. The next picture shows a sample database with three entries.
Finally the items are added to the player’s inventory.
On Your Own!
There are many ways to improve and extend this system. These are a few ideas.
- Implement a PickupComponent for the shared functionality between Auto- and ManPickup
- Add support for stackable items
- The Player also needs to be able to separate and combine stacks
- Add drag & drop to delete, drop or swap items
- Load the ItemDB from an external database
- Persistence for the player inventory
- Use a database to save different Pickups
- Also use an additional database to save spawn points for the Pickups
- A crafting system
- 4 piles of wood can be crafted to a wall etc.
- Loot chests with multiple different items which can be looted
- Chests which have to be destroyed in order to loot them
- NPCs who sell and buy items
- NPCs who can be looted after the player killed them
- A clothing system to equip the player character with items
- Use the clothing system for NPCs (as seen in Skyrim)
- Multiplayer functionality
- Direct Trading between two players
- An auction house
- A mailing system