Don't try to send GameObjects over the wire. Unity objects like GameObjects, Transforms, and other components are not set up for user serialization. There isn't a way to digest the whole thing into bytes to fit down the pipe then reconstitute it at the other end. Moreover, we wouldn't actually want to. It's both heavier-weight and less effective than what we really want to do, which is:
Send information about GameObjects.
For instance, if you want to tell the person at the other end of the line "GameObject #5 has moved to a new position" then you'd send something that looks like:
Identifier for a "move object" message
(so the receiver knows what kind of conversation we're having and can parse the following fields into your intended meaning).
Since you're making up your own networking code, how you identify each kind of message you can send is up to you — you might use enum values, or JSON objects with a string label "MessageType" etc.
Identifier for the GameObject you want to move
(so the receiver knows how to find the corresponding object in its replica of the game scene)
Again, since this is your own system, you get to decide how you'll identify objects. Maybe you assign each object a unique name string or numeric ID when it's created — just make sure any method you use is consistent over the network, so all participants agree on the names of the objects involved.
The position it's moved to
This is a Vector3
, so you send three floats, encoded as bytes or round-trippable strings depending on your format.
What this might look like in code, assuming a binary message format that you can read like a BinaryReader
, might be...
var messageType = (MessageType)message.ReadByte();
switch (messageType) {
case MessageType.Move:
var id = message.ReadInt32();
var myObject = GetLocalObjectById(id);
Vector3 position;
position.x = message.ReadSingle();
position.y = message.ReadSingle();
position.z = message.ReadSingle();
myObject.transform.position = position;
break;
// ...other message cases
}
Notice that to send, receive, and interpret this message, we only need to use primitive types: strings, floats, integers, or enum values that boil down to integers. At no point did we convert a whole GameObject
to a serialized form and send it over the wire. We just used a primitive message "Move GameObject #5 to these coordinates" to tell us what modification needed to be made to our existing object's transform.
This is true even for spawning new objects. We don't send the new object over the wire, we send the recipe the recipient needs to follow to achieve the same result. A spawn message might include:
An identifier for the "spawn" message
An identifier for the prefab we should clone
This references a table of runtime-spawnable objects that both copies of the game hold. So if I want to spawn a new coin object, I look in my table and find the coin prefab at entry 5, and tell you "spawn a prefab #5" again without sending the entire object's component data, model, textures, animation, etc over the wire.
The unique identifier this instance should have
So in future we can agree on how to refer to this shared object in both our scenes.
The position we should spawn it at
Vector3
: three floats.
The orientation to spawn it with
Quaternion
: four floats.
Any data needed to initialize the object in the same state
eg. If you're using procedural generation to add uniqueness to stuff in your game, you may send along the seed used to generate the object, so the receiver can generate it exactly the same way.
So again the rule is: only send primitive data over the wire. You can format it into your own message types that you can serialize into strings/bytes as you like, but you're only ever sending instructions for what to add to the scene or how to modify stuff in the scene. You're never trying to wrench a fully-fledged Unity object out of all its interconnections in the engine and scene, blend it down to bytes, and somehow re-assemble and transplant it into a foreign body at the other side.
Not only is this a lot less hassle than trying to rebuild/rewire all the object's many interconnections when deserializing, it's also a lot less data to push, which is faster and costs less bandwith.
You're exploiting the fact that you're not communicating with an alien species who doesn't speak your language: you're talking to a copy of your own program, that already knows most of what you know (What does the "coin" prefab look like? What fields does a "transform" component care about?), so all you need to tell it are the new details specific to your local game state in the moment ("I need a [coin] at [this position]" or "Rotate [this] transform to [this orientation]")