How to make players glue together when pushing in multiplayer

Godot Version

4.7

Question

I am new to mutiplayer and I try to create a multiplayer game where players are CharacterBody3D nodes with capsule collisions. I want to implement a custom mechanic where players can push each other.

Desired Behavior:

Host & Client: When Player A (host) pushes Player B (client), they should immediately “glue” together and move forward at the exact same speed as Player A.

Instant Stop: The moment Player A stops inputting movement, both players must stop completely and immediately. Player B should not slide or drift away.

Problem

Currently, when I stop pushing, both characters bounce or “push back” a little bit instead of coming to an immediate halt. Additionally, the client (Player B) frequently jitters back and forth while being pushed.

extends CharacterBody3D

@export var speed: float = 6.0
@export var jump_velocity: float = 4.5
@export_range(0.0, 1.0) var push_multiplier: float = 0.8
@onready var _input: PlayerInput = $Input

var _radius: float

func _ready() -> void:
	_radius = ($PlayerCollision.shape as CapsuleShape3D).radius
	$RollbackSynchronizer.process_settings()

func _rollback_tick(delta: float, _tick: int, _is_fresh: bool) -> void:
	if not is_on_floor():
		velocity.y += get_gravity().y * delta
	elif _input.jump:
		velocity.y = jump_velocity

	var move_dir: Vector3 = transform.basis * _input.movement
	move_dir.y = 0.0
	move_dir = move_dir.normalized() if move_dir.length() > 0.001 else Vector3.ZERO
	velocity.x = move_dir.x * speed
	velocity.z = move_dir.z * speed

	velocity *= NetworkTime.physics_factor
	move_and_slide()
	velocity /= NetworkTime.physics_factor

	_resolve_player_push()

func _resolve_player_push() -> void:
	for body in get_tree().get_nodes_in_group("Player"):
		if body == self or not (body is CharacterBody3D):
			continue

		var away: Vector3 = global_position - body.global_position
		away.y = 0.0
		var dist := away.length()
		var min_sep := _radius * 2.0
		if dist >= min_sep or dist < 0.001:
			continue

		var dir := away / dist
		var my_push := _push_intent(self, -dir)
		var their_push := _push_intent(body, dir)
		var total := my_push + their_push
		var ratio := 0.5 if total < 0.001 else their_push / total

		var my_share := lerpf(1.0 - push_multiplier, push_multiplier, ratio)
		global_position += dir * (min_sep - dist) * my_share

func _push_intent(player: Node, dir: Vector3) -> float:
	var node_input: PlayerInput = player.get_node_or_null("Input")
	if node_input == null:
		return 0.0
	var move: Vector3 = player.transform.basis * node_input.movement
	move.y = 0.0
	if move.length() < 0.001:
		return 0.0
	return clampf(move.normalized().dot(dir), 0.0, 1.0)

The jitter is mainly caused by changing global_position manually after move_and_slide(). Both players resolve the overlap separately, while rollback may restore the remote player’s previous position, so the corrections fight each other.

Since you want the players to “glue” together, I would make pushing an explicit state rather than a normal physics response:

  • Player A detects that they are moving into Player B.
  • While pushed, Player B ignores their own horizontal movement and uses Player A’s horizontal velocity.
  • When Player A stops moving, both horizontal velocities are set to zero.
  • Only one player should resolve the interaction.

Because you are using netfox rollback, whenever Player A changes Player B’s state, you also need:

body.forced_velocity = Vector3(velocity.x, 0.0, velocity.z)
NetworkRollback.mutate(body)

forced_velocity should be included in Player B’s RollbackSynchronizer state and applied before B calls move_and_slide().

I would remove the current global_position += ... correction. If both capsules must remain perfectly attached, temporarily disable collision between those two players while the push state is active and keep Player B at a fixed offset from Player A.