Sharing functionality between different classes

Say you have something like the following structure for a game:



Unit:
    Building:
        Base
        Barracks
        ...
    Vehicle:
        Tank
        ...
    Human:
        Gunner
        ...


My first thought would be to create a AUnit : public AActor class, a ABuilding : public AUnit class, etc.
The only problem is that while a building would probably be an Actor or Pawn, a vehicle or human unit would be a Character.

So my question is how would you go about structuring something like this?

This is a classic OOP problem, it may help if you try to think it in term of “is-a”, or “has-a”.
What you’re trying to do is to fit totally different objects into the “is-A” concept (My Building, Vehicle and Human classes ‘is-a’ Unit), which may be difficult like you mentioned.
If can you re-phrase your problem like so “My Building, Vehicle and Human classes have-a Unit property/behavior”, then you can rearrange your hierarchy like this:


AActor:
    Building:
        Base
        Barracks
        ...

ACharacter:
    Vehicle:
        Tank
        ...
    Human:
        Gunner
        ...

Every single one of the base classes ABuilding, AVehicle, AHuman, “has-a” pointer that points to a UUnit class that you instantiate in the constructor with CreateDefaultSubobject.

Thanks, that makes sense. I have two more questions:

  1. So would UUnit be a component or could it just be a plain UObject?
  2. Say I have a function UUnit::Upgrade, which has some common functionality that all units need, and I want to extend the functionality within each unit type as well as their subclasses, how would I do that?

Get the superclasses to have virtual functions that you are then going to override.
About the components, you can have a weapon or vehicle for example as an actor and extra components as the upgrades.
There are literally tons of configurations…

I understand that part, but for example AVehilce and ATank won’t be subclasses of UUnit so they can’t override UUnit::Upgrade.

I do realise that there are probably many different ways to do this, and which is best would depend on the situation, but this is all new to me and I thought that someone with experience could at least give me one idea that I could use as a starting point.

  1. I would just go with UObject, unless you need something a UComponent offers.
  2. Many ways to solve this,
    A. One that immediate came to mind is to make your Unit class an FUnit, it doesn’t derive from anything, just a normal C++ class. Then have your ABuilding, AVehicle, AHuman classes also derive from FUnit. Basically multiple inheritance. You declare a virtual Upgrade function in your FUnit class that you can derive in your ABuilding class function.
    B. Very similar to multiple inheritance, but using unreal’s UInterface, check this out for more info.

So if I were to use a plain C++ class:

  1. I could still operate on FUnit as an Actor using Cast<AActor>(this)
  2. I wouldn’t be able to interact with it in blueprints since I can’t use the UCLASS macro

Is this right?

Speaking from experience on this one…

Use an ActorComponent and if you need it, create an interface. My game has Human Pawns (inheriting from ACharacter), varying vehicle types (APawn) and buildings (AActor) and is an Multiplayer FPS / RTS hybrid.

The Component contains all of the shared stats and common functionality, such as holding and storing weapons, health / ammo stats etc, where the eyepoint location is. All of this can be accessed via the actor component also already supports replication (which UObject does not).

So all units have health, but they may want to do different things when killed. AVehicle might want to explode, whereas ABuilding might collapse. Whether a unit is killed or not is determined by the Actor Component, and it calls the Notify_Killed() function via the interface on it’s owner. As a security check, you can force the component to fire an assertion when it’s registered, to guarantee that the owner implements that interface.

By far the easiest and more flexible method. Haven’t run into any speedbumps yet.

Thanks, this is exactly what I was talking about.
Going by this, and everyone else’s input, this seems like a good way to do things.

Honestly “Is-a” with an interface requiring each to accept a UUnit would be my approach.

Treat objects like objects. At some point you will want to destroy them or remake them or save them. Self-inclusive, loosely coupled.

In this instance a UUnit could be a delivery mechanism for things to happen, in a predictable testable regression proof way.

If all things need something, that is a parent. “Is a”
If some things need something, that is a child, usually with an interface if “most” will have it. “Has a”
If some things need something, and they behave very differently based on that tiny thing, use an interface with a standard of messaging. “Can accept”

I’m not sure I understand.
So are you agreeing with TheJamsh? I would have an interface that all units will implement, and UUnit which contains all of the common functionality.

^ Do what you said in your post. Create a ‘UUnitComponent’ which you add to all of your various actors that support all that “unit-y stuff”, then implement the interface if you need to define custom behavior. Here’s an example from my project:

These two seperate classes inherit from different engines types, but share things like Health, Ammo, Weapons, they are detectable on radar etc.
ABZGame_Vehicle.h



UCLASS()
class BZGAME_API ABZGame_Vehicle : public APawn, public IBZGame_GameObjectInterface
{
	GENERATED_UCLASS_BODY()

public:
	FORCEINLINE UBZGame_GameObjectComponent* GetGameObjectComp() const { return GameObjectComponent; }

	// Begin GOC Interface
	void KillObject_Implementation(FDamageEvent const& DamageEvent, AController* InInstigator, AActor* DamageCauser);
	USkeletalMeshComponent* GetRootMesh_Implementation() { return VehicleMesh; }
	USkeletalMesh* GetFirstPersonMeshAsset_Implementation() { return CockpitAsset; }
	// End GOC Interface

protected:
	UPROPERTY(VisibleDefaultsOnly, Category = "Components")
	UBZGame_GameObjectComponent* GameObjectComponent;
}


