Some Thoughts on Deterministic Netcode in GameMaker

Topher Anselmo
9 min readJan 9, 2022

Hello! For the past month or so, I’ve been working on a small side project in GameMaker. Well, it was once a side project, but now it’s kind of become a full game with online multiplayer with a custom matchmaking server. It has a lot of interesting things going on technically, including a launcher/auto-updater, but in this article I want to discuss some of the things I’ve learned during my journey to get online networking functional. This article will be probably be pretty technical. You’ve been warned.

The Game

The game I’ve been working on is called Tiny Tussle TD, and it’s a tower defense game that let’s you play 1 on 1 against a friend. One person is the defender that sets up a maze of walls and towers, and the other person plays as the attacker that coordinates what units to send each wave. The game supports online play via direct peer-to-peer connection or using a server to facilitate connections between players.

Check out the game here!

A given “round” of the game goes like this: The attacker chooses a random buff, then can allocate what units to send during the wave. The defender then chooses a random buff for themselves, modifies their walls and towers, and begins the wave. Both players watch the wave commence, and the cycle repeats. The flow of the game is pretty straightforward, and is completely turn based. Given this, I decided to use deterministic netcode to make it work online.

Deterministic netcode, in as simple terms as possible, is a method of adding online capability to a game by banking on the fact that the game state can be reproduced given the same exact inputs. So for example, if I press certain buttons in the same order on a fresh start of a game, and the game ends up in the same place as before, then the game can make use of determinstic netcode. I won’t go into much more detail than that, but more in-depth explanations exist.

Working Upwards

As I mentioned before, my game has a few different modes of play. You can play locally against a friend by passing the mouse back and forth, you can play with a peer-to-peer connection to another friend online (given that the host port forwards), and you can play online by connecting to a custom server. You can also play against an AI, but that’s a separate topic. My order of operations for getting this to work was as follows:

Local Multiplayer > Peer-to-peer > Server

If I had to do it again, or recommend an approach to someone else, I’d recommend this exact process for a turn-based deterministic game. Here’s my reasoning.

Getting the game to work locally first involved actually architecting the gameplay, and figuring out all of the logic that goes along with it. Online netcode can be tough, and the last thing you want to be doing is struggling to figure out if something isn’t working because there’s a problem with your netcode or because of a problem with the way your game fundamentally works.

After it works locally, tacking on deterministic peer-to-peer networking isn’t so terrible. You simply set up signals for each action or input a player can make, and ensure that the games stay in sync no matter what. You know the game operates correctly locally, so if there’s any issues you can be fairly sure it’s because of your netcode and not anything else.

Finally, making the game work with an online matchmaking server will of course depend on how you implement the server, which can be done so many different ways. Really though, the best way I could find to accomplish this task was… basically by cheating.

Essentially, I created a custom server (using Node) that the game connects to. The server I made facilitates “matches” and allows players to create and join matches by name. Once two players are connected to a match, the server no longer sends any signals of its own. On the GameMaker side of things, I use the exact same code I used for the peer-to-peer bits. I assign the socket I’m using for the server to the socket variable I use for peer-to-peer games. Then, I just have the server pass along any signal it gets from one game to the other. And just like that I had a man in the middle server.

Of course, the process was pretty complicated, and figuring out this flow and architecture took a lot of trial and error. If I had someone tell me up front to just do things in this order and in this way, it would have saved me a lot of time. So hopefully laying it out like this helps anyone else interested in this kind of thing as well.

Nitty Gritty

To get into more detail, the online networking in my game doesn’t send regular updates while the game is running. Instead, it only sends signals to the other game when an action is performed. When the defender presses the “start wave” button, a signal is sent to the other player that the button has been pressed. The game does not send information about each unit that is spawned in the wave, or every shot fired from a defending tower. Since the game always reproduces the same result with the same inputs, the wave will play out the same for each player.

I’m interested in sharing tips and pitfalls I came across while implementing my networking, but I thought it would also be worth showing how I actually communicate between the games. Most games will network with packets constructed to serve a specific purpose. You might write a header on all of your packets, and then have a big switch or something on the receiving end to deal with the packet depending on the header. In my game, I do something a little different.

