Simulate Server Only Logic on Clients

I am writing an ability system and have come to the point where I need to simulate certain things on the client.
Some examples are changing materials, playing particle systems, playing sound effects, etc.

This is an example of something I need to execute on the Client for a cloaking ability.

A quick summary of my ability system design is…

  • UAbilityComponent - UActorComponent that exists on the Server and is replicated to Clients. It is attached to a Pawn that can execute abilities. Is the driver of all abilities. In charge of spawning, replicating, and enforcing rules around abilities.
  • UAbility - UObject that only exists on the Server (don’t replicate so it is more lightweight). Defines an abilities properties and effects. Does not maintain state.
  • FAbilityState - Replicated ability state hosted by UAbilityComponent. Holds the current values of properties that we want to replicate.

I’m am having a hard time figuring out the most lightweight and clean way to implement Ability Simulation.

Normally I would just do something like the following.

// AbilityComponent (ActorComponent)

void AbilityComponent::Multicast_SimulateAbility(int32 ID)
{
	Abilities[ID]->SimulateAbility();
}

// Ability (UObject)

UFUNCTION(BlueprintImplementableEvent, Category = Ability)
void SimulateAbility();

void Ability::Execute()
{
	AbilityComponent->Multicast_SimulateAbility(ID);
}

That would allow me to override the SimulateAbility function in BP and give me a shell for executing whatever I need to on the client.

However, the issue with that approach is that my UAbility does not exist on the clients (since I want to stay lightweight).

Therefore, I can’t go this route.

Is there some other way to call logic defined on a server only object on the client?

If not, the best way I can think to do this is to create various replicated properties on the UAblityComponent via OnRep.

For example,

  • CharacterMaterial
  • ParticleSystem
  • SoundEffect
  • anything else I end up needing

I could then have an OnRep function in UAbilityComponent that actually sets the characters material, plays the particle system, etc.

I would then set these properties from within UAbility to trigger the simulation.

It is a lot less flexible and requires me to keep adding properties to UAbilityComponent.

Can anyone think of anything better?

To be able to replicate the abilities logic (even if it’s just client logic and effects) you can just replicate the ability classes and instance them on each simulated/autonomous proxy by hand. I would really use the same classes on all sides even if they are not linked so you are able to reuse the same code paths. So using ‘UAbilityComponent’ and replicating the current ability class which you then instantiate on the clients would be a good and clean way to simulate your logic, all the info about the effects should be in the ability class.

The reason I separate Ability and the State of an ability is because it’s cheaper to replicate those properties rather than the entire abilities.

I was thinking about possibly instantiating the UAbility objects on the client as well (but not replicating). That way I would at least have the Ability simulation logic on the client. That seemed a bit messy though.

Now, what do you mean by replicating the ability classes and instancing them by hand? You lost me a bit.

I mean that you just send the ‘class’ over to the client not the instance. For example you just send the currently active ability class and then you instantiate it on the client side directly. You can then use that instance to drive your simulation.

That sounds promising. Can you provide an example? Would you send the active ability every time an ability is executed? And then clean it up once it is finished executing?

You can do it when you activate it, or when you select it. Depending on your setup you might send more that one class so you can optimize your bandwidth a bit more. The idea behind this is having the same code path on both sides. You can even just use a bit-mask with flags to indicate the active abilities, tons of ways to make it as small and fast as possible.

I am doing something kind of similar already for the animations. However, I’m not instantiating the UAbility since I was just dealing with playing animations. I am setting this ActiveAbility in Ability::Start(). Most of the simulation stuff should probably occur in Ability::Execute() though.

// ****UAbilityComponent.h****
USTRUCT()
struct FFPSActiveAbility
{
    GENERATED_BODY()
​
    UPROPERTY()
    int32 ID;
​
    UPROPERTY()
    UAnimMontage* AnimationTP;
​
    UPROPERTY()
    UAnimMontage* AnimationFP;
};
​
UPROPERTY(Transient, ReplicatedUsing = OnRep_ActiveAbility)
FFPSActiveAbility ActiveAbility;

UFUNCTION(BlueprintCallable, Category = Animation)
void SetActiveAbility(int32 AbilityID, UAnimMontage* AnimMontageTP, UAnimMontage* AnimMontageFP);

// ****UAbilityComponent.cpp****
void UFPSAbilityComponent::SetActiveAbility(int32 AbilityID, UAnimMontage* AnimMontageTP, UAnimMontage* AnimMontageFP)
{
    ActiveAbility.ID = AbilityID;
    ActiveAbility.AnimationTP = AnimMontageTP;
    ActiveAbility.AnimationFP = AnimMontageFP;
}
​
void UFPSAbilityComponent::OnRep_ActiveAbility()
{
    if (ActiveAbility.AnimationTP)
    {
        AFPSCharacter* Character = Cast<AFPSCharacter>(GetOwner());
        // Play the Third Person Animation
        Character->PlayAnimMontage(ActiveAbility.AnimationTP);
        // Play the First Person Animation
        Character->PlayAnimMontageFP(ActiveAbility.AnimationFP);
    }
}
​
// ****UAbility.cpp****
void UFPSAbility::Start()
{
	// Set the active animation to start the animation on the client
	AbilityComponent->SetActiveAbility(ID, AnimationTP, AnimationFP);
}

Bitmask with flags wow that sounds awesome. I would love to see an example setup for this. Show me your jedi ways :slight_smile:

Start() is called on player input and then calls Execute() either immediately or via an anim notify.

Right now Start and Execute are being called on the server only. Start is called via a server rpc from abilitycomponent. And Execute is called via Start or a custom anim notify.

So once I get the UAbility on the client how would I execute SimulateAbility? I would have thought to call it from Execute. However that if a server rpc (Start) is calling Execute then that simulate logic would still only run on the server.

You have to decide what you want to run on the server and what on the client. The new UT has a similar approach for their impacts, the idea behind it is to be able to simulate the effects on each side without really replicating everything (https://github.com/EpicGames/UnrealTournament/blob/clean-master/UnrealTournament/Source/UnrealTournament/Public/UTImpactEffect.h).

I was uploading a project to my github that features a simple powerup system (while mine are actors though it might help you a bit).

If I where you I would use the CDO of the ability class on the client. Then you have 2 options: first you could instatiate a new instance on that client and call your ‘SimulateAbility’ method, or (this one might be cleaner) get the configured ‘UTImpactEffect’ type class (you would have to create your own of course) to simulate the effects.

The second option gives you the flexibility to plug in different effects for different abilities by just having a static class of the current active ability. So the only thing you have to send to the client or to maintain on their end is which ability class is currently being used by that client.