Input
PlayerInput Script
extends Node
class_name PlayerInput
# this input node will translate all player input into a bit mask for efficent
# transmission. It will also parse any directional input into appropriate
# vectors. These inputs will be passed to the character.
# bit mask for input
enum {
NONE = 0x00,
MOVING = 0x01,
FIRE = 0x02,
JUMP = 0x04,
}
@export var move_dir := Vector2.ZERO
@export var aim_dir := Vector3.FORWARD
@export var input : int = 0
# external nodes
@export var camera : Camera3D
@export var player_character : PlayerCharacter
signal input_updated(input_bits:int, movement_direction:Vector3, aim_direction:Vector3)
func _ready():
var id : int = get_node(self.player_character).name.to_int()
if id == 0:
print(name, "player_character node's name is not an int!!!!")
else:
set_auth_id(id)
# make sure client can send input
func set_auth_id(id : int):
print(name, ": uid ", multiplayer.get_unique_id(), " setting auth to ", id)
call_deferred("set_multiplayer_authority",id)
func _process(delta):
update_aim_position()
#rpc
if is_multiplayer_authority():
set_player_input.rpc_id(1, input, move_dir, aim_dir)
func update_aim_direction():
self.aim_dir = camera.global_transform.basis.z
func _unhandled_input(event):
if is_multiplayer_authority():
if event.is_action("input_movement_left") \
or event.is_action("input_movement_right") \
or event.is_action("input_movement_forward") \
or event.is_action("input_movement_backward"):
pull_movement_direction_input()
elif not event.is_echo():
check_weapon_input(event)
check_jump_input(event)
func pull_movement_direction_input() -> void:
move_dir = Input.get_vector(
"input_movement_left",
"input_movement_right",
"input_movement_forward",
"input_movement_backward"
)
input = set_bit_mask( input, Vector2.ZERO < self.move_dir, MOVING)
func set_bit_mask(bit_field:int, bit_on:bool, bit:int) -> int :
var output = bit_field | bit if bit_on else bit_field & ~bit
return output
func check_weapon_input(event) -> void:
if event.is_action("input_equipment_fire_weapon"):
# buttons/key strength is always 1.0, test analog strength
if event.get_action_strength("input_equipment_fire_weapon") > 0.2:
input = set_bit_mask( input, true, FIRE)
else:
input = set_bit_mask( input, false, FIRE)
@rpc("authority", "call_local", "unreliable_ordered")
func set_player_input(input_bits:int, movement_direction:Vector2, aim_direction:Vector3):
input_updated.emit(input_bits, move_direction, aim_direction)
Character
PlayerCharacter Script
class_name PlayerCharacter
extends CharaterBody3D
const SCENE_RES : = "res://src/character.tscn"
static func create_scene() -> PlayerCharacter:
return preload(SCENE_RES).instantiate()
var weapon : Node3D = null
var move_direction : Vector2 = Vector2.ZERO
var look_direction : Vector3 = Vector3.ZERO
var state = STATE.OKAY
var input : int = 0
enum STATE { OKAY, DEAD }
func is_moving() -> bool :
return input & MOVING
func _enter_tree():
if is_remote():
remove_puppet_nodes()
func is_remote()->bool:
return not self.name == str(multiplayer.get_unique_id())
func remove_puppet_nodes():
#TODO: if we can spectate we need these nodes below,
# but should hide/switch from other instances of the players.
if $Camera:
$Camera.queue_free()
if $HUD:
$HUD.queue_free()
# signal from PlayerInput node
func _on_player_input_input_updated(input_bits, movement_direction, aim_direction):
set_input(input_bits, movement_direction, aim_direction)
func set_input( input_bit_array : int, player_direction : Vector3, aim_direction : Vector3 ):
# validate incoming input: normalize vectors
if state != STATE.DEAD:
move_direction = player_direction.normalized()
look_direction = aim_direction.normalized()
input = input_bit_array
func _process(_delta):
dispatch_input()
if is_multiplayer_authority():
update_remote_peer.rpc(global_position, velocity, global_rotation, state)
func dispatch_input():
$StrafeSystem.strafe_direction( move_direction )
$CameraSystem.aim_direction( look_direction )
$JumpSystem.user_input( input )
set_fire( input )
func set_fire( user_input : int):
if weapon != null:
weapon.set_fire( user_input )
elif user_input & PlayerInput.FIRE:
print( name, ": no weapon equiped to fire" )
@rpc("authority", "call_remote", "unreliable_ordered") # The server is the authority so this should be trusted on client.
func update_remote_peer(pos:Vector3, vel:Vector3, rot:Vector3, st:int):
global_position = pos
velocity = vel
global_rotation = rot
state = st
details
This code has been heavily redacted and simplifed so there is probably errors. basically the tree looks like this
|-- 12345678 : PlayerCharacter (authed to server)
| \-- PlayerInput (authed to player)
PlayerCharacter sends back server state and PlayerInput sends data to server to act, and acts on it locally.
PlayerInput is connected to PlayerCharacter via a signal, but could be a direct call during the RPC
I dont have any client/server prediction stuff yet, but there is only one rpc per class that can easily be buffered. you can also send the input back with the player character to figure out if an input was lost to the network.
The second benifit to this approach is we are not using “any_peer” and can trust that only the authority is sending the RPC.