Godot Version
Godot Engine v4.4.stable.arch_linux
Question
Hey all!
I’ve been looking around for an easy way to transfer object data via RPC calls, but I haven’t found a solution that feels completely satisfactory. I’m hoping someone here can suggest a better method than what I have. I came up with a hacky solution last night. It is functional, but I’m not in love with it.
Right now, my loose requirements are:
- Does not require defining each property of an object as parameters for the function.
- Communicates (as clearly as possible) what data is expected as input.
- Does not introduce RCE when deserializing messages.
- Validates values are of the expected type (i.e. cannot provide a
callable
when it should be anint
).
My Existing Solution
How it works
The process is:
- Sender serializes a message to a dictionary via the serialize method →
- Sender sends message via RPC to receivers →
- Recipient create a new object (of the expected message) →
- Recipient uses each key/value from the message to assign properties to their appropriate values via the deserialize method. →
- Recipient performs some action based on data from the message.
Messages
Messages (data class) are created and implement const PROPERTIES: Array[String]
defining which properties to serialize. As an example, here is my CastResquest
class.
class_name CastRequest
# This approach seems reasonable for a few reasons:
# 1. How to serialize classes only needs to be defined once and is easy to see.
# 2. It ensures only the expected properties are set when deserializing.
# 3. As long as the property types are statically set,
# we get validation when deserializing.
const PROPERTIES: Array[String] = [
"skill",
"caster",
"target",
]
var skill: String
var caster: String
var target: String
func _init(skill_file: String = '', caster_node: String = '', target_node: String = ''):
self.skill = skill_file
self.caster = caster_node
self.target = target_node
Serializing Messages
Message objects are created, serialized, and then sent via RPC. Here a player creates a CastRequest
, serializes it, and tells everyone to add it to their cast queue.
func cast_skill() -> void:
var request := CastRequest.new(skill.file, caster.name, caster.target.name)
GameManager.queue_cast.rpc(Network.serialize(request))
RPC Calls (Deserialization)
Messages are then received, deserialized to the expected message type, and some action is performed by the recipient. This function ingests a CastRequest
(shown on the first line of the function) and pushes it to the queue.
@rpc("any_peer", "call_local", "reliable")
func queue_cast(message: Dictionary) -> void:
var request = Network.deserialize(CastRequest.new(), message)
# TODO: !!! Check if sender has authority over this caster !!!
Logger.info('Queued cast.', {'skill': request.skill,'target':request.target,'caster':request.caster,'sender':multiplayer.get_remote_sender_id()})
cast_queue.push_front(request)
Note: I have a custom logging solution to help identify which peer the output came from. This is the
Logger
class. These are just fancy print statements. I should look at the built in Log class. I had this lying around though, so was faster to use this in
Serialization Method
This method takes an object and uses the PROPERTIES
Array[String]
property to identify which properties should be serialized. Finally, it returns a Dictionary
of property names mapped to their values.
# Used by the sender before transferring via RPC.
static func serialize(obj: Object) -> Dictionary:
var dict = {}
if not obj:
Logger.error("Tried to serialize a null Object! An empty Dictionary will be returned.")
return dict
var properties: Array[String] = obj.get("PROPERTIES")
if not properties:
Logger.error("Tried to serialize an Object without a PROPERTIES property! An empty Dictionary will be returned.")
return dict
for property in properties:
var value = obj.get(property)
if not value:
Logger.error("Did not find property when serializing Object! Please ensure the values in your property array are up to date.", {"property":property,"all properties":properties})
continue
if value is Array or value is Dictionary:
dict[property] = value.duplicate()
else:
dict[property] = value
return dict
Deserialization Method
Opposite to serialize, deserialze takes in a message template as obj
and the received message. It then tries to map each expected property from that message type to the value defined in the message, and returns the modified Object
.
# Used by the receiver when processing the RPC call.
static func deserialize(obj: Object, data: Dictionary) -> Object:
var dict = {}
if not obj:
return null
var properties: Array[String] = obj.get("PROPERTIES")
if not properties:
Logger.error("Did not find PROPERTIES on object when deserializing! returning null.")
return null
for property in properties:
var value = data.get(property)
if not value:
Logger.error("Property not found on object when peforming deserialization! Please ensure the values in your property array are up to date.", {"missing property":property, "all properties":str(properties)})
continue
obj.set(property, value)
return obj