ABZGame_Character.h



UCLASS()
class BZGAME_API ABZGame_Character : public ACharacter, public IBZGame_GameObjectInterface
{
	GENERATED_UCLASS_BODY()

public:
	/* Begin GOC Interface */
	void KillObject_Implementation(FDamageEvent const& DamageEvent, AController* InInstigator, AActor* DamageCauser);
	USkeletalMeshComponent* GetRootMesh_Implementation() { return Mesh1P; }
	USkeletalMesh* GetFirstPersonMeshAsset_Implementation() { return nullptr; }

	FORCEINLINE UBZGame_GameObjectComponent* GetGameObjectComponent() const { return GameObjectData; }

protected:
	UPROPERTY(VisibleDefaultsOnly, Category = "Stuff")
	UBZGame_GameObjectComponent* GameObjectData;


They both create the component as a sub-object, and initialize as required.



ABZGame_Vehicle::ABZGame_Vehicle(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	GameObjectComponent = ObjectInitializer.CreateDefaultSubobject<UBZGame_GameObjectComponent>(this, TEXT("GameObjectComponent"));
	GetGameObjectComp()->bGenerateMaterialInstances = true;
	GetGameObjectComp()->SetNetAddressable();
	GetGameObjectComp()->SetIsReplicated(true);
}

void ABZGame_Vehicle::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	if (GetWorld() && GetWorld()->IsGameWorld())
	{
		if (Role == ROLE_Authority)
		{
			GetGameObjectComp()->SetPawnOwner(this);
			GetGameObjectComp()->SpawnDefaultHardpoints();
			GetGameObjectComp()->InitHealthAndAmmo();
//etc.


This is my ‘GameObjectComponent’ (your UnitComponent). It garauntees that the owner supports the “IBZGameObjectInterface” when registered, so that the required functions are garaunteed to be there.
“ASSERTV” is from my assert library, but it just soft-crashes the game if it can’t find the component.



void UBZGame_GameObjectComponent::OnRegister()
{
	Super::OnRegister();

 	ASSERTV(GetOwner()->GetClass()->ImplementsInterface(UBZGame_GameObjectInterface::StaticClass()), *FString::Printf(TEXT("Game Object %s Does not Implement a Game Object Interface!"), *GetNameSafe(GetOwner())));
}


And it calls the appropriate functions on the owner through the interface, as and when it needs to:



void UBZGame_GameObjectComponent::KillObject(FDamageEvent const& DamageEvent, AController* InInstigator, AActor* DamageCauser)
{
	// Assert if this is ever called Client-Side
	ASSERTV(GetOwner()->Role == ROLE_Authority, *FString::Printf(TEXT("GOC %s Owned by %s is trying to call KillObject Client-Side"), *GetNameSafe(this), *GetNameSafe(GetOwner())));

	// Ensure Health is Zero & Mark All Weapons for Destroy
	CurrentHealth = 0.f;
	DestroyInventory();

	// Run the Type-Specific KillObject functionality for any owning class that implements the GOC Interface.
	// If any 'Score' is to be added, it should also be done in the override interface function.
	IBZGame_GameObjectInterface::Execute_KillObject(GetOwner(), DamageEvent, InInstigator, DamageCauser);
}


EDIT: The interface:



UINTERFACE()
class BZGAME_API UBZGame_GameObjectInterface : public UInterface
{
	GENERATED_UINTERFACE_BODY()
};

class IBZGame_GameObjectInterface
{
	GENERATED_IINTERFACE_BODY()

public:
	UFUNCTION(BlueprintNativeEvent, Category = "Game Object")
	void KillObject(FDamageEvent const& DamageEvent, AController* InInstigator, AActor* DamageCauser);

	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Game Object")
	USkeletalMeshComponent* GetRootMesh();

	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Game Object")
	USkeletalMesh* GetFirstPersonMeshAsset();
};


Thanks for the example code, it’s really helpful. It’s just that I didn’t quite understand what Dodgin said.

On an unrelated note, may I ask what the purpose of the if ( GetWorld() ) check in your code is for?
I’ve seen it used in some examples but then not in others, and I’ve haven’t been able to figure out when it’s required.
In what situation would GetWorld return null? and should I will perform this check before using GetWorld or only in certain situations?

So I call GetWorld() and GetWorld()->IsGameWorld(), basically because I want to make sure I’m actually in a ‘game world’ at the time, and not a preview world such as a Blueprint one, or the editor preview world for example.

In PostInitializeComponents(), I do that because I don’t want the objects creating / spawning the inventory weapons in the editor windows, I only want that to happen in the game world.

In GOC as part of ‘OnRegister()’, I do the check there because my Game Instance creates a Game Object Manager, which the object tries to register itself in immediately afterwards. In preview / editor worlds the Manager isn’t there until I start using PIE or unless the game is running, so to prevent firing an assertion I just check to make sure we’re in the world :slight_smile: