Generic Network Action System - Extracting RPC method definitions

Hi everybody,

i am currently trying to write a generic action system for ingame events where you can easily dispatch an

action locally, to the server or to both. In addition you can register callbacks to handle the execution of all actions (preExecute, onExecute and afterExecute).

Dispatching actions is completely handled by the GameActionDispatcher ( one is spawned for each player and stored in the player controller) itself and as easy as calling a single function:

GetGameActionDispatcher(this)->ExecuteAction<FThrowAction, ActionExecutionType::Net>(GetPawn(), force);

An action is basically just a simple struct containing the parameter data like this:

USTRUCT()
struct BOUNCE_API FThrowAction : public FGameAction
{
    GENERATED_USTRUCT_BODY()
    
public:
    FThrowAction();

    void init(AActor* actor, FVector2D force);

    UPROPERTY()
    TWeakObjectPtr<AActor> Actor;

    UPROPERTY()
    FVector2D Force;
};

This is the base of the Dispatcher implementation:

UCLASS()
class BOUNCE_API AGameActionDispatcher : public AActor
{
    template<class ActionClass, ActionExecutionType Type>
    friend class TypeBasedGameActionExecution;

    GENERATED_BODY()

public:
    AGameActionDispatcher();

    template<class ActionClass, ActionExecutionType Type, typename ... Params>
    void ExecuteAction(Params... params);

private:
    **//list of RPC methods - THIS HAS TO BE IN AN EXTRA USER DEFINED CLASS**
    UFUNCTION(Server, WithValidation, Reliable)
    void executeServer(FThrowAction action);
    //... further rpc calls

    // wrapper methods for local and net execution
    template<class ActionClass>
    void ExecuteNet(const ActionClass& action);

    void ExecuteLocal(const FGameAction& action);
};

template<class ActionClass, ActionExecutionType Type, typename ... Params>
void AGameActionDispatcher::ExecuteAction(Params... args)
{
    ActionClass action;
    action.init(args...);

    TypeBasedGameActionExecution<ActionClass, Type>::run(this, action);
}

// template implementation
template<class ActionClass>
void AGameActionDispatcher::ExecuteNet(const ActionClass& action)
{
    ExecuteLocal(action);

    if (Role != ROLE_Authority)
    {
        executeServer(action);
    }
}

Further relevant code can be found further down.

To keep it as simple callable as possible with minimal effort of writing new functions,
all the magic in the background is heavily template based because for the RPC Calls i need to specify the correct Action type (obviously sending non replicated pointers does not work so i cannot take advantage of polymorphism).

These RPC calls are exactly my problem. Currently these method specifications are directly inside of my Dispatcher class. However in terms of reusability and potentially making a library out of it, i would like to extract them so the user can specify them in terms of their needs.

Ideas i tried:

  • Defining the RPC methods in a macro and place that call in the Dispatcher header → fails because the UBT runs before evaluating the macro

  • Extract the RPC methods into an “BaseRPCChannelComponent” class deriving from UActorComponent. Then add a setter for the rpc channel to the dispatcher and call rpcchannel->executeServer(…). Problem is since the base channel class obviously does not have the executeServer rpc functions. I would also need to concrete RPCChannelComponent type of the user created one here.

    UCLASS()
    class BOUNCE_API UBaseRPCChannelComponent : public UActorComponent
    {
    friend class AGameActionDispatcher;

     GENERATED_BODY()
    

    public:
    UBaseRPCChannelComponent();

    private:
    // Derive from this class and list server rpc calls for each game action like this
    /UFUNCTION(Server, Reliable, WithValidation)
    void ExecuteServer(ActionClass action);
    /

     // auto generate template validate and implementation functions for server methods
     template<class ActionClass>
     bool ExecuteServer_Validate(ActionClass action);
    
     template<class ActionClass>
     void ExecuteServer_Implementation(ActionClass action);
    
     AGameActionDispatcher* GetDispatcher();
    

    };

    template
    bool UBaseRPCChannelComponent::ExecuteServer_Validate(ActionClass action)
    {
    return action.validate();
    }

    template
    void UBaseRPCChannelComponent::ExecuteServer_Implementation(ActionClass action)
    {
    GetDispatcher()->ExecuteLocal(action);
    }
    void AGameActionDispatcher::UseRPCChannel(TSubclassOf channelClass)
    {
    if (RPCChannel)
    {
    RPCChannel->DestroyComponent();
    }

     RPCChannel = ConstructObject<UBaseRPCChannelComponent>(channelClass, this, FName(TEXT("RPCChannelComponent")));
    

    }

    // template implementation
    template
    void AGameActionDispatcher::ExecuteNet(const ActionClass& action)
    {
    ExecuteLocal(action);

     if (Role != ROLE_Authority && RPCChannel)
     {
         RPCChannel->executeServer(action);
     }
    

    }

  • Ok since i need the RPC channels concrete type i came to templates again. And because i have to store the RPC channel as a member variable i would have to make the Dispatcher itself a template which does not work with UCLASS definitions… So i could led the user derive from my templated dispatcher with the rpc channel. But then again i do not know the users dispatcher class to refer to it and store in the player controller

I think u see where i am stuck now xD

Do you have any ideas on how to solve this? Or maybe know about a completely different approach than what i tried?

Greetings.

TypeBasedGameActionExecution - Partially specialized template class to only generate the executeNet and executeServer methods when the execution type requires it.

template<class ActionClass, ActionExecutionType Type>
class TypeBasedGameActionExecution
{
public:
    static void run(AGameActionDispatcher* dispatcher, ActionClass action)
    {
        // do nothing, work is done based on type in specialized templates
    }
};

template<class ActionClass>
class TypeBasedGameActionExecution < ActionClass, ActionExecutionType::Local >
{
public:
    static void run(AGameActionDispatcher* dispatcher, ActionClass action)
    {
        dispatcher->ExecuteLocal(action);
    }
};

template<class ActionClass>
class TypeBasedGameActionExecution < ActionClass, ActionExecutionType::Server >
{
public:
    static void run(AGameActionDispatcher* dispatcher, ActionClass action)
    {
        dispatcher->executeServer(action);
    }
};

template<class ActionClass>
class TypeBasedGameActionExecution < ActionClass, ActionExecutionType::Net >
{
public:
    static void run(AGameActionDispatcher* dispatcher, ActionClass action)
    {
        dispatcher->ExecuteNet(action);
    }
};

I found another big issue with my approach…

U cannot define multiple RPC server functions with the ame name that differ by parameter types…