Custom Generic Components

Hello! Long time lurker first time poster. I was wondering if I could get some help on a generic inventory component I am trying to make. I would like to make a basic system where components inherited from this component have an array of a generic type, and a protected add and remove function. I have implemented it just fine with a hard coded struct, but I would like to make it something where inherited components chose a type and then the component is an inventory of that type. Is this possible, or should I look into a JSON type input on my struct to store item-specific data?

This is what I have right now:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Inventory/InventoryDataTypes.h"
#include "GenericInventoryComponent.generated.h"


UCLASS(abstract, Blueprintable, meta = (BlueprintSpawnableComponent), Blueprintable)
class MYPROJECT_API UGenericInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this components properties
	UGenericInventoryComponent();

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

public:	

        ' would like to remove the references to FInventoryItem and replace them with something that is defined in inherited components '

	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Inventory")
		void AddItem(const FInventoryItem ItemToAdd);
	UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Inventory")
		void RemoveItem(const FInventoryItem ItemToRemove);

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inventory")
		int ItemLimit;
	UPROPERTY(BlueprintReadOnly, Category = "Inventory")
		TArray<FInventoryItem> Items;
		
};

Thanks!

C++ does not have Generics like in C#, but the concept in C++ that is most similar to what you are talking about is “Templates”, usually expressed as Class<T>

these are a mechanism for dynamically allowing for a type to be chosen partially at runtime (because they have an overhead cost due to that strongly typed nature of C++, and the performance gained from pre-compilation), but mostly done at compile time.

