Network Code
Basic Server setup
Since we want Client-Server and we want to send a PoolByteArray, I'm assuming we are making an authoritative server with Godot (not other technology). In particular we will be using the MultiplayerAPI.
We can start with an object of the class NetworkedMultiplayerEnet. And we will pick a port to listen to (Ideally you would make it configurable, but here it is hard coded):
SERVER Server.gd:
extends Node
var network := NetworkedMultiplayerENet.new()
var port:int = 1024
func _ready():
# start server
var error: = network.create_server(port)
if error != OK:
print ("Error Creating Server: ", error)
return
get_tree().multiplayer.set_network_peer(network)
Basic Client setup
The client code is very similar. Except we need to provide the address and port of the server (here I have hard coded "127.0.0.1").
CLIENT server.gd:
extends Node
var network := NetworkedMultiplayerENet.new()
var server_port:int = 1024
var server_address:String = "127.0.0.1"
func _ready():
# start server
var error: = network.create_client(server_address, server_port)
if error != OK:
print ("Error Connecting to the Server: ", error)
return
get_tree().multiplayer.set_network_peer(network)
Client-Server connection
We can connect the "network_peer_connected" and "network_peer_disconnected" signals of the MultiplayerAPI:
CLIENT and SERVER Server.gd:
get_tree().multiplayer.connect("network_peer_connected", self, "_on_peer_connected")
get_tree().multiplayer.connect("network_peer_disconnected", self, "_on_peer_disconnected")
We can find similar signals on the SceneTree and on NetworkedMultiplayerEnet. They all work. I'm using the MultiplayerAPI just to be consistent.
Regardless of which version of these signals we connect, the handlers will look and behave the same:
func _on_peer_connected(player_id:int) -> void:
print("Player Connected with id: ", player_id)
func _on_peer_disconnected(player_id:int) -> void:
print("Player Disconnected with id: ", player_id)
On the client you should get a peer connection with the player_id of 1. That is always the server. On the server you will get a peer connected signal with a large player_id number which identifies the client. If there are multiple clients, they get peer connection signals for each other.
Similarly you will get the peer disconnected signal when a client goes offline. However, the clients will not get a peer disconnected signal when the server goes offline. Instead you get a "server_disconnected" signal. There is also a "connected_to_server" signal available on the MultiplayerAPI (and on the SceneTree).
Sending a PoolByteArray across the network
On the server, let us send our PoolByteArray. For example on a successful connection:
func _on_peer_connected(player_id:int) -> void:
print("Player Connected with id: ", player_id)
get_tree().multiplayer.send_bytes(PoolByteArray([125, 30, 50]), player_id)
You would, of course, keep track of the ids of the connected players, and send the PoolByteArray to the appropiate client at the appropriate time. For explanations purposes I'm doing it like this.
Receiving a PoolByteArray across the network
On the client, we will connect the "network_peer_packet" signal:
get_tree().multiplayer.connect("network_peer_packet", self, "_on_network_packet")
And we get the PoolByteArray as parameter on the handler:
func _on_network_packet(player_id:int, packet:PoolByteArray) -> void:
print("Packet from ", player_id)
Read and Write file as PoolByteArray
Read PoolByteArray
We will use an object of the File class, and the get_buffer function.
func read_file_to_pool_byte_array(path:String) -> PoolByteArray:
var file = File.new()
var error = file.open("user://path/to/file", File.READ)
if error == OK:
var result:PoolByteArray = file.get_buffer(file.get_len())
file.close()
return result
else:
print("Error opening the file.")
Write PoolByteArray
func write_file_from_pool_byte_array(path:String, data:PoolByteArray) -> void:
var file = File.new()
var error = file.open("user://path/to/file", File.WRITE)
if error == OK:
file.store_buffer(data)
file.close()
else:
print("Error opening the file.")
Resource serialization
As you would know, you can load a resource with load, preload, ResourceLoader.load or ResourceLoader.load_interactive (see Background loading).
The counterpart is ResourceSaver. You can save a resource to a file with ResourceSaver.save.
FBX?
No.
The features necessary to import FBX can only be instantiated by the editor. It is not available on game builds. Ok, at least it is not available by default, but enabling it requires a custom build of Godot. Unless you want to build Godot from source to enable them… That is a "No".
Bringing it all together
Import your FBX on a Godot project. It gets imported as a scene. Open it and save it as a resource format. That is the file you will send, not the FBX.
On the server:
- Read the resource file to a
PoolByteArray.
- Send the
PoolByteArray to the client.
On the client:
- Receive the
PoolByteArray from the server.
- Save the
PoolByteArray to a file.
- Load the file with
ResourceLoader.load or ResourceLoader.load_interactive, so you get a PackedScene
Then you can instance the PackedScene by calling instance on it when necessary.
Since you will be storing the resource file on the client. You can get more involved in the solution and have the client request the resource from the server only when it does not have it.
You might even have the server tell the client a hash of the file... Then, for each file, the client can check if it has it, and if the hash matches (see HashingContext). If it does not have it, or the hash does not match, then the client can request the file from the server.
You might do this on demand… Or have the server send a manifest with all the files and their hashes, and do it up front, with a progress bar.
You could also consider having a binary diff solution, but that is beyond the scope of this post.