C++ Inventory

From Epic Wiki
Jump to: navigation, search

Template:Rating

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.

CppInventory newCppClass.png
  • <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.

CppInventory inventoryClassDiagram.png

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.

Template:Note

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.

CppInventory input.png

Also it could be useful to add an additional mapping for RemoveMouseButton (Right Mouse Button) instead of defining the binding inside a blueprint.

Template:Note

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

CppInventory ingameGetInteractText.png

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.

CppInventory ingameWB.png

Open the level blueprint as seen in the screenshot below.

CppInventory openLevelBP.png

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.

CppInventory itemVariable.png

The next step is to design the tooltip.

CppInventory tooltipDesigner.png

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

CppInventory tooltipGetValueText.png

GetWeightText

CppInventory tooltipGetWeightText.png

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.

CppInventory itemVariable.png

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.

CppInventory inventoryslotThumbnail.png

Template:Note

Add the function GetToolTipWidget.

CppInventory inventoryslotGetTooltip.png

Also the function has to be bound to the canvas panel.

CppInventory inventoryslotCanvas.png

Add the RemoveItemFromInventory function.

CppInventory inventoryslotRemoveItemFromInventory.png

Override the function OnMouseButtonDown with the following blueprint.

CppInventory inventoryslotOnMouseButtonDown.png

Template:Note

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.

CppInventory inventoryDesigner.png

Add the LoadInventoryItems function.

CppInventory inventoryLoadInventoryItems.png

GetMoneyText

CppInventory inventoryGetMoneyText.png

GetWeightText

CppInventory inventoryGetWeightText.png

GetWeightLimitText

CppInventory inventoryGetWeightLimitText.png

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.

CppInventory inventoryEventGraph.png

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.

CppInventory controllerInventoryWidget.png

Then add these blueprints to the event graph:

CppInventory controllerToggleInventory.png
CppInventory controllerEventGraph.png

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.

Template:Note

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.

CppInventory teapotBP.png

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.

Template:Note

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.

CppInventory itemDB.png

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