Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Network] execute remote players' actions on client with no authority #44

Open
Zotrun opened this issue May 4, 2023 · 4 comments
Open

Comments

@Zotrun
Copy link

Zotrun commented May 4, 2023

Hello,

Say you have an action registered:

network.register_action("attack", false)

in the physics_process() of the player script, the code input = network.get_input(_uid) is only executed on the local machine and on the server:

network.gd:

func get_input(player_id: int) -> InputData:
	var is_authority: bool = has_authority()

	if (!is_id_local(player_id) && !is_authority):
		return null
	
	var pnode: NetPlayerNode = player_data.get_pnode(player_id)
	var retval: InputData = pnode.get_input(_update_control.sig) if pnode else null
	
	return retval

meaning a remote player scene will not be able to check and execute the input.is_pressed("attack") to play, for example, an attack animation.

For now, the attack animation is played if the player is local and on the machine that has authority, but a client that doesn't have authority will not be able to execute actions of other players.

How would you go about synchronizing the actions of all players on all machines?
Same question goes for an NPC entity.

Would the solution be to put new booleans in the snapshot script extending SnapEntityBase, and set them as action = true when receiving a new action from the snapshot ?

Thanks for the addons and for your help.

@Kehom
Copy link
Owner

Kehom commented May 5, 2023

Using booleans in the snapshot could be a solution. However I'm not entirely sure about how resilient it would be when packet loss occur.

Since you are dealing with animations (and here I assume you are using the AnimationPlayer Node), perhaps a better approach would be to add which animation is being played within the snapshot. Now in here there are a few options:

  • In the entity snapshot data, use the animation name. Because this is a String it might not be the most efficient in terms of bandwidth usage. However it scales very well if you are constantly adding new animations.
  • Use an animation code/ID. This becomes a simple integer and can even save a bunch of bandwidth if setting the value to be of a shorter type than 32 bits.

Then, to increase the resilience an additional float can be used to indicate the playback position.

The AnimationPlayer node offers current_animation_position property that can be used on the server to build the authority state. On clients you can use AnimationPlayer.seek(position_in_seconds, true) to apply the incoming correct position while also updating the animation on screen.

And to check if the correct animation is being played you can use AnimationPlayer.current_animation property and compare it with the value received from the server. Just remember that if you used animation ID as replication data (to save bandwidth), you have to "convert it back into animation name".

@Zotrun
Copy link
Author

Zotrun commented May 5, 2023

Thanks for your reply!

I see how sending the current animation data in the snapshot could help in the case of simple scenes:
-an entity having multiple animations and nothing else.

Though it would be quite cumbersome with a more complex scene:
-an entity having animations and composed of an other entity, such as a weapon that can be changed during gameplay, having animations as well.

In this case, the entity attacking would play an attack animation and the weapon equipped would also play an attack animation. Meaning the weapon state/animation would need to be synchronized via snapshot as well.

Ideally, the weapon would be synchronized only when the entity/player changes it, letting the other peers know about the current weapon.
After that, any attack action input.is_pressed("attack") would be executed locally for each peer with the currently selected weapon, with disabled hitboxes for everyone except the server.

Also, another problem occurs when you want to add an attack combo, executing the attack id = 2 after the attack id = 1 if the action key is pressed again in a short period of time. I struggle to see how this kind of information could be passed to the other peers.


Here is the solution I'm using, to help anybody who may need this in the future:

Your answer pointed me in the right direction, but instead of sending the animation name and the animation time, I'm sending the "action state" of the entity: such as no_action (int = 10), attacking (int = 20), blocking (int = 30), etc. as well as an action_id, incrementing every time a new action is to be sent. I'm using a base 10 in my example, so that I can divide by 10 to get the action to execute and use the last digit (with a modulo) to "pass" an argument: for example the attack_id for a combo (20, 21, 22...)

The client keeps in memory the last_action_id executed, as to not execute a second time the same action (since the snapshot are sent 60 times/sec, there are a lot of duplicate action/action_id).

The physics_process() in the entity:

func _physics_process(delta: float):
  if (_correction_data.corrected):
    _correction_data.corrected = false
    action = _correction_data.action
    action_id = _correction_data.action_id
    ...

    if last_action_id < action_id:
      match int(_correction_data.action / 10):
        1:
          #no action, reset the state of the entity
        2:
        #you may want to reset the player state here as well, in case of high latency,
        #you probably want to cancel any active action and execute the new one
          weapon_node.attack(action % 10)
        3:
          init_block()
      
      last_action_id = action_id
    ...

When the local player/server entities do an action, in this case an attack, update the variables sent in the snapshot (action, action_id):

func init_attack(attack_id : int = -1):
  action = 20
  action += current_attack_id
  action_id += 1

  ...
  #If attack_id == -1, the init_attack() is called by the local machine, so the combo should be managed locally
  #else execute the specified attack, forced by the snapshot
  weapon_node.attack(attack_id)

@Kehom
Copy link
Owner

Kehom commented May 6, 2023

That is a very interesting solution! It will work very well as long as you don't need to have 10+ attack IDs.
Based on your idea, you could use a single integer but divide it into two parts. The high 16 bits could be the attack ID while the low 16 bits could be the action id. Or, because 16 bits might be too much for each ID, you could use 8 bits for attack ID and 8 bits for action ID. You still get plenty available IDs to be used.

Now bear in mind: I know it's a bit clunky to replicate game state rather than the inputs so the clients could calculate the result. When I designed the Addon I was too focused on the idea server does everything while clients "only" replicate the state of the game. I might review this design decision in the future.

@Zotrun
Copy link
Author

Zotrun commented May 6, 2023

I will indeed use the integer divided into two parts as it's definitely cleaner, thanks for the suggestion!

I definitely see where you are coming from by replicating the game state and not the inputs, either way, compromises have to be made and your addon already helps me a lot, so again, thanks for that!

Using the method I described above, the action synchronization is working quite well, even though I still didn't really test on bad network conditions. But even on high latency, since the simulation/animations are running locally, only the starting point of these actions matters: when the client receives a snapshot containing a more recent action.
In the case of latency fluctuating a lot, not allowing the current action to be finished before receiving a new one, it would be quite easy to cancel it and start the more recent action. It would create a visual artifact, but that is to be expected on bad network conditions.
Another solution, to prevent cutting the current action/animation, would be to change the starting point of the new action based on how much time there is left on the currently playing animation; or to simply change the animation speed to catch up with the latency.
Client side, animations should only be "eye-candy" anyway, cutting them or speeding them up shouldn't affect gameplay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants