RPC Client to Server, call function on unowned Actor not working

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.

Hello, RPC functions should only work on actor classes, maybe also on actor components not sure right now. Is your AnimationHelper also derived from the UObject class, I wouldn’t know how that can actually work in networking. So simple solution just move your code over from the helper classes to the character and player controller class. If you want helper classes to outsource some functionality I would advice you to use components instead of uobjects anyways because that’s how the framework is meant to be used, allows you to customize the components in blueprints for example. Usually a good approach in terms of networking is to give the clients as little control as possible and minimize network traffic, if you have client side hit detection run a server RPC on the character that the player hit something, the server then applies damage and calls a multicast RPC to let all clients know they need to apply damage locally as well to trigger the recoil animation. Also your RPC’s are a little bit too universal in my opinion, I would rather have RPC’s like ServerSwingSord and MulticastSwingSord, that way it is easier to avoid malitious data sent from clients and you reduce the data amount for every RPC quite a lot.

I had your same problem not long ago, and I recently saw a “trick”.

Basically, as you said, you can’t do RPCs on actors that are not owned by a “network enabled” entity. So the idea is to just set the owner, do the RPC and then set the owner back to what it was.

For example, if there is an actor placed on the level (which thus has no owner), you could do something like this:



void AMyActor::BeginPlay()
{
     Super::BeginPlay();

     if(GetNetMode() == ENetMode::ENM_Client)
     {
            SetOwner(UGameplayStatistics()->GetPlayerPawn(GetWorld(), 0);

            ServerDoRPC();

            SetOwner(nullptr);
     }
}

bool AMyActor::ServerDoRPC_Validate()
{
     return true;
} 

void AMyActor::ServerDoRPC_Implementation()
{
     // Something
​​​​​​​} 


I honestly thought this couldn’t work, since I assumed that it should be the server to set ownership of objects, but it actually works…

@Beriol Interesting idea, I would have been very surprised if that actually worked so I tested it and fortunatelly it doesn’t, because that would have been a very exploitable bug :wink: I tested RPC’s on UObject derived objects with a network owned actor as the outer as well, doesn’t work either. What does work is RPC’s called on actor components.

Well, it DOES work. I’m currently using it, and I’ve seen it being used in a VR plugin which is very used (this one: https://forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin)

Are you sure that it works for remote clients, and not the listen server? Are you sure the VR plugin doesn’t set the owner on the server? I tried with this setup:

Result: Running with one client on a dedicated server in PIE and pressing space bar repeatedly, nothing happens.
Doing the same thing as it is supposed to work, i.e. setting the owner on the server and having it replicate to the client:

Result: Same client setup as before, pressing spacebar results in a screen message “Server: Hello” as expected.

Hey guys, thanks for your responses. The UObject RPC revelation was a big news, I had no idea it performed like that.

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:


[2018.03.18-20.43.45:145] 78]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.043889, ClientTrackPosition: 0.000000, DeltaTrackPosition: 0.043889. TimeStamp: 3.379651
[2018.03.18-20.43.45:232] 81]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.180708, ClientTrackPosition: 0.136296, DeltaTrackPosition: 0.044412. TimeStamp: 3.470864
[2018.03.18-20.43.45:298] 83]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.273266, ClientTrackPosition: 0.222346, DeltaTrackPosition: 0.050920. TimeStamp: 3.532569
[2018.03.18-20.43.45:326] 84]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.321367, ClientTrackPosition: 0.273266, DeltaTrackPosition: 0.048101. TimeStamp: 3.564636
[2018.03.18-20.43.45:354] 85]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.362251, ClientTrackPosition: 0.321367, DeltaTrackPosition: 0.040884. TimeStamp: 3.591892
[2018.03.18-20.43.45:382] 86]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.405036, ClientTrackPosition: 0.362251, DeltaTrackPosition: 0.042785. TimeStamp: 3.620416
[2018.03.18-20.43.45:411] 87]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.448626, ClientTrackPosition: 0.405036, DeltaTrackPosition: 0.043590. TimeStamp: 3.649476
[2018.03.18-20.43.45:439] 88]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.491702, ClientTrackPosition: 0.448626, DeltaTrackPosition: 0.043076. TimeStamp: 3.678193
[2018.03.18-20.43.46:024][108]LogRootMotion:Warning:  Server disagrees with Client's track position!! ServerTrackPosition: 0.285017, ClientTrackPosition: 0.245157, DeltaTrackPosition: 0.039860. TimeStamp: 4.264612
[2018.03.18-20.43.46:324][118]LogNet: Received invalid swap message with child index -1

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:

Wondering if anyone has had similar strange events.

Is it at all a result of having the actor component on the PlayerController? Seems unlikely, but I’ll take any explanation for this bizarre stuff!

[FONT=“Helvetica Neue”]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:

Ok so what I think is happening is that you had your helper component on the player controller which only exists on the server and the owning client. So multicast events will effectively only run on the owning client as well, so when the server swings only he will receive the multicast event. However, montages played on characters are replicated and synced automatically, that’s why it was still playing on the other client but weirdly. They are synced mostly because montages can have root motion which influences the movement, since that’s probably not the case for you I would recommend to step away from montages and use PlaySlotAnimationAsDynamicMontage instead of Montage_Play. As to why your client was unpossessed that seems very odd, maybe your forgot to remove some code from testing where you changed the owner of the character to run the RPC?