Accessing owner of PlayerState on multiplayer client

I have a custom property on a PlayerState, “craftColor”, that determines the color of the possessed pawn’s static mesh. I have this property set to replicate with a ReplicatedUsing callback.

The problem is that when I receive the replicated callback on a client, I don’t have an obvious way to tie the current PlayerState to any specific pawn. GetOwner() returns null. I understand that PlayerControllers don’t exist on all clients, but I was hoping there would still be a way to go from PlayerState → Possessed Pawn.

I may be able to use PlayerState->PlayerId to find the pawn, but at that point it feels like I’m fighting the system.

Am I using PlayerState incorrectly? It really seems like the correct place to put something like a player selected color. Should I move the “craftColor” variable to the Pawn, or is there some access method I’m failing to see?

Could you not also do this through the Pawn itself? The Pawn has a reference to the PlayerState so you could just watch for changes in the Pawn’s tick.

If you wanted to do something Async you could setup a delegate that gets fired whenever you replicate the color variable.

Yeah, color could live on the Pawn but it does make me wonder exactly what PlayerState is for. It seems awkward that the intent is to store per-player information, but there isn’t a clear way for it to access the possessed pawn to apply any of this information.

Sorry, wasn’t clear. I mean you have access to the player state inside the pawn, you don’t need access to the pawn in the player state. Either setup a delegate inside the player state that triggers a function on the pawn when the color changes, or constantly check inside the pawns tick.

No, but what you could do is something like this, declared in your custom PlayerState header.

HEADER
-------------
DECLARE_DELEGATE(FOnPlayerColorChange)

class AMyPlayerState : APlayerState
{
	GENERATED_BODY()
public:
	UPROPERTY(ReplicatedUsing=OnRep_Color)
	FLinearColor PlayerColor;
	FOnPlayerColorChange OnColorChangeDelegate;

	UFUNCTION()
	void OnRep_Color();
};


SOURCE
-------------
void AMyPlayerState::OnRep_Color()
{
	OnColorChangeDelegate.ExecuteIfBound();
}

And then in your custom Pawn header.

HEADER
-------------
class AMyPawn : APawn
{
	GENERATED_BODY()
public:
	virtual void PostInitializeComponents() override;
	void OnPlayerColorChange();	
};


SOURCE
-------------
void AMyPawn::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	AMyPlayerState* MyPlayerState = Cast<AMyPlayerState>(PlayerState);
	MyPlayerState->OnColorChangeDelegate.BindUObject(this, &AMyPawn::OnPlayerColorChange);
}

void AMyPawn::OnPlayerColorChange()
{
	AMyPlayerState* MyPlayerState = Cast<AMyPlayerState>(PlayerState);
	FLinearColor NewColor = MyPlayerState->PlayerColor;
	//DO COLOR RELATED STUFF
}

I’d rather not poll in tick if possible.

What you said about a delegate inside PlayerState that triggers a function on Pawn is what I’m requesting, if I’m understanding you correctly. The problem is you can’t trigger anything on a Pawn that you know nothing about. I suppose I could do something like add a variable to PlayerState that points to the Pawn, and initialize it on initial replication… but that is starting to get convoluted.

After pondering it some more, I think the solution in this particular instance is to simply move the variable to Pawn and replicate it there. PlayerState seems better suited for global information you’d display on a scoreboard – not information specific to the pawn.

Thanks for that example! It is my first time to see the DECLARE_DELEGATE syntax… seems handy!

Funny that is exactly what I’ve tried! Unfortunately this doesn’t help in my case, I have Player1 as Listen Server, here’s the order of events for Player2’s client:
Character::Character() // P1 and P2
Character::PostInitializeComponents() // P1 and P2
Character::BeginPlay() // P1 and P2
// up to here, impossible to bind to an event from PlayerState as it gets created right after

PlayerState::PlayerState() // P1 and P2
PlayerState::On_RepTeamColor() // P1 and P2

PlayerState::ClientInitialize() // P2 Only
Character::PawnClientRestart() // P2 Only
Character::Restart() // P2 Only → Here I can check the PlayerState for P2 but not P1
So in the end, P2 doesn’t see the proper TeamColor for P1’s Character (even if his PlayerState has the right Team data) I couldn’t find a method in Character that is called once the PlayerState is set for said Character.

So next test for me is trying to set the Character’s color from PlayerState OnRep_TeamColor. Crossing fingers! :stuck_out_tongue:

In fact, after overriding a ton of methods everywhere just to log and see the order of events, I found that overriding APawn::OnRep_PlayerState() was the thing I was looking for!

1 Like

…and override virtual void PossessedBy(AController* NewController); so it works on the server too as OnRep is obviously not called :stuck_out_tongue:
So I end up with the following, also using a delegate in case of Team changes during the session.
While you’re here, I’ve also included the color change code that works on the default UE Character. Ps. TeamId is set in the PlayerState from a custom game mode in PostLogin(). Have fun!

void AGameCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	SyncTeamFromPlayerState();
}

void AGameCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	SyncTeamFromPlayerState();
}

void AGameCharacter::SyncTeamFromPlayerState()
{
	if (AVsPlayerState* VsPS = Cast<AVsPlayerState>(GetPlayerState()))
	{
		UpdateTeam(VsPS->GetTeamId());
		VsPS->OnTeamIdChanged.AddDynamic(this, &AGameCharacter::UpdateTeam);
	}
	else
	{
		ensureMsgf(false, TEXT("AUEPhoneGameCharacter expects a AVsPlayerState!"));
	}
}
void AGameCharacter::UpdateTeam(uint8 inTeamID)
{
	if (inTeamID == 0)
	{
		SetTeamColor(FVector(1.f, 0.f, 0.f));
	}
	else if (inTeamID == 1)
	{
		SetTeamColor(FVector(0.f, 0.f, 1.f));
	}
}

void AGameCharacter::SetTeamColor(FVector NewColor)
{
	USkeletalMeshComponent* MeshComponent = Cast<USkeletalMeshComponent>(GetComponentByClass(USkeletalMeshComponent::StaticClass()));
	if (MeshComponent)
	{
		UMaterialInstanceDynamic* MITeam = UMaterialInstanceDynamic::Create(MeshComponent->GetMaterial(0), this);
		MITeam->SetVectorParameterValue(FName(TEXT("BodyColor")), FLinearColor(NewColor));
		MeshComponent->SetMaterial(0, MITeam);
	}
}
1 Like