This function sends a “remote call” to the given socket. Basically, I provide a function name and arguments to that function, and when the packet is received on the other end, it executes that function. More or less a fancy way of saying “run this script on the other player’s game”.

In the networking event, I pass any received data buffer through the above function to parse these remote calls. This setup allowed me to not worry about architecting different kinds of packets and adding a specific handler for each case, and instead let me focus on the logic of networking. Using the system in game is as simple as this:

Which, on the receiving end, translates into: chat(“howdy partner!”)

Working With a Custom Server

I have two important pieces of advice when communicating with a custom TCP server.

The first one, is to always include a header in your packets when using network_send_raw. This can be as simple as a specific few bytes at the start of your packet, or a detailed header that explains the exact data in your packet and the exact size of said data. This is imperative because of packet coalescing. TCP packets are streamed, and sometimes get combined to save on bandwidth. This means that multiple packets can arrive all at once on your server, so having a header that describes your information will be necessary for parsing it out on the other end.

The other piece of advice that I have, is to never forward packets from your server sent from a GameMaker game via network_send at the same time that you send your own custom packet generated on the server. My server forwards information along from one game to another, but during that time it never sends signals of its own. This is because if those two different types of packets coalesce, then GameMaker won’t be able to parse it correctly. The built-in header that gets included when you use network_send can only get correctly handled when it is seen by itself or if it’s combined with other packets of the same kind. If you send other types ofpackets along with it, such as custom data generated by the server, then the GameMaker generated header will appear in your buffer, and you won’t really be able to correctly parse it all out.

These issues caused me endless headaches when developing the server, and it was mostly because I didn’t know that TCP packets could be combined and arrive all at once. Well, they can. So watch out. Punk.

Randomization

When deciding to use deterministic netcode for my game, I knew the biggest hurdle would be making sure the games stayed in sync with regards to randomly generated numbers. Since the games have to always reproduce the same random numbers every time, it was necessary to make sure the seed for each game was exactly the same as the game started. The way that I did this was pretty straightforward.

set_seed is just a wrapper for random_set_seed

I simply generate a random a random positive integer (within the limits of a signed 16 int) to use as a seed, and set the same seed on both players right before the match begins. Of course, this ensures that the the seeds are synced, but there’s more involved to keeping the games in sync.

For one, I had to make sure that all of my random calls were done in a predictable way. This included not calling random functions at times that could be affected by network lag. The biggest hang-up I had in this space was generated random pitches for sound effects.

Dealing With Random Audio Pitches

This kind of code should be familiar to anyone who’s worked with sounds in GM. Most of the time you want repeated sounds to have a slightly different pitch to make it sound less monotonous. However, in my game, I limit the number of times a sound can be played in a single frame. How can this cause a desync? Well, let’s say the defender places 5 walls down, causing this code to trigger 5 times. But let’s say there’s lag in the network, so five “place wall” signals arrive to the other player all at once. Since my game only allows one place wall sound effect in any given frame, that would result in only 1 random_range call on the attacker side, but 5 random_range calls on the defender side. Obviously, this is a no go, because now the game’s randomization states are out of balance.

To fix this, I decided to pre-generate a whole bunch of random numbers into an array when the game starts. Then when the sound plays, it simply plays the random number at the first index of the array and increments that index for next time. Once it reaches the end, it’ll loop back. It doesn’t matter if the index of that array is out of sync, since it only dictates the pitch of the sound, and the outcome is essentially the same.

Okay that’s a lot of info jeez

I think I’ll wrap this up here, but there are still other things I could share about this project that some might find interesting. Perhaps I’ll write another article in the future about it, maybe about the custom launcher/updater system, or specifics on the implementation of my server. Maybe.

I hope the information in this article will prove to be helpful for anyone looking to follow in my footsteps! Thanks for reading y’all.

Links:

--

--

Topher Anselmo

A web developer that likes to tinker and build cool stuff. Also likes long walks on the beach and cheesecake.