RPC Calls to Unowned Actors Not Working (C++ networking)

Hi guys,

Been tearing my hair out over this one for some time, and still not sure what the best approach is. As such, any help would be extremely appreciated.

I’ll quickly outline my situation and approach so far: I have a top-down multiplayer RPG game. When a player presses a button, they’ll swing a sword, and this should be seen on the server and on all connected clients. When the sword connects with another actor, that actor will play a “hurt/recoil” animation, to show they have taken damage.

So far, swinging a sword is seen on all clients currently connected, plus the server, regardless of whether the client is swinging or the server is. So that’s working as expected. I encountered problems when trying to play the “recoil” animation, because that’s me asking another actor, that I don’t own, to do something.

A quick current structure of how this works:

Character class has an AnimationHelper class, which has various functions to process animations etc. This is used to play the swing animation, and formerly the recoil animation. A quick demonstration of the code header:

 ///////////////////PLAY ANIMATIONS
 void PlayAnimation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable);

 UFUNCTION(Server, Reliable, WithValidation)
 void ServerPlayAnimation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable);
 virtual bool ServerPlayAnimation_Validate(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable) { return true; };
 virtual void ServerPlayAnimation_Implementation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable);

 UFUNCTION(NetMulticast, Reliable)
 void MulticastPlayAnimation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable);
 void MulticastPlayAnimation_Implementation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable);

PlayAnimation looks like this:

void UCharacterAnimationManager::PlayAnimation(ARoguelikeCharacter* character, UAnimMontage* animName, float speed, bool requiresStationary, bool interruptable)
{
 if (character->HasAuthority())
 {
  MulticastPlayAnimation(character, animName, speed, requiresStationary, interruptable);
 }
 else
 {
  ServerPlayAnimation(character, animName, speed, requiresStationary, interruptable);
 }
}

I had tried to follow this pattern to do the “recoil” functions as well. Unfortunately, trying to call the function via character->AnimationHelper->PlayHitReaction was disallowed, because the requester was not the owner of that actor.

After some research, it seems a character cannot call another actors functions if they do not own them. What I did read is that this may be possible if we go via the PlayerController. So, I created a new class, AnimationRequester, and created a member of this type on the PlayerController class. This is instantiated at BeginPlay.

It looks like this:

UCLASS()
class ROGUELIKE_API UCharacterAnimationRequester : public UObject
{
 GENERATED_BODY()

public:
 void RequestPlayHitReaction(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false);
 
 UFUNCTION(Server, Reliable, WithValidation)
 void ServerPlayHitReaction(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false);
 virtual bool ServerPlayHitReaction_Validate(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false) { return true; }
 virtual void ServerPlayHitReaction_Implementation(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false);

 UFUNCTION(NetMulticast, Reliable)
 void MulticastPlayHitReaction(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false);
 void MulticastPlayHitReaction_Implementation(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack = false);
};

And request play looks like this:

void UCharacterAnimationRequester::RequestPlayHitReaction(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack)
{
 if (instigator->HasAuthority())
 {
  target->Animator->PlayHitReactionInternal(instigator, target, hitSocket, force, hitLocation, knockedBack);
  MulticastPlayHitReaction(instigator, target, hitSocket, force, hitLocation, knockedBack);
 }
 else
 {
  ServerPlayHitReaction(instigator, target, hitSocket, force, hitLocation, knockedBack);
 }
}

Extremely similar to the other PlayAnimation function. The implementation is a little different.

This obviously doesn’t work. The animation will play for the person who swung the weapon and caused the damage, but does not show on any other instance (no matter if it was the client or the server who did the action).

If I put a breakpoint on the functionality, it only appears to be called once - when I expected once per connection.

So a quick rundown again: when an enemy is hit by a sword, the RequestPlayHitReaction function is called, and I expect the animation etc. to play across all connected instances of the game. The function is on an object derived from UObject held as a member within my PlayerController class.

So my questions are: is this a dreadful approach? Is there a simpler way? Am I missing something obvious as to why the animation isn’t playing for all? And: what is the best means of calling functions on unowned actors when RPC functions are involved?

Thanks a lot - really frustrating stuff to figure out. Any and all help greatly appreciated.

Hey,

it seems to me that you cannot use RPC in the class that inherits from UObject, based on this: [click][1]

==========================================================================================

I’m not tested this, but here have some concept.

Warrior.cpp (ACharacter)

// When Client or Server press AttackButton
void AWarrior::AttackPressed()
{
	if (Role < ROLE_Authority) // client want attack, but server must allow
	{
		ServerAttackAction();
	}
	else // if this is called from server then tell to all to use MulticastAttackAction()
	{
		MulticastAttackAction();
	}
}

// UFUNCTION(Server, Reliable, WithValidation)
void AWarrior::ServerAttackAction()
{
	// Server tell to all to use MulticastAttackAction()
	MulticastAttackAction();
}

// This will be executed for everyone
// UFUNCTION(NetMulticast, Reliable)
void AWarrior::MulticastAttackAction()
{
	// without checking HasAuthority()
	// play attack animation
	Mesh->PlayAnimation(...);
}

