Godot Version
Godot 4.4 beta 3
Question
Hi everyone,
I’m working on a client-server game in Godot 4, but I’m using Godot’s low level networking system (TCPServer, PacketPeerStream & StreamPeerTCP) instead. I’m finding that while the client-side (visuals, input) is relatively straightforward, structuring the server-side logic and handling “remote” versions of client-side objects is quite difficult to design.
The setup that I’m designing currently looks something like this (with all the tcp networking taken out first for simplicity):
- Client Scene: This is a standard Godot scene that handles all the visual representation of the game, animations and user input. It’s also the root node for the game
class_name Client extends Node2D
##Handles network communication
var network_layer: NetworkLayer
func _ready() -> void:
# Register cliient-related items
CommonRegister.register_client()
# Create network_layer
network_layer = NetworkLayerIntegrated.new()
network_layer.data_received.connect(_on_data_received)
add_child(network_layer)
# Connect the integrated network layer to the local server
if network_layer is NetworkLayerIntegrated:
# Create and add a server instance directly to the client scene
var server := Server.new()
add_child(server)
# Attach the server to the network layer to communicate to
@warning_ignore("unsafe_method_access")
network_layer.attach_local_server(server)
# Connect to server via network_layer
network_layer.connect_to_server({"username":"ExampleName"})
func _on_data_received(packet_name: String, packet_data: Dictionary) -> void:
pass
- NetworkLayer (Abstract): A base class for network communication.
class_name NetworkLayer extends Node
##Base network layer class defining common interface
var port := 3115
var address := "127.0.0.1"
##Receive packet data FROM server
@warning_ignore("unused_signal")
signal data_received(packet_name: String, packet_data: Dictionary)
##Unique Player ID generated by server
var my_player_id: String
##Connect to server
func connect_to_server(_user_data: Dictionary) -> void:
pass
##Send packet data TO server
func send_data(_packet_name: String, _packet_data: Dictionary) -> void:
pass
- Integrated NetworkLayer: Manages client-server communication if client and server are running locally
class_name NetworkLayerIntegrated extends NetworkLayer
##Integrated network implementation for direct local server communication
var _server: Server
func connect_to_server(user_data: Dictionary) -> void:
var username: String = user_data["username"]
my_player_id = generate_player_id(username)
_server.connection_request(my_player_id, user_data)
func send_data(packet_name: String, packet_data: Dictionary) -> void:
_server._on_data_received(my_player_id, packet_name, packet_data)
func generate_player_id(username: String) -> String:
return username.sha256_text()
func attach_local_server(server_: Server) -> void:
_server = server_
_server.send_data_to_player.connect(_on_server_data_send)
func _on_server_data_send(player_id: String, packet_name: String, packet_data: Dictionary) -> void:
if player_id == my_player_id:
data_received.emit(packet_name, packet_data)
else:
# Handle network communication here like `put_var(var)`
pass
- Connection NetworkLayer: Manages Client to Server communication over a network. Mostly empty since I’ve stripped out most of the network communication for the sake of simplicity.
class_name NetworkLayerConnection extends NetworkLayer
##Network Layer for actual network communication to a server
##Connect to server
func connect_to_server(user_data: Dictionary) -> void:
# Handle network communication here like `put_var(var)`
pass
##Send packet data TO server
func send_data(packet_name: String, packet_data: Dictionary) -> void:
# Handle network communication here
pass
- Server Instance: A separate object that runs the server logic.
class_name Server extends Node
signal send_data_to_player(player_id: String, packet_name: String, packet_data: Dictionary)
var world_manager: WorldManager
var global_player_list: Dictionary[String, PlayerData] = {}
func _ready() -> void:
# Register server-related items
CommonRegister.register_server()
func connection_request(player_id: String, _request_data: Dictionary) -> void:
if player_id in global_player_list:
printerr("Duplicate ids connection request")
return
global_player_list[player_id] = PlayerData.new(player_id)
player_join_world(player_id, "test_map")
func get_player(player_id: String) -> PlayerData:
return global_player_list.get(player_id, null)
func send_data(player_id: String, packet_name: String, packet_data: Dictionary) -> void:
send_data_to_player.emit(player_id, packet_name, packet_data)
func _on_data_received(sender_id: String, packet_name: String, packet_data: Dictionary) -> void:
pass
The problem I’m running into is that the server needs to maintain its own authoritative representation of the game world. I need to create server-side equivalents (“remote” versions) of the client-side objects (players, enemies, etc.) to track their state and enforce game rules.
I’ve been thinking about different approaches, but I’m not sure what the best practice is for structuring these “remote” objects.
Here are my questions/concerns:
- How should I structure the server-side “remote” versions of my entities? Should I be duplicating the entire scene graph on the server, or is there a more efficient way to represent the game state?
- What’s the best way to keep the client and server synchronized? Should I send full state updates, delta updates, or a combination of both?
- How do I avoid tight coupling between the client-side visuals and the server-side logic? I want to keep the client as a “dumb” renderer that simply displays the state it receives from the server.
If you have any code examples to help me or to visualize your understanding of the problem I would greatly appreciate it
Here’s some approaches I’ve found out so far:
- Separate “Game Entity” Class: Create a base class (or script) that defines the common properties and behaviors of all my game entities (players, enemies, items, etc.). This class would not include visual or rendering code.
- Client-Side: Visual Representation: A Godot
Node
that handles the visual representation of the entity. It receives data from the network and updates its appearance accordingly. - Server-Side: Logic and State: A script that inherits from the “Game Entity” class and handles the game logic and state of the entity on the server. It receives input from clients, updates its state, and sends updates to clients.
I would really appreciate any guidance, suggestions, or examples you can provide. I understand that using low-level networking without godot’s RPC is more complex, but I want to gain a deeper understanding of the underlying network architecture.
Thanks in advance!