Templates are already used throughout the Engine basically any time you see <VariableType> that is a template function or Template class (for example the TArray<T> or any Engine class that has a leading character of “T” for that matter. (technically the Cast<T> breaks this rule but that is because it is a special wrapper on dyncamic_Cast<T> for consistency and friendlier integration on other platforms.

for implementing them in your game you can look at things like TArray (in like VS you can right-click the TArray and pick “Go To Definition”) though realistically I would strongly discourage making your own Template class as they can be very tedious, and you might need to incorporate many edge cases. my suggestion would be to either use “Interface” based inheritance, or “pure inheritance”:

in the pure inheritance approach you would create a “Generic” UGenericInventoryComponent that has the bare minimum needed, but also includes some kind of enumeration, or type identifier for “more precision”
give it the “needed” functions marked as Virtual and in the function declaration instead of giving it a function body put = 0 before the semicolon

public:
    // and classes with functions function marked as "virtual" and set "=0" cannot be instantiated only the child classes
    virtual GetCurrentItem() = 0;

then in the classes that inherit from UGenericInventoryComponent you would put

public
    virtual GetCurrentItem() override;

then give the implementation to the function there

then you can still have a member pointer of say TSubclassOf<UGenericInventoryComponent> or pass a UGenericinventoryComponent* as an argument

the reason for the enumeration of other Type identifier is in the event something needs to be strictly cast to a type you can go into a Switch-Case on an enum rather then then multiple if statements like:

if ( MyType1 var1 = Cast<MyType1>(TestCastingVariable))
{
}
else if ( MyType2 var1 = Cast<MyType2>(TestCastingVariable))
{
}
...

*the other option is interfaces: these are more a set of function promises, and C++ technically allows for you to give them data members (similar to how C++ allows you to give structs functions). this is how C++ gets into “technically” having “multi-inheritance”

this way you can do tricks where say in your interaction to “pick up” you can do you collision or trace out of the OtherComponent, or HitComponent you can test if it “implements Interface”

you will still need to implement the functions of the interface similar to the pure inheritance, but this is one other option.

Thank you gardian206 this is really helpful! Templates mostly seem like what I am looking for, as I am hoping to make this component in a way where I could have like an inherited component that could be like a “ActorInventoryComponent” which only holds actors or “StructInventoryComponent” which only holds structs.

The inheritance is a really interesting solution but I am curious as to how it would work for a sort of generic thing like this. You showed the example of GetCurrentItem, you don’t have any parameters or return values in your code snippet. Is that something you would implement in an inherited component? Or would it need to match the signature provided?

I mostly want this component because it feels like the inventory behavior will be reused often, but do you think I would better off making specific TArrays per actor instead? The more I am thinking about it this kind of feels like I am making a really general component that acts as a glorified list.

this is really a question of 2 parts:
with the inheritance, or interface approach you will need to at some point cast, though in reality you would need to cast with the template approach anyways, where every time you call a template function, class you are casting to the type, most of it is just being done on the back end.

for your ActorInventory I am kind of assuming you mean like some kind of AInventoryItems : AActor which you might be going to “code re-usability” too quickly where code can be re-used without making sudo-generics either through Template or either flavor of inheritance, because you will be incurring the cost of casting at some point, which will be traversing either a Virtual Function Table (inheritance), or resolving/checking types (Templates).

when you can keep what looks to be a serviceable class and just make another class that is similar.

what I end up doing is that the inventory is a TArray<FGameItem> then it has pointers to the like the equipment slots. the FGameItem holds a TSoftClassPtr<ASpawnableItem> to save on memory footprint through dynamic or even Async loading (really the TArray is for FSimpleItem that holds like an IDNumber, Level, Quantity the Instance value stuff. then the FGameItem is held in a repository for more Archetype only stuff so that I don’t have a too much data running around per item instance in each inventory instance). When an item is Equip(ASpawnableItem* EquippingItem) I use the inventory to determine what slot to put it on, and then just put it there.

the reason to avoid Arrays “at times” is because they can be random accessed (by index, iterated over for a key) and modified, if you have say your character’s helmet at index = 5 at some random point in the programs execution index =5 might not be the helmet but rather a sword and then what?

  • if there is to be some random amount of things, that it is OK to be in a random order then use a TArray (to get a specific one you will need to know the index which might have changed, or traverse the array looking for a key)
  • if there is to be some random amount of things, that shall be in a defined order then maybe a TMap (to get a specific one you will need the Key, but to get multiple different keys you will have to do a C++ foreach which is iterating over the whole map anyways)
  • if there is to be a set number of things, that shall always be in the same order, then probably a struct, or just member references/pointers (especially if you want to call them by specific name)

if you want to move forward with inheritance approach for these things

UENUM(BlueprintType)
enum class EContainerType : uint8
{
   NONE = 0,
   EquipmentStructs = 1,
   ActorItems= 2
};

UCLASS(BlueprintType, ClassGroup=(Custom), meta = (BlueprintSpawnableComponent)) 
class [ProjectName]_API UGenericContainer : public UActorComponent
{
    GENERATED_BODY()

public:
    UGenericContainer();

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Container)
    EContainerType ContainerType = EContainerType::NONE;

    UFUNCTION(BlueprintCallable, Category = Container)
    virtual int32 GetQuantity() = 0;

    UFUNCTION(BlueprintCallable, Category = Container)
    virtual bool IsEmpty() = 0;

protected:
    int32 Qty = 0;
    int32 CurrentIndex = 0;
private:

};
UCLASS()
class [PROJECTName]_API UStructInventoryComponent : public UGenericContainer
{
    GENERATED_BODY()
public:
    UStructInventoryComponent()
    {
          ContainerType = EContainerType::EquipmentStructs;
    }
   
    UFUNCTION(BlueprintCallable, Category = Container)
    virtual int32 GetQuantity() override
    {
         Qty = Array.Num();
         return Qty;
    }

    FItemStruct* GetCurrent()
    {
         if(!Array.IsEmpty() && Array.Num() < CurrentIndex)
         {
              return Array[CurrentIndex];
         }
         return nullptr;
    }

    UFUCTION(BlueprintCallable, Category = Container)
    void AddItem(const FItemStruct& inItem);
protected:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Container)
    TArray<FItemStruct> Array;
private:

};

then to access it you could

TObjectPtr<UStructInventoryComponent> StructItems;
// or 
TObjectPtr<UGenericContainer> StructItems;
// but to say get an item you would need to
switch (StructItems->ContainerType)
{
case EContainerType::EquipmentStructs:
    {
        FItemStruct Item = Cast<UStructInventoryComponent>(StructItems)->GetCurrent();
        // do stuff with Item here
        break;
    }
}

Gotcha thank you so much! This has given me a lot to think on for how to implement this feature. I appreciate you talking it over with me :smiley:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.