Godot Version
4.3
Background
Ok, these aren’t fully objective questions, or even fully questions at all.
However, I’m having some anxiety about whether or not my netcode truly works how I want it to. Do I REALLY know what’s going on under the hood?
For reference, I’m developing a prototype/MVP of a multiplayer PvP, third-person shooter that uses a dedicated server-to-client model. Where the client only sends input, while the server processes the true game state and sends that to the clients.
I’ve been spending the past 2-3 months learning Godot multiplayer architecture + the engine itself in order to create it. However, I’m having a problem with the way Godot abstracts multiplayer functionality.
I have a good amount of experience with other game engines, so I have no issue with the way Godot abstracts things like code (GDscript is amazing), nodes, rendering, lighting, etc. This is NOT the case for netcode.
Before I ask my questions I need to clarify something important:
I’m ready for my code to be imperfect, buggy, and flawed, because a broken game that gets fixed/improved is better than a perfect game that doesn’t exist.
With that being said, here’s my specific issue:
“Question”
The way I currently sync player actions is via RPCs. NOT the multiplayer synchronizer node. I explain why with one of my previous topics:
Basically, I use direct RPCs in order to directly control what gets synchronized when and by who.
Let’s look at my working multiplayer movement code for an example. The server is the authority of the player script, while the client has a separate script to control their input authority.
@rpc("any_peer","call_local","unreliable",0)
func client_move() -> void:
@warning_ignore("unused_variable")
var Server_Authorized = false
if Player_ID == str(Client_Input.get_multiplayer_authority()):
server_move.rpc(Input.get_vector("move_right", "move_left", "move_back", "move_forward"), true)
@rpc("any_peer","call_local","unreliable",0)
func server_move(input : Vector2, Server_Authorized : bool) -> void:
if Server_Authorized == false: return
if Client_Input.is_multiplayer_authority() == false: return
var Movement_Direction = Camera_Rotation.basis * Vector3(input.x, 0, input.y).normalized()
velocity.x = Movement_Direction.x * Movement_Speed
velocity.z = Movement_Direction.z * Movement_Speed
sync_player_position.rpc(self.global_position)
The client sends their movement input to a server authorized RPC, then sends that info to the player script’s _physics_process
function. Which triggers a separate RPC that sends the movement to all other clients:
func _physics_process(delta) -> void:
client_move.rpc()
if Input.is_action_just_pressed("jump") \
and Client_Input.is_multiplayer_authority():
client_jump()
apply_gravity(delta)
move_and_slide()
@rpc("any_peer","call_remote","unreliable_ordered",0)
func sync_player_position(pos : Vector3) -> void:
self.global_position = pos
THIS. CODE. WORKS. However…
…When I was trying to implement interpolation, I noticed I truly have no idea how many packets my RPCs are sending, when they were being sent, if they were successfully sent, if they’re synchronized with every other client, if my server authority was even working, and even more critical info.
I mean, I “understand” what’s happening. It still works. But I feel I need even more precision in order to make the most out of Godot’s network systems and my game’s networking requirements. Right now I feel I understand the abstraction more than the true networking systems at play.
These are rhetorical questions:
- How will I implement server-side validation for networked hitscan/projectile/melee hit detection when I can’t create a reliable timestamp for packets and world state?
- How will I implement interpolation if there’s no saved worldstates to interpolate from?
- How do I know that server is actually the one in calling the shots? (Because if it was the movement of my client would be delayed by their latency, which it isn’t) (Since it isn’t, there’s no client prediction or anti-cheat measures either)
Luckily, there might be another way.
I was combing through Godot’s well written and maintained documentation, when I stumbled upon the PacketPeer
, PacketPeerUDP
, UDPSserver
, and more. Which allow you to directly send packets and manager servers without RPCs or the multiplayer nodes.
It’s still technically an abstraction, and you lose the ability to conveniently use RPC arguments and the like. But this might be just what I need in order to have full control over network synchronization and build/maintain the necessary features for my game.
So after all this, here’s the REAL question(s):
- Should I take the leap and learn these raw multiplayer solutions? (I’m ready to get dirty, my codebase is tiny, and I got git so my code won’t get gone)
- If I do go with these raw solutions:
- How can I replicate RPC arguments likemode
,sync
,transfer_mode
, andtransfer_channel
?
- How can I serialize objects/callables over the network to rid myself of the multiplayer spawner node? Do I pass a reference to a game file? - Do you have any other misc networking advice?
Thank you in advance and I apologize if this is a loaded question.