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.