#Modular Gameplay in Aura

28 messages · Page 1 of 1 (latest)

paper root
#

I took some time to implement this and did everything except communicate via Gameplay Tags. Turns out Lyra already implements a nice modular architecture, so I removed everything I've been doing for the last week. I'll be documenting the process here using the example of AutoRun being extracted from the controller.

You need to enable the modules first, then reboot, get the error and fix with the suggested method.

Secondly you need to copy 2 plugins from Lyra project. One for registration in module architecture, the other for communication via Gameplay Tags.

paper root
#

The game controller must be inherited from the modular controller

class X_API AXPlayerController : public AModularPlayerController

Add a dependency to ModularGameplayActors and GameplayMessageRuntime in the X.Build.cs file

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayAbilities", "ModularGameplayActors" });

PrivateDependencyModuleNames.AddRange(new string[] { "GameplayTags", "GameplayTasks", "NavigationSystem", "Niagara", "AIModule", "GameplayMessageRuntime" });
paper root
#

Create new Gameplay Feature with name AutoRun.
The creation process will create the appearance of freeze, you must be patient and wait for completion so as not to break the process.

#

After the creation will not show the directory for the code, because it is empty, you need to start creating a class in the C++ code directory of the game. However, select the newly created plugin from the list. As always you need to quit compiling the code.

#

In the module file, select Active by default, do not touch any other settings. Save everything, close Unrial Editor, compile from IDE, everything should build without errors.

#

Start Unreal Engine, enable our module, close Unreal Engine. It's time to commit to git, the preparation is finished, let's write code.

paper root
#

Since, I've already done this ahead of time, let's get the resources ready. We need to clear the old variable, it will be deleted, but the cache will remain inside the Blueprint. After that we move the effect for the cursor to our new module. If we decide that AutoRun Feature will be disabled, we don't need to supply all the resources with the game.

paper root
#

When I wrote the code for AutoRun, I realized that I would have to create code that would be included in all modules. There is one structure here now, but most likely all Enums, Structs and probably all Native Tags will be here.

#

You need to delete GameplayMessageStructs.cpp and insert the following code into GameplayMessageStructs.h, deleting the code created by default.

#pragma once

#include "CoreMinimal.h"
#include "GameplayMessageStructs.generated.h"

USTRUCT (BlueprintType)
struct FTargetLocationMessage
{
    GENERATED_BODY()

    UPROPERTY (EditAnywhere, BlueprintReadWrite)
    APawn* Pawn = nullptr;
    
    UPROPERTY (EditAnywhere, BlueprintReadWrite)
    FVector TargetLocation = FVector::ZeroVector;
};

After which you can commit module to git.

paper root
#

AutoRun_CC.h

#pragma once

#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Components/ControllerComponent.h"
#include "GameFramework/GameplayMessageSubsystem.h"
#include "AutoRun_CC.generated.h"

struct FTargetLocationMessage;
class UNiagaraSystem;
class USplineComponent;

UCLASS()
class AUTORUNRUNTIME_API UAutoRun_CC : public UControllerComponent
{
    GENERATED_BODY()

public:
    UAutoRun_CC(const FObjectInitializer& ObjectInitializer);

    virtual void BeginPlay() override;

    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

    virtual void PlayerTick(float DeltaTime) override;

protected:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="X|PlayerController|AutoRun",
        meta=(Categories="GameplayMessage"))
    FGameplayTag AutoRunStartMessageChannel = FGameplayTag();

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="X|PlayerController|AutoRun",
        meta=(Categories="GameplayMessage"))
    FGameplayTag AutoRunStopMessageChannel = FGameplayTag();

private:
    UPROPERTY()
    TObjectPtr<APawn> ControlledPawn = nullptr;

    bool bAutoRunning = false;
    FVector CachedDestination = FVector::ZeroVector;

    UPROPERTY(VisibleAnywhere)
    TObjectPtr<USplineComponent> AutoRunSpline;

    UPROPERTY(EditDefaultsOnly, Category="X|PlayerController|AutoRun")
    float AutoRunAcceptanceRadius = 50.f;

    UPROPERTY(EditDefaultsOnly, Category="X|PlayerController|AutoRun")
    TObjectPtr<UNiagaraSystem> ClickEffect;

    void Start(const FVector& InDestination);
    void Stop();

    FGameplayMessageListenerHandle AutoRunStartMessageListener;
    FGameplayMessageListenerHandle AutoRunStopMessageListener;
    void OnAutoRunStartMessage(FGameplayTag InChannel, const FTargetLocationMessage& IncomingMessage);
    void OnAutoRunStopMessage(FGameplayTag InChannel, const FTargetLocationMessage& IncomingMessage);
};
#

