Need some advices for the network management of a .io game

Hello,
I’m trying to do a multiplayer game online using Mirror.
I have a server which send around 24 messages / second (the message contains the state of the map and the other players)
But if the client has a bad connection, it will take time to download a message so it will receive less message / second and the game will be completly unplayable… But I’m sure there is a way to optimise this because games like brawl stars or fortnite run correctly with this bad connection.

For info, I use Mirror version 2022.9.15 and I use a SimpleWebTransport as transport for the network.
The game should work on Mobile and WebGL

So do you have some advice for me?
Thanks in advance

What does that mean? What kind of data do we talk about and how much is it?

Designing a network protocol means you have to minimize the amount of data you have to send. Are you sure all the data you send is necessary 20 times per second? You usually want to apply “delta compression” by only sending information about things that have actually changed compared to the last state. Of course initially you would send the whole state and maybe every x seconds / frames as well to avoid issues.

Without the information what you actually send we can’t really help you.

The map is a int[37,37] each element of the map is an int between 0-7 . to send it I convert it in a string where each char is an element of the map.

se here is the structure of my message:

        public struct GameData
        {
            public string Map;
            public string State;
            public string Chrono;
            public int Warning;
            public GlobalVars.PlayerInfo[] Players;
            public Bomb.BombInfo[] Bombs;
            public Flame.FlameInfo[] Flames;
        }

where PlayerInfo is (max 24 players)

    public struct PlayerInfo
    {
        public Guid PlayerId;
        public string PlayerName;
        public int HeadId;
        public int ChestId;
        public int LegsId;
        public int BombId;
        public string State;
        public Vector3 Position;
        public float Speed;
        public int BombMax;
        public int Power;
        public int Angel;
        public int Pills;
        public bool Push;
        public bool Multi;
        public bool Kick;
        public int Vehicle;
        public int Disease;
        public int Killed;
        public bool Dead;
        public bool Invincible;
        public bool Immune;
        public string KilledBy;
    }

and

        public struct BombInfo
        {
            public Guid ID;
            public Vector3 Position;
            public Vector3 Scale;
            public int BombId;
            public float Speed;
        }

        public struct FlameInfo
        {
            public Guid ID;
            public string Type;
            public int PosI;
            public int PosJ;
            public int ScaleX;
            public int Rotation;
        }

I don’t know how to simplify it…

Omg :slight_smile: You send that information 20 times per sec? Well of course that doesn’t work. All that state that a player has doesn’t need to be send every frame. You should have dedicated messages when some of that state changes. Does the player name, headid, legid, etc change every frame? Most of that does probably never change at all. So there’s no point of sending it over 20 times a sec. You usually have things like the position that would probably change the most and some other things. However those should be communicated seperately when its necessary. The same is with your map information. A map with 1369 cells, even when send as 1 byte per cell, requires 1.4kB. Again, you usually should send cell information when there’s actually a change. So you would have a dedicated message that indicates the row and column as well as the new value. This is essentially a 3 byte payload message to transmit a single cell change. Of course messages need to have some sort of message type. So a 4 byte message would be enough. Though since the server could accumulate several changes between two network frames, it may make sense to actually combine several cell changes into a single packet.

So a packet may contain a byte for the packet type, a byte for the number of cell information in the packet followed by the row, column and value of the cell. Depending on the type of game, if map changes can involve cell changes over an area, this may have a seperate message to just encode the start and end cell and then fill those cells with a given value. That way you have small individual packets.

While it’s a good idea to use GUIDs to identify players in general, it’s not a good idea to use it in network packets. You usually have a list of connected clients and the server can hand out short single digit IDs Of course those IDs should not change during a sesstion. So you would usually manage them in a dictionary. So the first playet may get id 1, the second the id 2 and so on. In most cases you don’t have to worry about reusing ids if a player left and another joins, though it’s of course possible.

Of course you still need “keyframe” packets which you send initially to sync the whole state of a player and the whole state of the grid. This may be repeated every now and then in case something went wrong. Though websockets run over TCP. So you can’t really miss or drop any packets as they are automatically re-transmitted.

As I said, the exact message types you want to use, what data they should contain completely depends on your type of game, which data changes how often an in which cases. On top of that certain data, if there’s a lot, can be compressed. For example you said the values are in the range 0 - 7. If you think about possible additions if the value can stay below 16 (0-15) you can actually store two cells in a single byte / char since a value up to 15 can be stored in just 4 bits. Though if there’s a lot repetition one can use simply run-length encoding to drastically reduce the transmitted data.

Those are the most fundamental basics about network communication. As I said, it depends on your game mechanics what the best approach is. Though it’s certainly not necessary to send all this information 20 times a second. Things like the player name is probably send once at the start and never again (unless you have the option to change the name un the fly in which case you can send a single “player name changed message” when it actually got changed).

1 Like

