Handling Ability activation after posession change on multiplayer client (GAS)

Hello,

In our multiplayer game, each player always has two characters spawned, and they can switch control between them at any time. We are using GAS for their abilities and it’s working fine.

What we want to achieve now is something like the following:

Player is controlling character A and starts pushing a button. This causes the camera (and posession) to change to character B, allowing the player to aim. When the button is released, character B shoots a projectile and camera and posession goes back to character A.

What I’ve tried so far is having 2 different abilities. Ability A handles possessing character B and making it activate Ability B, waiting for it to end. Ability B handles the aiming, waiting for the input release and shooting the projectile.

Ability A code looks something like:

void UAbility_A::ActivateAbility(...)
{
    // ...
          
    // Possess Character B
    if (HasAuthority(&CurrentActivationInfo))
    {
       AOurGameMode::ChangePossession(Character_A, Character_B);
    }

    // Activate Ability B in Character B
    Character_B->GetAbilitySystemComponent()->TryActivateAbilityByClass(UAbility_B::StaticClass());

    // Task that waits for Ability B to end
    UAT_WaitAbilityEnd* WaitAbilityEndTask = UAT_WaitAbilityEnd::CreateWaitAbilityEndTask(this, UAbility_B::StaticClass(), Character_B);
    WaitAbilityEndTask->OnAbilityEndedDelegate.AddDynamic(this, &ThisClass::OnAbilityBEnded);
    WaitAbilityEndTask->ReadyForActivation();
}

void UAbility_A::OnAbilityBEnded(UGameplayAbility* EndedAbility)
{
    // ...

    // Possess ourselves back
    if (HasAuthority(&CurrentActivationInfo))
    {
       AOurGameMode::ChangePossession(Character_B, Character_A);
    }

    // End Ability
    EndAbility(...);
}

This is working fine when playing as the server, but not on clients though.

Since possession is done in server, when client tries to force Character B to activate Ability B, it fails because Character B is a simulated proxy.

Also, when ability is executed server side, Ability B activation replication to client fails too, since apparently when the client RPC gets to client, Character B is still simulated proxy.

I’ve also tried having a task that makes client wait for the controller replication to Character B after posession, but in that exact moment Character B is still a simulated proxy, and haven’t found a way to be notified when the Role changes.

Is there any way that we could make this work? Is this approach reasonable at all? Or maybe what we want to achieve is better done in a different way. Any hint on this would be very much appreciated.

Thanks a lot for your time.

Hey there, is the AbilitySystemComponent in your setup on the character(s) or on the PlayerState, or on some other class?

The possession part of this ability makes things quite complex, because that must happen server-side and the information that the player now possesses character B needs to travel to the client. I take it Ability_A is configured as LocalPredicted so it executes both on client and server? I would then do the following:

  • Let the owning client (CharA->LocalRole >= AutonomousProxy) listen to CharB changing possession to the current player (CharB->ReceiveControllerChangedDelegate.Add… ).
  • Only then try activating Ability_B on CharB.

I might change my answer depending on:

  • Which ASC(s) owns Ability_A and Ability_B and which actor owns those ASCs
  • Whether Ability_A and Ability_B should be local predicted (depends on what other behavior they have), or can be ServerOnly

So if you need more guidance, that info would be useful to know!

Hello!

“Character A has its ASC in a different Actor”

Great!

“I’ve already tried waiting for possession change, listening to ReceiveControllerChangedDelegate in an AbilityTask, but when this fires in client, Character B Role seems to be still Simulated Proxy. If you think this might be a way to achieve it, I’ll test it more in depth.”

I do think you have to wait for possession change. However, the pawn’s Controller and Role are separate variables that don’t necessarily replicate in the same frame or in the same order. For Ability_B to activate successfully, at least the following conditions must be met:

  • [unpredictable latency] Client-side CharB’s controller has been set to the player’s controller. This is observable via ReceiveControllerChangedDelegate.
  • [unpredictable latency] Client-side CharB’s Role is updated to AutonomousProxy. AActor::Role doesn’t have an OnRep function, so unfortunately you will have to poll it in some tick function.
  • After both above conditions are met, you should call InitAbilityActorInfo on CharB’s ASC.
  • Then CharB’s Ability_B can finally be activated client-side, triggering local prediction and replication to server.

Those first two points, if you will have multiple of these abilities I think it would be useful to wrap those in a custom ability task that

  • Server-side triggers possession of the other character.
  • Client-side manages registering/unregistering to ReceiveControllerChangedDelegate and then polling the target pawn’s Role. So a reusable ability task class to help with scheduling.
  • In GA blueprints: either the server and client both receive the same target pawn as parameter. Or (safer) the client calls a server RPC to provide the pawn it wants to possess. (Also validate it server-side to prevent cheating).