AutoRun_CC.cpp

Includes

#include "AutoRun_CC.h"

#include "GameplayMessageStructs.h"
#include "GameplayTagContainer.h"
#include "NavigationPath.h"
#include "NavigationSystem.h"
#include "NiagaraFunctionLibrary.h"
#include "Components/SplineComponent.h"
#include "GameFramework/GameplayMessageSubsystem.h"

The constructor is as similar to the original as possible.

UAutoRun_CC::UAutoRun_CC(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    SetIsReplicated(true);
    AutoRunSpline = CreateDefaultSubobject<USplineComponent>("AutoRunSpline");
}

Registered by the listener to receive commands from outside.

void UAutoRun_CC::BeginPlay()
{
    Super::BeginPlay();

    ControlledPawn = GetPawn<APawn>();

    UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this);
    AutoRunStartMessageListener = MessageSubsystem.RegisterListener(AutoRunStartMessageChannel, this, &ThisClass::OnAutoRunStartMessage);
    AutoRunStopMessageListener = MessageSubsystem.RegisterListener(AutoRunStopMessageChannel, this, &ThisClass::OnAutoRunStopMessage);
}

At the end of the life cycle, don't forget to unsubscribe.

void UAutoRun_CC::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    AutoRunStartMessageListener.Unregister();
    AutoRunStopMessageListener.Unregister();

    Stop();
    ControlledPawn = nullptr;
    
    Super::EndPlay(EndPlayReason);
}
#

The logic of updating the path is very close to the old AutoRun function called from PlayerTick. The component has its own PlayerTick and has no external dependency, so the order of call is not important.

void UAutoRun_CC::PlayerTick(float DeltaTime)
{
    Super::PlayerTick(DeltaTime);
    
    if (!IsValid(ControlledPawn) || !bAutoRunning) return;
    
    const FVector LocationOnSpline = AutoRunSpline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);
    const FVector Direction = AutoRunSpline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);

    if ((LocationOnSpline - CachedDestination).Length() > AutoRunAcceptanceRadius)
    {
        ControlledPawn->AddMovementInput(Direction);
    }
    else
    {
        Stop();
    }
}

Pathfinding logic, very close to the original. Never called inside the module, as it expects a coordinate from an external controller.

void UAutoRun_CC::Start(const FVector& InDestination)
{
    Stop();
    
    if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), InDestination))
    {
        AutoRunSpline->ClearSplinePoints();
        for (const FVector PointLoc : NavPath->PathPoints)
        {
            AutoRunSpline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
        }
        if (NavPath->PathPoints.Num() > 0)
        {
            CachedDestination = NavPath->PathPoints.Last();
            bAutoRunning = true;
            UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ClickEffect, CachedDestination);
        }
    }
}

Stopping and clearing values

void UAutoRun_CC::Stop()
{
    if (!bAutoRunning) return;
    
    bAutoRunning = false;
    CachedDestination = FVector::ZeroVector;
    AutoRunSpline->ClearSplinePoints();
}
#

Callbacks for external management.

void UAutoRun_CC::OnAutoRunStartMessage(FGameplayTag InChannel, const FTargetLocationMessage& IncomingMessage)
{
    if (IncomingMessage.Pawn == ControlledPawn)
    {
        Start(IncomingMessage.TargetLocation);
    }
}

void UAutoRun_CC::OnAutoRunStopMessage(FGameplayTag InChannel, const FTargetLocationMessage& IncomingMessage)
{
    if (IncomingMessage.Pawn == ControlledPawn)
    {
        Stop();
    }
}
#

AutoRunRuntime.Build.cs
It is necessary to add dependencies that will be disabled when a module is disabled. It seems to me that we use NavigationSystem directly only in this place and we don't need to pull this dependency to the main project.

PublicDependencyModuleNames.AddRange(
    new string[]
    {
        "Core",
        "GameplayTags",
        "ModularGameplay",
    }
    );
        