A lot things are not clear about your playerinfo struct and if there’s an actual limit for the string field or how they are actually send over. You use a Vector3 for the position, however you map is a grid that is 37x37. So how does the position actually relate to that? A Vector3 uses 3 floats and would require 12 bytes if send as raw binary data. If the player position is always at a grid position you could use a single byte for the rwo and one for the column. As I said we just don’t have enough information to make any meaningful concrete suggestions. This is up to you. You know your game, what the limits and range of certain values are and what may be the best / simplest representation of that. For example I would never use a string to represent the state of a player. You usually have well defined states in your game. A single number is usually enough. Same for the “killedby” which could be just a player id. Though you usually only need that when the player actually get killed, so it doesn’t really matter if it’s a string or not since such a packet would only be send when the player actually get killed / died. Though the player id has the advantage that each client can easily lookup other things related to the killer since you have the ID at hand.

You should distinct between variable sync data and reliable RPC data. Things that arent relevant if messages are missed should be variable sync typical player position etc. That data can be send unreliable. These are sent with a tick rate, in your case 20hz.

Then you have reliable RPC, this is state changes that are relevant and cant be skipped on clients. THese are sent when ever needed. For example when a player shoots.

There is no unreliable when using WebSockets :). WebSockets is a normal HTTP connection that runs over a TCP connection. However instead of having the normal HTTP request / response overhead, once the connection is made and the “upgrade” happened, you can send “packets” of arbitrary data in a stream. So each packet has a very small header which basically consists of an opcode byte, indicating the type of frame as well as a “variable length encoded” length of the packet. That’s it. Both sides can send such frames independent from each other in both directions and those frames always arrive in order as they are transferred over TCP which is a streaming protocol. It’s an endless stream of data that is always in order. You can not do UDP in a browser. WebSocket is the only thing you can use besides individual HTTP requests.

I’ve recently written my own WebSocket server from scratch in C# (for learning purposes). I can successfully send and receive packets from my browser’s JS console to my turtle in Computercraft which does also support websockets in the latest releases :slight_smile:

Unrelated to this question:
Even though UDP is considered an unreliable protocol, unless you really have a bad / noisy connection most packets actually arrive quite reliably. Though they may arrive out of order. Yes, when using UDP you can not rely on the arrival. So when using a UDP to send information you need to arrive reliably, you need to implement your own transmission control. However since this is about websockets it does not apply here. It’s already send over the Transmission Control Protocol.

TCP is not really suited for game related networking. (Not even for reliable messages since you cant specify a scope/channel for your reliability, its connection bases)
You need to use UDP… Sadly UDP is not an option if you need to rely on websockets.

Not sure what I should take out of that. A lot games use TCP, one example is Minecraft Java (yes Bedrock switched to UDP). Most fast paced FPS games use UDP, yes, but the majority of older games usually used TCP and there’s no reason you shouldn’t. It’s only a matter what latency you can / want tolerate. I don’t understand what you mean with what you wrote in your parenthesis. TCP is 100% reliable, always. So what do you mean by scope or channel? A TCP connection is just an endless stream of reliable data. What application data protocol you run on it is completely up to you. Whatever you send it will arrive 100% reliablea dn in order as long as the connection isn’t completely lost which only happens when you actually loose the internet / network connection for too long. Both protocols, UDP and TCP are transported by the IP protocol anyways. TCP just makes sure that no fragment is ever lost. If a dropped packet is detected, it would be resend.

Depending on the type of game in most cases you don’t need that high performance as fast paced FPS shooters. Even games like WoW initially only used TCP. More recently they also used UDP afaik as it gives more options for custom balancing.

TCP works pretty well for most applications and even has the benefit that it actually is reliable out of the box. Yes, on average is has a slightly higher latency and it gets worse quicker when the connection is bad. However it does work just fine.

Note that UDP doesn’t have channels or scopes just like TCP doesn’t have them. You can implement multiplexing in your application protocol, sure. Though this applies for both protocols. However as I said TCP is reliable, so everything arrives in the exact same order as it was send out.

UDP you can define channels or scopes for your reliability. For example in our game each interactable item gets a channelId so if one item does not received its reliable RPC or if it’s out of order and it needs to wait for correct message to arrive it does not block all other items. Only way to achieve this with TCP is to have one connection per item.

Hey, it’s good to discuss stuff and I’m sure it’s all valuable info but also please be mindful of not derailing from the original topic as was done here .

Thanks.

1 Like

Yes, absolutely. We are already far off since the OP already said he does an “.io” game so he is restricted to WebSockets anyways.

Just as a final thought: You may use a higher level protocol that’s based on UDP that does stream / channel multiplexing. UDP is just this. It doesn’t have channels and nothing related to reliabilty. It’s a fire and forget protocol. Anything reliable has to be implemented manually on top.

Anyways without more details on how the networking is currently implemented it’s difficult to give concrete directions what to do.

1 Like

I’m just saying that with UDP you can have much more granular reliability which is not possible with TCP. But since OP is making a web game UDP is not available to him.

I thought it was small messages… So I you suggest, I will try to send only updated data and reduce the size as much as I can.
And do you think it is a good idea for the client to send an acknowledge message (empty) to the server? And the server will only send a new message after receiving the acknowledge message?

Another thing you need to take into consideration is that any data that the client holds will be subject to hacking. So for example they would change player position to teleport anywhere. This can be avoided by letting the server maintain the player info. However for performance reasons it is not always possible.

No, that’s not really necessary with TCP. TCP already has an integrated acknowledge system. That’s how it makes sure all packets have arrived.