Featured image of post Async Actions

Async Actions

A digusting silver bullet to null playerstates.

Difficulty | Easy ⚖️

In this guide i’m going to upset a lot of network programmers.

Have you ever been really annoyed that the player state isn’t available on Begin Play? That annoying problem in the design of Gameplay Framework where you have to wait for your own playerstate to replicate down, and it has a lower net priority than your character so it gets sent in a later batch? Yea that one. Well this isn’t so much an issue in C++ because there’s a few hooks, but in Blueprint, damn inconvenient. Over the years I have tried and seen many attempts to wrangle the gameplay framework into working nicely here that range from the comically overengineered to clinically insane.

There is only 1 silver bullet I have seen that never fails and doesn’t have edge cases, and it’s a very disgusting one - SetTimerNextTick(). Most people will understandably try extremely hard to try to bind to delegates or to generally avoid using timers in this way, and in Blueprint it’s especially digusting to do this code and make it robust. But honestly, after years of fighting with this problem this is the most elegant solution I’ve found and the least amount of lines.

So lets cut to the chase, here’s the code:

AsyncAction_PlayerStateReady.h

#pragma once

#include "Kismet/BlueprintAsyncActionBase.h"
#include "ASyncAction_PlayerStateReady.generated.h"

class UWorld;

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPlayerStateReadyAsyncDelegate);

/**
 * Asynchronously waits for the playerstate to be ready and valid and then calls the OnReady event.  Will call OnReady
 * immediately if the game state is valid already.
 */
UCLASS()
class UAsyncAction_PlayerStateReady : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
public:
	
	UFUNCTION(BlueprintCallable, meta=(WorldContext = "WorldContextObject", BlueprintInternalUseOnly="true", DefaultToSelf="WorldContextObject"))
	static UAsyncAction_PlayerStateReady* WaitForPlayerStateReady(UObject* WorldContextObject);

	virtual void Activate() override;

	UPROPERTY(BlueprintAssignable)
	FPlayerStateReadyAsyncDelegate OnReady;

private:

	void Step1_AwaitPlayerStateValid();
	void Step2_BroadcastReady();

	TWeakObjectPtr<UObject> CalleePtr;
	TWeakObjectPtr<UWorld> WorldPtr;
};

AsyncAction_PlayerStateReady.cpp

#include "Async/AsyncAction_PlayerStateReady.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"

UAsyncAction_PlayerStateReady* UAsyncAction_PlayerStateReady::WaitForPlayerStateReady(UObject* WorldContextObject)
{
	UAsyncAction_PlayerStateReady* Action = nullptr;

	if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
	{
		Action = NewObject<UAsyncAction_PlayerStateReady>();
		Action->CalleePtr = MakeWeakObjectPtr(WorldContextObject);
		Action->WorldPtr = MakeWeakObjectPtr(World);
		Action->RegisterWithGameInstance(World);
	}

	return Action;
}

void UAsyncAction_PlayerStateReady::Activate()
{
	if (UWorld* World = WorldPtr.Get())
	{
		Step1_AwaitPlayerStateValid();
	}
	else
	{
		// No world so we'll never finish naturally
		SetReadyToDestroy();
	}
}

void UAsyncAction_PlayerStateReady::Step1_AwaitPlayerStateValid()
{
	if(!WorldPtr.IsValid()) // World went kaput.
	{
		SetReadyToDestroy();
		return;
	}

	APlayerController* PC = nullptr;

	if(auto* Pawn = Cast<APawn>(CalleePtr.Get())) // Path for Pawns like bots to directly get their PS.
	{
		PC = Pawn->GetController<APlayerController>();
	}
	else if(auto* Self = Cast<APlayerController>(CalleePtr.Get())) // Path for PlayerControllers.
	{
		PC = Self;
	}
	else // Path for anything else, like an external caller that isn't a pawn.
	{
		PC = WorldPtr.Get()->GetFirstPlayerController();
	}
	
	if(PC && PC->GetPlayerState<APlayerState>()) // PlayerState is valid, we're done.
	{
		Step2_BroadcastReady();
		return;
	}

	// No valid playerstate repped down yet, check again next tick.
	WorldPtr->GetTimerManager().SetTimerForNextTick(
		FTimerDelegate::CreateUObject(this, &ThisClass::Step1_AwaitPlayerStateValid));
}

void UAsyncAction_PlayerStateReady::Step2_BroadcastReady()
{
	OnReady.Broadcast();
	SetReadyToDestroy();
}

With this we are able to make nodes like the following:

alt text

Now whenever you are in a place where you can’t reliably get your playerstate you can just use the async action to wait for your playerstate, the primary execution pin will fire straight away and the on ready pin will fire when you can safely get your playerstate without it being null. This is a pretty easy example usage where async actions can be extremely useful, if you are familiar with Lyra you will probably know that they do basically this exact same method for experiences, but it’s definitely not limited only to these use cases.

Built with Hugo
Theme Stack designed by Jimmy