In Weapon class I have event OnHit that is called when weapon hit something. OnHit is called only from server.

Weapon.cpp

void AWeapon::OnHit(
	UPrimitiveComponent * HitComponent,
	AActor * OtherActor,
	UPrimitiveComponent * OtherComp,
	FVector NormalImpulse,
	const FHitResult& Hit
)
{
	if (Role == ROLE_Authority) // only server (or HasAuthority())
	{
		if (OtherActor)
		{
			if (OtherActor->GetClass()->IsChildOf<AWarrior>()) // check if the hit actor is a AWarrior
			{
				AWarrior * TargetWarrior = Cast<AWarrior>(OtherActor);

				// CalculateDamage() will be executed only in server side
				TargetWarrior->CalculateDamage(
					Hit.BoneName.GetPlainNameString() // Here you have a bone name that weapon hit
					);
			}
		}
	}
}

CalculateDamage() is executed only on the server side and only on the Warrior instance that receives damage. Next server changing replicated property HP and tell all to call proper animation.

Warrior.cpp

void AWarrior::CalculateDamage(
	FString NameHitBone
)
{
	// Some damage calculations
	// UPROPERTY(Replicated) float HP ;
	if(HP <= 0.0f) // player dead
	{
		MulticastDeadAnim();
	}
	else
	{
		MulticastHitAnim();
	}
}

// UFUNCTION(NetMulticast, Reliable)
void AWarrior::MulticastDeadAnim()
{
	// without checking HasAuthority()
	Mesh->PlayAnimation(...);
}

// UFUNCTION(NetMulticast, Reliable)
void AWarrior::MulticastHitAnim()
{
	// without checking HasAuthority()
	Mesh->PlayAnimation(...);
}

As you can see I using only AWarrior and AWeapon.
On the server side, I perform important actions, such as calculating the damage, etc.
Clients only say what they want to do, but the server does it.

I treat the server as some “Game Master”, which controls events, characters, performs calculations.
Clients can do only what the server will allow.

For example:

if each client will calculate the damage on their own, then:

ClientA has exactly the same weapon as ClientB (and stats so, they do the same damage), but ClientA cheats (or badly calculates damage) and deals more damage than it should.
If the server performs damage calculations, ClientA and ClientB will do the same damage even if there is some miscalculation.

==========================================================================================

More information

Read this: [Gameplay framework review][2] (especially the beginning)

PlayerController

The server has all PlayerController, but each client has only his own PlayerController.

234132-playercontorller.png

APawn and APlayerStat

The server and clients have all APawn and APlayerStat.

234133-pawn-and-playerstate.png

I hope it will be useful :D.

Hi there,

First of all, thank you for your thorough and thoughtful response, it’s been a big help. The UObject RPC revelation was a big thing for me, so I’m glad that’s cleared up.

I’ve tweaked my implementation and am seeing some strange behaviour. I felt more comfortable with a design of the server calls and animation logic being on a distinct component rather than the actor themselves, so I have the AnimationRequester, now inheriting from UActorComponent, existing as a member on the PlayerController.

When a player swings, the animation is seen, as before. When the client character hits the server one, I see success, and the reaction animation is played for both the client and the server. However - if the server hits the client, some bizarre functionality appears: the reaction animation is played really slowly, with tens of frames being skipped, and the client itself seems to depossess from the pawn and stop responding, with the camera now following the server pawn. I cannot explain what’s going on!

Here’s the function that’s being called from the multicast, fairly basic

void UCharacterAnimationRequester::MulticastPlayHitReaction_Implementation(ARoguelikeCharacter* instigator, ARoguelikeCharacter* target, const FString& hitSocket, float force, FVector hitLocation, bool knockedBack)
{
	target->GetMesh()->GetAnimInstance()->Montage_Play(Animations->GetAnimation(FullBodyKnockback), 1);
}

The output log is none too revealing. Before the animation finishes in its unusual state, the following is printed:

Any ideas? Unsure on what to try next. I’ve also tried it with 2 clients connected and one server for three characters in total, and the same behaviour happens :frowning:

Alright, small update: moving the object from the PlayerController to the actual Character seems to have fixed it. Utterly stumped. Glad it’s working, but would love an understanding as to why. Any ideas? :stuck_out_tongue:

Hey,

maybe it has something to do with the fact that every Client has only his own PlayerController. However, all Pawn exist on every Client.

Animations are associated with Pawns. So when you run Anim on the server side in PlayerControllerA this is executed on all clients in PlayerControllerA, but ClientB doesn’t have PlayerControllerA he has only his own PlayerControllerB.

Not sure for 100%, these are only my speculations.

btw. I think you can run animation only on the server side.
The server should automatically send this to the clients if in your ACharacter class you use SetReplicates(true) and SetReplicateMovement(true)
So instead NetMultcast try to use Server.

In my project, I use Server RPC to reload a weapon and this works.

void Reload_Pressed()
{
	ServerReloadWeapon();
}

//UFUNCTION(Reliable, Server, WithValidation)
//void ServerReloadWeapon();

void ServerReloadWeapon_Implementation()
{
	RunReloadAnim();
}

If this works, you probably will reduce the flow of data on the network.