简体   繁体   中英

How can I create a UE4 UCLASS base class that uses CreateDefaultSubobject() but allow sub-classes to change the type of the sub-object?

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.
  • If I define function templates on the constructor, there's no way to supply the values for those templates when the base class constructor is being invoked because C++ only allows function templates to be supplied when a function has a name, and constructors don't have a name (only the name of the class), so the compiler flags a syntax error if I try to supply the type parameters when invoking the parameterized base class constructor.
  • I can't move the 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.
  • I can't move the logic that creates the ASC and AS into a virtual method that the parent class calls and sub-classes override because that would be unsafe and lead to only the parent's version of the virtual methods getting called (if at all) due to the way that classes are constructed.
  • I can't have the child class simply overwrite the ASC and AS that the parent created with its own copies because the ASC and AS have already been added as child components with the same name to the parent.

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>())
{
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM