I am writing a re-usable RPG framework with the UE4 Gameplay Ability System. As part of the framework, I want to provide a character base class that games can override. The base class should instantiate both the Ability System Component (ASC) and Attribute Set (AS) of the character as sub-objects, preferably via the CreateDefaultSubobject()
, which, as you may know, can only be invoked in the constructor. The problem is that I need the type of the ASC and AS to be something that sub-classes can override, so if they have a more specific ASC and AC that can plug it in.
Here are the things I've tried/constraints I need to satisfy:
UCLASS()
objects cannot be templated, so I can't make the type of the ASC and AS class template variables.CreateDefaultSubobject()
calls into the initializers for the constructor because doing so causes a crash. I suspect the initializer list is evaluated before the object is constructed and the CreateDefaultSubobject()
tries to access some of the state of the parent character. The objects must be created as part of the body of the constructor.How can I satisfy these constraints and make it possible for child-classes to provider their own types for the ASC and AS?
The solution here was to create a factory class that is templated, and then have the constructor take the factory in as a parameter. The factory class has methods that create the ASC and AS; the constructor invokes those methods and passes-in the character, allowing the ASC and AS to be created as part of the body of the constructor.
Here's what the factory class looks like:
/**
* Type of object for instantiating the ASC and attribute set for this type of character.
*
* This is in a separate object to allow these types to be parameterized/templated so sub-classes can swap out the type
* of ASC and attribute set just by supplying different types in their constructors.
*/
template<class AscType, class AttributeSetType>
class TMyCharacterComponentFactory
{
public:
/**
* Creates an Ability System Component (ASC) for this character.
*
* The ASC is automatically created as a default sub-object of the character, with the name
* "AbilitySystemComponent".
*
* @param Character
* The character for which the ASC will be created.
*
* @return
* The new ASC.
*/
static AscType* CreateAbilitySystemComponent(AMyCharacterBase* Character)
{
AscType* AbilitySystemComponent = Character->CreateDefaultSubobject<AscType>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
return AbilitySystemComponent;
}
/**
* Creates an Attribute Set for this character.
*
* The attribute set is automatically created as a default sub-object of the character, with the name
* "AttributeSet".
*
* @param Character
* The character for which the attribute set will be created.
*
* @return
* The new attribute set.
*/
static AttributeSetType* CreateAttributeSet(AMyCharacterBase* Character)
{
return Character->CreateDefaultSubobject<AttributeSetType>(TEXT("AttributeSet"));
}
};
With this in place, here's what the base class looks like (note that the constructor that takes-in the factory object must be defined in the header of the class to ensure that the compiler can generate a constructor with the correct template parameters at compile-time):
#include <GameFramework/Character.h>
#include <AbilitySystemInterface.h>
#include <UObject/ConstructorHelpers.h>
#include "Abilities/MyAbilitySystemComponent.h"
#include "Abilities/MyAttributeSet.h"
#include "MyCharacterBase.generated.h"
// Forward declaration; this is defined at the end of the file.
template<class AscType, class AttributeSetType>
class TMyCharacterComponentFactory;
/**
* Base class for both playable and non-playable characters.
*/
UCLASS()
class MY_API AMyCharacterBase : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
/**
* Default constructor for AMyCharacterBase.
*/
explicit AMyCharacterBase();
protected:
/**
* The Ability System Component (ASC) used for interfacing this character with the Gameplay Abilities System (GAS).
*/
UPROPERTY()
UMyAbilitySystemComponent* AbilitySystemComponent;
/**
* The attributes of this character.
*/
UPROPERTY()
UMyAttributeSet* AttributeSet;
/**
* Constructor for sub-classes to specify the type of the ASC and Attribute Set.
*
* @param ComponentFactory
* The factory component to use for generating the ASC and Attribute Set.
*/
template<class AscType, class AttributeSetType>
AMyCharacterBase(TMyCharacterComponentFactory<AscType, AttributeSetType> ComponentFactory)
{
this->AbilitySystemComponent = ComponentFactory.CreateAbilitySystemComponent(this);
this->AttributeSet = ComponentFactory.CreateAttributeSet(this);
}
};
// In the CPP file
AMyCharacterBase::AMyCharacterBase() :
AMyCharacterBase(TMyCharacterComponentFactory<UMyAbilitySystemComponent, UMyAttributeSet>())
{
}
Here's what a base class that extends this and provides its own type for the ASC and AS looks like:
#include "MyCharacterBase.h"
#include "Abilities/OtherAbilitySystemComponent.h"
#include "Abilities/OtherAttributeSet.h"
#include "AnOtherCharacterBase.generated.h"
UCLASS()
class OTHER_API AnOtherCharacterBase : public AMyCharacterBase
{
GENERATED_BODY()
public:
// Constructor and overrides
AnOtherCharacterBase();
}
// In the CPP file
AnOtherCharacterBase::AnOtherCharacterBase() :
AMyCharacterBase(TMyCharacterComponentFactory<OtherAbilitySystemComponent, OtherAttributeSet>())
{
}