Network Packet Optimization

Hello.

How can I optimize network packets to be as smooth as possible as well as to reduce bandwidth usage when constant packets are sent by the server.

For example, every time a player fires a weapon, client sends constant "firewp" packet to the server. The server then proceed to send back packets to client constantly until the bullet is finished and there's nothing to hit, updating sound base on player coordinate etc.

Here is example code I use to play sound from the server which sends a packet to the player's client for the sound to actually play.

void play(string snd, double x, double y, double z, string m, bool reliable = true, bool onlyother = true, int index = -1, string alt_snd = "") {
	if (players.length() > 0) {
		for (uint b = 0; b < players.length(); b++) {
			if (m != "" && players[b].map != m) continue;
			if (index > -1 && onlyother && players[index].peer_id == players[b].peer_id) continue;
			messager a;
			a.add("message", "playsound");
			a.add("soundname", snd);
			a.add("lx", players[b].x);
			a.add("ly", players[b].y);
			a.add("lz", players[b].z);
			a.add("x", x);
			a.add("y", y);
			a.add("z", z);
			if (alt_snd != "") a.add("alt_snd", alt_snd);
			uint id = players[b].peer_id;
			send(id, a, sndchannel, reliable);
		}
	}
	return;
}

Note: The messager class is used from dictionary for easy sending which serializes the packet when sent, and deserializes the received packet if so.

So I will try coming back here and giving more details based on what you are interested in, but just a couple random things to think about for the moment.

  1. Why send the coordinates of all sounds to the client which can already know the coordinates of all players? That makes the packets larger.
  2. Why send continuous machinegun fire sounds when the client already knows or can be made to know the speed of the weapon being fired? You could instead send fire_start and fire_stop packets.
  3. You can enable enet's ranged packet coder by setting network.packet_compression = true on both client and server, or you can use string_deflate and string_inflate yourself.
  4. If you really want, you could use datastreams to write your packets in binary, such as with stream.write_int(), stream.read_float etc. Probably not necessary though.
  5. To some extent, constant bandwidth is OK, for example a typical voice chat client does constant communication and remains stable. You should determine whether the issue is your packet handling or your packet reception/management?

Basically everybody seems to do this.

While (true) { // game loop
	wait(5);
	const network_event@ e = net.request();
	handle_message(e);
}

Which is super laggy, and only allows for one packet to be handled every 5 milliseconds. But what if you got 3 packets in the last 5 milliseconds? You need to do:

while (true) { // game loop
	wait(5);
	network_event e;
	while ((e = net.request()).type != event_none)
		handle_message(e);
}

Or at the very least, allow the network library to handle your waiting for you, such as:

while(true) {
	refresh_window();
	const network_event@ e = net.request(5);
	handle_message(e);
}

In this last case, the network object will wait up to 5ms for a packet but will also return instantly if one is found, thus no spurious waiting between packets. We need to manually call refresh_window() in this case because the wait() function usually does that, which we are no longer calling.

Hopefully this pokes the brain a bit, let me know if you want me to elaborate on any of this!

1 Like

Thanks. I'll head to it now

Hello.

Could you show an example of using datastream as you've said in the message?

In the NVGT discord server someone is talking about an msgpack library that they might release, which might be your best bet for binary like packet serialization.

You can do things like this though, just a note that I wrote this code in the forum editor as a proof of concept so it might have compilation errors.

enum commands { walk, jump, shoot }
void send_shoot() {
 datastream ds;
 ds.write_uint8(shoot).write_float(player.x).write_float(player.y).write_float(player.z);
 net.send_reliable(player.peer_id, ds.str(), 0);
}
//And on the other end
void handle_packet(const network_event@ event) {
 datastream ds(event.message);
 uint8 cmd = ds.read_uint8();
 if (cmd == shoot) {
  float x = ds.read_float(), y = ds.read_float(), z = ds.read_float();
  // Handle the message
 }
}

The advantage is that those x y and z coordinates are now in only 12 bytes. Instead of sending"shoot 89.573263 382.558273853 10", you send a 13 byte packet only that doesn't need to be parsed from a string so thoroughly.

As I said earlier, probably best to use something more established as opposed to manipulating bytes directly.