I wanna make a fighting game! A practical guide for beginners — part 5

Andrea "Jens" Demetrio
10 min readSep 28, 2021

--

Determinism — the root of all woes

First off, let me start with an apology: I abruptly ended this series of tutorials back in 2018, without inasmuch as a justification for it. Truth is, I went through a phase where I asked myself if I was really able to teach something, as some fellow indie fighting game developers seemed much more knowledgeable than me. This sort of inferiority complex was what made me abandon my endeavor. But — guess what? — four years after my original article, I finally feel “good” enough to resume my endeavor and try to guide you through the hurdles of — finally — making a fighting game!

To restart this series of tutorial with a bang, let’s tackle a theme whose importance I wasn’t aware of until years after my engine was coded and ready to run — a theme that alone makes the difference between a fighting game with online modes or not: the dreaded “Determinism

“Determinism!” <horses whinnying>

What is determinism?

Determinism is the idea that repeating a certain action with the same starting condition will always cause the same outcome. In real life, quantum mechanics throws a spanner in the works, but in games this is actually something doable.

If you make use of emulators, think about loading a save state and repeating the same input over, and over, and over, only to obtain always the same outcome. As an example, imagine you are playing as Ruy in Hyper Road Warriors 2: Octane Edition and you press your Hard Punch button, then you save your state. No matter how many times you load it back, Ruy will always move forward for the same amount of pixels, transition between frames of animations, always following the same timing, and hit Nek with his active hitbox always at the same moment, for the same damage.

Why is this important? Well, for starters, it makes the gameplay feel consistent. It would feel very off if the success or failure of a setup was tied to a (virtual) coin toss, the framerate or other amenities. Secondly — and more importantly — it is absolutely necessary for peer-to-peer online multiplayer.

When your engine is not deterministic, the same action, performed with the exact same initial conditions will cause the game to trigger wildly different results, depending on the computer it is running on, the weather, the baldness level of your boss, and the number of seconds left on your microwave’s timer. The third scenario is WAY exaggerated, but I personally saw that happening once or twice in some test projects of mine [1]

The Online Warrior

One step back. Aside from some exceptions (cough Virtua Fighter 5: Ultimate Showdown cough), fighting games are played online as peer-to-peer (p2p) connections: your and your opponent’s computer exchange data without an intermediary, and the game is run on each of your PCs separately. If there was a server in between, running the game and sending the state back to each contestant, this problem would probably be less evident. But, due to the sheer amount of instances needed to support such a system and to avoid unnecessary additional input lag, p2p is the most alluring option, when setting up an online infrastructure for this kind of games.

Then, what is the problem? Well, the information sent from each copy of the game usually contains only the inputs of each player, plus maybe some metadata to detect disconnections. The game instances on each player’s machine then update their own states, based on the inputs received by the opponent, and the game advances to the next frame.

Now, what happens if the game is not deterministic?

Shit hits the fan.

The hard punch that connected on your screen might whiff on the opponent’s screen. A KO could happen on one player’s station only. Characters would look like moving erratically because the inputs of each players reflect what they are seeing on their monitor, which might be different from what happens on the other side of the connection.

If you are planning to add online multiplayer to your game, you don’t want this at all.

Maybe unsurprisingly, but this paper-bag-doctor guy here was the root cause of MANY early desync issues in the rollback netcode beta of Guilty Gear XX AC+R. The engine was actually deterministic, but not managing certain variables properly was most likely messing with the internal random number generator.

Prevention is better than cure

Making a game deterministic might be hard, especially if you have already built your engine and need to go back and extirpate the root causes of issues. Also, if you are developing for PC or with cross-play in mind, it might be even harder, due to some quirks of the hardware.

The best way to tackle the problem is to make your engine deterministic from the very beginning, unless you are absolutely, positively, indubitably sure you don’t want to add any online multiplayer mode to your fighting game.

And when I say absolutely, positively, indubitably sure, I mean it. There’s no way back: retrofitting determinism in an already complete engine is a hellish endeavor which you don’t want to have to deal with. I speak from my personal experience: if you ever wondered why Schwarzerblitz never received a native online versus mode, here’s your answer.

But, if you are reading this essay, there is a good chance that you are still working on your engine, which is good because you can avoid falling into the same trap.

“You activated my trap card: Retrofitting Determinism! This will force you to spend your next five years fixing your non-deterministic engine, Kaiba!” [2]