PrivateDependencyModuleNames.AddRange(
    new string[]
    {
        "CoreUObject",
        "Engine",
        "Slate",
        "SlateCore",
        "NavigationSystem",
        "Niagara",
        "GameplayMessageRuntime",
        "CoreStructs",
    }
    );
#

I created a Native Gameplay Tag, but it's not necessary since the system is fully customizable.

FGameplayTag GameplayMessage_PlayerController_AutoRunStart;
FGameplayTag GameplayMessage_PlayerController_AutoRunStop;

...

GameplayTags.GameplayMessage_PlayerController_AutoRunStart = TagsManager.AddNativeGameplayTag(FName("GameplayMessage.PlayerController.AutoRunStart"));

GameplayTags.GameplayMessage_PlayerController_AutoRunStop = TagsManager.AddNativeGameplayTag(FName("GameplayMessage.PlayerController.AutoRunStop"));
paper root
#

AXPlayerController.h
Add in protected section

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="X|PlayerController|AutoRun",
    meta=(Categories="GameplayMessage"))
FGameplayTag AutoRunStartMessageChannel = FGameplayTag();

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="X|PlayerController|AutoRun",
    meta=(Categories="GameplayMessage"))
FGameplayTag AutoRunStopMessageChannel = FGameplayTag();

Add in private section

void AutoRunStartMessage() const;
void AutoRunStopMessage() const;

Remove next lines

FVector CachedDestination = FVector::ZeroVector;
bool bAutoRunning = false;
UPROPERTY(EditDefaultsOnly)
float AutoRunAcceptanceRadius = 50.f;

UPROPERTY(VisibleAnywhere)
TObjectPtr<USplineComponent> Spline;

UPROPERTY(EditDefaultsOnly)
TObjectPtr<UNiagaraSystem> ClickNiagaraSystem;

void AutoRun();
#

AXPlayerController.cpp
Remove from constructor

Spline = CreateDefaultSubobject<USplineComponent>("Spline");

Remove from PlayerTick

AutoRun();

Also place CursorTrace() before Super. It's not that important, but it's in Super that the PlayerTick components will be called.

CursorTrace();
Super::PlayerTick(DeltaTime);

Delete the function AXPlayerController::AutoRun()

paper root
#

Replace Pathfinding to AutoRunStartMessage() call

void AXPlayerController::AbilityInputTagReleased(FGameplayTag InputTag)

...

const APawn* ControlledPawn = GetPawn();
if (FollowTime <= ShortPressThreshold && ControlledPawn)
{
    if (GetASC() && !GetASC()->HasMatchingGameplayTag(FXGameplayTags::Get().Player_Block_InputPressed))
    {
        AutoRunStartMessage();
    }
}
#

Remove CachedDestination

CachedDestination = CursorHit.ImpactPoint;
...
const FVector WorldDirection = (CursorHit.ImpactPoint - ControlledPawn->GetActorLocation()).GetSafeNormal();
#

Replace all

bAutoRunning = false;

to

AutoRunStopMessage();
#

Messages

void AXPlayerController::AutoRunStartMessage() const
{
    if (APawn* ControlledPawn = GetPawn())
    {
        UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this);
        
        FTargetLocationMessage OutgoingMessage;
        OutgoingMessage.Pawn = ControlledPawn;
        OutgoingMessage.TargetLocation = CursorHit.ImpactPoint;
        
        MessageSubsystem.BroadcastMessage(AutoRunStartMessageChannel, OutgoingMessage);
    }
}

void AXPlayerController::AutoRunStopMessage() const
{
    if (APawn* ControlledPawn = GetPawn())
    {
        UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this);

        FTargetLocationMessage OutgoingMessage;
        OutgoingMessage.Pawn = ControlledPawn;
        
        MessageSubsystem.BroadcastMessage(AutoRunStopMessageChannel, OutgoingMessage);
    }
}
#

Start UE and create a Blueprint inside the AutoRun module. It is needed for customization and will not contain any logical part.

paper root
paper root
#

Setup GameplayTags and Niagara System.

#

Set the same GameplayTags inside BP_XPlayerController.

#

Most importantly, to connect the component, we specify the receiving actor and our created BP_AutoRun_CC.