CodeTravelAbout Me

Remaking a dead Multiplayer Java Game - Lentokonepeli

So close, yet so far from success.
August 24, 2020

Lentokonepeli

When I was a teen I used to play on a gaming website called Playforia around 2008. It was built on Java and had a sizeable community of players from around the world. They had many multiplayer games such as pool, cannons, minigolf, and many more. Since all of the games were custom made for the website, it created a wholesome feeling of community fun.

One of my favorite games was a game called Dogfight, (Lentokonepeli in Finnish). It was a World-War-1 themed airplane combat game which pit the Central team vs. the Allies. The first team to destroy the other's aircraft hangars was declared the winner. This innocuous looking game was easy to start playing, yet had a very high skill ceiling. But most of all, it was fun. Lots of fun. Clans formed over time and competed against each other. Some dedicated players had spent thousands of hours mastering the mechanics of the game in order to out-gun other players. I had played this pretty routinely back in the day.

demo

A screenshot from live gameplay of my remake.

A few years ago in 2018, I randomly remembered this game, and thought I'd go check it out. To my surprise, there were no available rooms to join, and no active players at all. It was a ghost town. A large part of the decline in the website's overall popularity was because it was built on Java. As major web browsers began to phase out Java, it slowly strangled the playerbase to death. Seeing the writing on the wall, I downloaded the client-side JAR file from the server to preserve what I could. Less than a few months later, the website admitted defeat and shut its doors permanently.

With the death of the website, many fans and players lost access to their favorite games and community. Around 2019, I had the idea of trying to remake the game. It seemed simple enough, and I also liked the idea of a free, open source version that could be preserved and maintained forever, not being owned by a single company. I had the game's original assets, images, sounds, and client-side code. Could that be enough?

Creating a Remake

I made a Discord server and got to work. The modern web is the perfect coding playground to host such a game nowadays. The rendering library PIXI.js seemed like a perfect fit for drawing the game. I envisioned a game built on Node.js so that game logic could be written once and shared between the server and the client. This turned out to be a huge advantage, because I could simulate a server on the client side, so that way a static website could run the game without needing to run on a server. This meant that any player testing the game could load an ordinary webpage and tweak game physics, and other settings, getting immediate feedback in order to fine-tune the physics.

Here is a link to the latest version of the remake, running inside a simulated local server on your web browser. You can jump right in and start flying planes, as well as tweak their physics values. As you can see, a lot of the game's functionality and physics have been implemented, but it isn't finished. (Press D to toggle a debug view)

Challenges of Remaking a Game

As the sole developer, I had a lot on my plate, and I often feared I had bitten off more than I could chew. In order for the game to feel the same, I needed to simulate it as accurately as possible. This was made infinitely harder by the fact that the game wasn't hosted live anymore for testing, and was only captured in old, sparse, low-quality youtube videos. Attempting to recreate the game put me up against a lot of interesting challenges, most of which I was able to overcome.

Unfortunately, I wasn't able to accurately reproduce the most fundamental part of the game: The flight physics. There was nothing in the client-side code to help me figure out how to implement them. I had a friend who is a physics graduate try to decipher the physics, and he got pretty close. After weeks of tampering with the physics, we simply couldn't capture the right feeling. No matter how much we tweaked the physics, the loyal players who remembered the game just didn't agree with how it felt, and we reached a stand-still. After spending hundreds of hours in total on this project, this is where I am choosing to leave it behind. What we ended up with feels like a Chinese knock-off of something great.

Despite the sad ending, I learned a bunch of interesting things about game development since I built everything (minus the renderer) from scratch. I'd like to also use this blog post to detail one of the fun things I tackled while creating the remake:

Optimizing Network Traffic

Surprisingly, one of the most fun and rewarding challenges was designing and optimizing the amount of data that the game sends about these objects over the network. The game server and client communicate over WebSockets. The game logic runs at 60 Hz on the server, meaning that the game calculates physics, player movement, collisions, and everything else at 60 times per second. It also has to broadcast this information to every connected player in order to keep their game up to date and accurate.

There are many objects in the game such as planes, pilots, bombs, bullets, airfields, the ground, and so on. They are all game objects which can interact with each another. Each game object has unique properties, and some can change every time the game updates.