And, fortunately, there are indeed silver bullets to make this work, even if some can be painful and require a lot of effort to work properly.

  1. separate logic and rendering: your game’s logic update (movement, input, collisions, damage…) should have no dependency on what is drawn on screen. If you are working with 3D models, make sure that the position of your hitboxes is not calculated at rendering time. Everything that concerns gameplay must be able to be processed separately;
  2. fix your time step: one update frame must be one update frame and must advance the in-game timer by always the same, fixed amount, no matter the framerate. This makes sure that when you are looking at e.g. frame 25 on your machine and your opponent’s machine, the same total amount of time has elapsed. This is especially useful to avoid movement being interpolated differently depending on the elapsed time. For example, if your chosen timestep is 16 milliseconds, It doesn’t matter if in reality 16.8 milliseconds have passed since the last update on your PC and 18.4 on the opponent’s: the next frame will always have to advance the in-game timer by 16 milliseconds, no matter what. Here’s a good reference article about this topic and how to tackle it in practice, from the excellent Gaffer on Games blog;
  3. process inputs once per logic frame: you don’t want a player with a faster PC flood the opponent with sub-frame inputs. Process the input only once per each logic frame. You can collect the input with sub-frame precision, but you shouldn’t act on it as long as it’s not time to update;
  4. avoid using floating point variables: floating point numbers are… rather messy. Sure, there is a well defined standard, but the standard leaves many operations “vendor defined”. As long as you are only using sum, subtraction, multiplication, division and square root you might be fine (emphasis on might), but — for example — sine, cosine, exponential, logarithms, and other commonly used operations can have different results depending on the processor’s architecture, model, manufacturer, and other implementation-specific details. If you are developing for console and you aren’t planning for cross-play, this is arguably not an issue, as the hardware is standardized, but woe is you if PC is your platform of choice. The best way to avoid this problem altogether is sticking to fixed point libraries or just use plain integers. This will save you the horror of having to debug obnoxious desyncs that have no other possible explanations. Truth be told, there are ways to make floats behave more deterministically across all machines, but they are quite complex and might still incur in some issues (again, thanks to Gaffer on Games for the informative link!). So, for your sanity, just avoid the problem altogether. Please, notice that this applies only to the game’s logic update. You are free to use floating point numbers for anything that doesn’t have an impact on the gameplay!
  5. fix your randoms: random numbers add some sauce to some characters. Faust without his hilariously random item launch wouldn’t be as engrossing or interesting. But random numbers must be deterministic too across machines. Be sure you are initializing the random number generator with the same seed for both players, so that the sequence of “random numbers” is the same on both machines and your Faust-function character always draws the correct item. If you are unsure about how to implement it, do it the “dumb” way: fill an array with an arbitrary number of integers (e.g. 255) in a pre-determined “random”, hard-coded order and advance a counter by one at every new random number request, “folding” it back to 0 when reaching the end of the array. Okay, it’s rough, but it works and it’s simple;
  6. ditch Unity Physics and Unity Animator. No, seriously: if you are using Unity, throw away the built-in physics engine and don’t look back. That thing isn’t even deterministic on the same device. I have heard and seen desync horror stories from developer friends who professionally work on Unity multiplayer games. Write your own physics engine without using any floating point operation and don’t look back — after all, traditional fighting games just need gravity and collisions, so the endeavor should be relatively doable, even if I wouldn’t call it easy for beginners. Bonus points for all the other Unity features that aren’t deterministic. Seriously, the best way to have a deterministic game on Unity is to run the game logic independently from it and simply use Unity for rendering the game on screen. For some additional details on how to implement a deterministic physics engine in Unity, this Reddit thread might be of help, but realistically you should go for something more simple, if this is your first game.

All of the above should make sure that your game is deterministic enough to be playable online with only the inputs exchanged between the players. This is also the baseline for having a good delay netcode.

What about rollback?

If you are interested in fighting games, you might be familiar with the concept of rollback netcode. Put it simply, instead of waiting for the other player’s inputs, the local copy of the game repeats the inputs received at the last frame to advance the game’s logic state and “rolls back” to the previously confirmed state if the repeated input didn’t match the real input received from the opponent. The game then “rolls forward” and re-simulates all frames from the wrong prediction on, thus syncing again the two clients. In addition to the determinism outlined above, to accommodate this kind of online setup, you need to take care of several other quirks and design your engine accordingly— but this is arguably material for another article.

If you think your game engine is bad because it is non-deterministic, it could be worse: it could be non-deterministic and run Blitz Netcode [3]

I have already built my engine without thinking about determinism. What do I do?

I have been in this exact situation. It sucks and there is no way to make the pill sweeter. The hard, truthful answer is that either you rework your engine to be deterministic or you accept the fact that your game can only be played locally. There is no magic way to turn an indeterministic engine into a deterministic one without a lot of effort. In this case, my suggestion is to think about your priorities: you can still make a memorable game without online modes, but you will need to offer something else to your players. If you absolutely cannot or don’t want to work to solve this issue, you might as well focus on a rich set of single player and local modes instead, refining training mode, adding precise tutorials and combo trials. It’s not a panacea and won’t suddenly make your game a commercial success, but it’s another way to try to cut yourself a niche.

There are also ways around like Parsec and Steam Remote Play, which, despite the limitations, offer the best alternatives for playing offline games online with friends. Some competitive communities have been built on Parsec (Schwarzerblitz and Beatdown Dungeon, only to cite two of them), so this can work well as a fallback solution. Just be aware of its limitations and know it won’t be as good as a proper online mode, but will spare you many headaches if you find yourself in this sticky situation.

What next?

I realize this article was a bit too much on the technical side of things, but I needed it as an anchor for more content to come! Next time, we will delve into the art of building an input interpreter, with some practical examples and applied solutions!

Notes and additional credits

[1] Sprites from Super Street Fighter 2, downloaded from The Spriters Resource. Credits to: Cavery210, OmniGamerX, T0misaurus for ripping Ryu’s sprite; Luis-MortalKombat14, xxxJohnnieWalker2005 for ripping Ken’s sprite;
Maxim
for ripping Ken’s stage

[2] Fake card “retrofitting determinism” created by means of https://www.cardmaker.net/yugioh/

[3] Game: Infinity Versus, clip recorded by GriffyBones

Other articles in the series

  • Part 1 — Introduction;
  • Part 2 — Design decisions;
  • Part 3 — Characters as state machines;
  • Part 4 — Hitboxes and hurtboxes;
  • Part 5 — Determinism;
  • Part 6 — Input buffers and “reading” moves;
  • Part 7 — Hit stun and combo systems;
  • Part 8 — Implementing a simple block/guard system;

If you are interested in more game-making tutorials, you can find me on Twitter at @AndreaDProjects. My DMs are open!

--

--

Andrea "Jens" Demetrio

PhD in Physics, indie game developer, fighting games connaisseur (he/him).