Fighting Game with Rollback Netcode
NAIR is an exercise in intentional, minimalist game design and controlled scope.
Originally developed and released in Summer, 2019, I have continually added to the game in short sprints, turning it into what it is today. The game boasts top of the line netcode, over 3000 players across the world, and frequent tournaments with community-donated prizes. NAIR makes frequent appearances at local trade shows, has featured in the news, is currently reviewed 95% positively on Steam, and has gone viral several times.
Below, you'll find a rough project timeline, code snippets, & testimonials.
Play it in browser, on this website!
Rough Timeline:NAIR was developed in several sprints, lasting about a month each. Roughly:
July 2019 : Original game is developed and released August 1st on Itch.io. This version has had its source code released.
January 2020 : Work on Unity enhanced port begins. Project dropped at the onset of COVID-19.
May 2021 : Work on Unity port resumes, this time with a focus on rollback netcode. Released in june on Itch.io for free.
August 2021 : Game and netcode is ported to Steam, released mid-September.
Development has been ongoing since the Steam release, with regular content updates.
Throughout the Steam release and since, I've used Trello to keep myself focused and organized. It essentially boils down to a To-Do list, a bug tracker, and some out of scope stretch goals (that I still intend to implement, eventually).
NAIR has been played by thousands of people, has had 2 successful releases, and currently holds a 95% Very Positive rating on Steam. The game has received almost universal praise for its movement, pace, fun factor, art style, and everything else.
For more information, check out:
This review by YouTuber Big Yellow
This news article about the launch
This interview I gave shortly after launch
This video interview from a Game Fest in 2022
One of the game's achievements going viral
NAIR was originally made in Monogame, and later ported to Unity to save time. Unity was chosen becuase of its shared programming language, C#.
For more information about the game's programming, you can check out the original version's source code.
The netcode utilizes GGPO, a C++ rollback library, and a Unity-focused C# wrapper. Both of these needed to be modified by me to support external networking services, such as Steam, and to keep
it up and running at all. GGPO ships with built-in UDP code, which needed to be scrapped. Instead, I define my own networking calls in NAIR's network manager, and pass function pointers through the wrapper, which has also been modified.
On the gameplay side, let me talk about one of my favorite small optimizations I made to get NAIR running online.
The original version of NAIR allowed full analog control of the character with your stick, allowing the player to control things like aerial drift, run speeds, airdodge and wavedash lengths, and more. However, since I had to transmit the player's inputs over the wire, storing it as two floats didn't leave much room for buttons. I managed to compress these values into two bytes. I do this by representing the x and y values as ranges 0-200, with 0-100 representing -1.00 to 0.00 and 100-200 representing 0.00 to 1.00. I lossily throw out anything beyond 2 decimals of precision, and this still represents a wide enough range to not have any observable impact on gameplay. If it did, though, this change also does not go into effect on offline matches. The code looks like this:
"restartInput" and "restartDetected" are two bools I used to track if either player wanted to start a new match- there's a small amount of lag at the start of a new GGPO session while it syncs with the opponent. Most fighting games cover this up with a cutscene, but I didn't have that luxury, nor did I want players to have to wait for new games as my game's matches can be fast. I instead opted to make the entire game state restartable within the game loop. I ended up needing to rollback-proof bits of the UI itself, as well as the restart and quit requests. This was one of the hardest things to get feeling right, and I didn't get it flawless until update 3.1.0, with some help from a beta tester on the game Rivals of Aether. The key ended up being to separate the restart "inputs" from separate trackers in the game state class, since GGPO syncs these two data sets differently. The code looks like this:
Regarding the text if-statement, when a player disconnects online, I place the remaining player in an offline match against a dummy. Despite closing GGPO, this match is in the exact state the online match was left in, and the transition is generally seamless. I frequently ignore calls to change the message when this happens, as I don't want the player believing they are still in a match.