That way, the custom ability task can be reused for other abilities.

Glad to hear that! I’ll await an update.

Still have to round some edges but it’s working now! Here is my current code for the AbilityTask that waits for the possession, in case anyone finds it useful

.h

#pragma once
 
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "AT_WaitPossession.generated.h"
 
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPossessionComplete, APawn*, Pawn);
 
/**
 * Task that waits for a Pawn to be possessed
 */
UCLASS()
class MYPROJECT_API UAT_WaitPossession : public UAbilityTask
{
	GENERATED_BODY()
 
public:
	UAT_WaitPossession();
	
	FOnPossessionComplete OnPossessionCompleteDelegate;
	
	static UAT_WaitPossession* CreateWaitPossessionTask(UGameplayAbility* OwningAbility, APawn* InPawn);
 
protected:
	virtual void Activate() override;
	UFUNCTION()
	void OnReceiveControllerChange(APawn* PossessedPawn, AController* OldController, AController* NewController);
	virtual void TickTask(float DeltaTime) override;
	virtual void OnDestroy(bool bInOwnerFinished) override;
 
	UPROPERTY()
	TWeakObjectPtr<APawn> Pawn;
 
	bool bPollLocalRole = false;
};

.cpp

#include "AT_WaitPossession.h"
 
UAT_WaitPossession::UAT_WaitPossession()
{
	bTickingTask = true;
}
 
UAT_WaitPossession* UAT_WaitPossession::CreateWaitPossessionTask(UGameplayAbility* OwningAbility, APawn* InPawn)
{
	UAT_WaitPossession* MyObj = NewAbilityTask<UAT_WaitPossession>(OwningAbility);
	MyObj->Pawn = InPawn;
	return MyObj;
}
 
void UAT_WaitPossession::Activate()
{
	if (APawn* PawnPtr = Pawn.Get())
	{
		if (PawnPtr->HasAuthority())
		{
			OnPossessionCompleteDelegate.Broadcast(PawnPtr);
			EndTask();
		}
		else
		{
			PawnPtr->ReceiveControllerChangedDelegate.AddUniqueDynamic(this, &ThisClass::OnReceiveControllerChange);
		}
	}
	else
	{
		EndTask();
	}
}
 
void UAT_WaitPossession::OnReceiveControllerChange(APawn* PossessedPawn, AController* OldController, AController* NewController)
{
	if (PossessedPawn->GetLocalRole() == ROLE_AutonomousProxy)
	{
		OnPossessionCompleteDelegate.Broadcast(PossessedPawn);
		EndTask();
	}
	else
	{
		bPollLocalRole = true;
	}
}
 
void UAT_WaitPossession::TickTask(float DeltaTime)
{
	if (bPollLocalRole)
	{
		if (APawn* PawnPtr = Pawn.Get())
		{
			if (PawnPtr->GetLocalRole() == ROLE_AutonomousProxy)
			{
				OnPossessionCompleteDelegate.Broadcast(PawnPtr);
				EndTask();
			}
		}
		else
		{
			EndTask();
		}
	}
}
 
void UAT_WaitPossession::OnDestroy(bool bInOwnerFinished)
{
	Super::OnDestroy(bInOwnerFinished);
	
	if (APawn* PawnPtr = Pawn.Get())
	{
		PawnPtr->ReceiveControllerChangedDelegate.RemoveDynamic(this, &ThisClass::OnReceiveControllerChange);
	}
}

For now I still do the possession outside the task but maybe I’ll add it too as you suggested.

Thank you so much for your help!

That looks great! Glad to have helped. I’ll close this.

Hey Zhi! Thanks for your response.

I’ve already tried waiting for possession change, listening to ReceiveControllerChangedDelegate in an AbilityTask, but when this fires in client, Character B Role seems to be still Simulated Proxy. If you think this might be a way to achieve it, I’ll test it more in depth.

About our ASC setup:

There are 2 ASCs, one owning Ability_A and the other owning Ability_B

Character A has its ASC in a different Actor (We do this to be able to destroy Character A and keep Attributes, GameplayEffects, etc working without having an Avatar spawned).

Character B has its own ASC.

Both abilities are indeed local predicted.

Hope this info its useful, thank you again!

Hey! Thank you for the thorough answer!

I’ll try adding the Role polling and will report back with the results.

Thanks!