For example, let's consider the properties of an example plane in our game in JSON form:

{
    x: 994,
    y: 174,
    planeType: 7,
    team: 0,
    direction: 145,
    health: 100,
    fuel: 226,
    ammo: 90,
    bombs: 0
}

As we can see, each plane has 9 properties. After using JSON.stringify on the above object, we get a 100 byte string. Now let's pretend we have 20 players flying around in a game, and that on average, the payload is going to stay around this size.

20 players * 100 bytes per update = 2000 bytes per game tick = 2000 * 60 ticks per second = 120,000 bytes per second = 7,200,000 bytes per minute = 432,000,000 bytes per hour. 432MB per hour is a lot of data being transferred for a simple 2D game with 20 players! To put that into perspective, Fortnite uses ~100MB per hour, and CS:GO uses ~250MB.

We could cut the simulation down by half to 30 Hz and theoretically half our output, but then the accuracy of our game simulation suffers. There is a better way. It's clear that sending the entire state of the game objects on every frame is incredibly wasteful. Imagine if a player is standing on the ground and not moving. Why would we keep sending the location of a player who is standing still over the network?

The first optimization I made was to only send changes that have happened to the game objects since the last time they were processed. Now this makes it so that only properties that change rapidly every tick will update (such as X and Y positions), and things that change less frequently (fuel, bullets, bombs) will be sent out less frequently. Unchanging properties such as the team of a plane, or the type of a plane will only be sent out once when the object is created.

Let's start with a new scenario. A game tick has just been calculated between two players who are flying. Both of their plane's X and Y coordinates have been updated, and one of the plane's fuel has decreased. We want to send this information in a packet to the client:

{type: 7, data: {7: {4: {type: 7, x: 994, y: 174, fuel: 226}, 5: {type: 7, x: -1513, y: 238}}}}

An example packet with changes to game objects.

The highest level type is an enum that tells the client what type of packet is coming. data holds a nested object of game objects, and each game object holds objects which are indexed by their own unique ID's. Each instance of a game object only has its own changes included.

Our example above is a 100 byte string for two planes. It would have been 200 bytes if we used the "send everything" approach with two planes. With just a little effort we've already reduced the size by >50%, and the savings would be significantly greater the more planes you add.

Only sending a small JSON object with relevant changes to game objects already saves us quite a bit of data, but once again, we can do better.

Binary Compression

since JSON is sent over the network as a string, it has a lot of overhead. Labels such as "fuel":, and characters such as ", :, and , are all just fluff in order to keep the structure of the JSON object in tact. In fact, we wouldn't even need to have these labels if we knew the order and size of the properties of an object and sent them in a specific way every time. By creating our own custom binary protocol, we can cram a lot of information into a small stream of bytes.

The compression algorithm is as follows:

  1. Always begin each packet with 1 byte to describe the type of data being sent so the client knows how to read it.

For each game object (repeat):

  1. Add one byte for type of object
  2. Follow that with a two byte unique ID for that object (allows 2^16 = 65,536 unique IDs for each type)
  3. Use n number of bytes as a bitmask, where bits set to 1 are the properties that are being sent immediately proceeding this bitmask, in order.

For each property set in the bitmask:

  1. Write n bytes depending on how large the property is

And repeat until the end.

binary

Example packet of my custom binary game object protocol.

Applying that compression to the JSON data, we now have all of that information packed into this little binary update which is only 20 bytes (shown as a hexadecimal string here with spaces for ease of reading):

07 07 00 04 43 00 03 E2 00 AE E2 07 00 05 03 00 FA 17 00 EE

~5x smaller than our previous optimization, and ~20x smaller than the "send the world" approach.

Final Thoughts

All and all, it was a fun project to work on, But it was mentally taxing having to write nearly everything from scratch. It was enjoyable to see the game start to have new life again. At one point, we even had the programmer of the original game join our server. He searched for the original sources to try and help us, but they were long gone and it seems like they will never be shared if they do still exist somewhere. I wish I were able to figure out how to fix the physics, but I can't. It seems like this is where this project ends for now. If the original sources are found, or someone figures out the physics, I will complete the game, but that doesn't seem likely.