1
\$\begingroup\$

Recently I've learned about running my clients and servers on separate threads than main, and I was able to send string data back and forth, open/close sockets, etc.

Now I'm curious: How do I send gameobject data (transform, variables, actions, etc.) between server and client? I understand how you turn data into bytes with GetBytes() and then into strings with GetString(), but how do you deal with any other data types, like Gameobjects, or Transforms?

I have tried changing everything to strings, then bytes, then back to strings, but I honestly don't even know where to start otherwise, as this seems excessive and wrong.

Important information I almost forgot to mention:

  1. I am not using the unity built in networking tools, as this is for my own education.
  2. I'm using a Udp server and client, all coded in C#, currently using MonoBehaviour(which I can remove if necessary)
  3. Being it's all c# Udp, I'm using UdpClient and Sockets.

Thanks in advance for any help, if more information is needed to clarify anything feel free to ask!

\$\endgroup\$
1
  • \$\begingroup\$ What exactly seems "excessive and wrong" about serializing Unity components to bytes or strings? Any network protocol on top of a transport layer is either byte-based or string-based. \$\endgroup\$
    – Philipp
    Commented Jul 25, 2019 at 8:54

1 Answer 1

3
\$\begingroup\$

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:

  1. 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.

  2. 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.

  3. 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]")

\$\endgroup\$
6
  • \$\begingroup\$ I'm having several different problems, however I have one question that potentially can be answered quickly if you don't mind: if you have several clients connected to your server who are all requesting various things, how do you ensure the right clients receive the right things? I.e. How do you choose a specific client if they're all on the same port? \$\endgroup\$
    – schnondle
    Commented Jul 26, 2019 at 7:53
  • \$\begingroup\$ That's a different question, and not one specific to game development, so you can find the topic addressed elsewhere including for the UDP case. \$\endgroup\$
    – DMGregory
    Commented Jul 26, 2019 at 7:58
  • \$\begingroup\$ Let's assume I have 2 players and want to create GameObjects when they join, and move them during game. I understand generally all of it should be done from the server and "broadcast" out, but how do I tell the server when/which/how much to move a gameobject? "Which" I can figure out some id system for, but what type of data should the client be sending for movement if the server is doing the actual moving? \$\endgroup\$
    – schnondle
    Commented Jul 26, 2019 at 19:52
  • \$\begingroup\$ The simplest answer would be a Vector3, though other data types can work too depending on your needs. What did you try so far, and have you encountered any unwanted consequences as a result? \$\endgroup\$
    – DMGregory
    Commented Jul 26, 2019 at 20:58
  • \$\begingroup\$ I've written, rewritten, and deleted about 4 different versions of both the server and client so far. I get about half way through and realize why it won't work each time. The next one I write I'll use to edit the post. \$\endgroup\$
    – schnondle
    Commented Jul 27, 2019 at 3:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .