banner.png
Formerly available on iseeicy.itch.io

Formerly available on iseeicy.itch.io

OST available at fizzypopnesta.bandcamp.com

Introduction

At the time of writing, the date is April 24th, 2021. It has been one year since mass - one of my favorite game projects - was released.

Happy birthday mass! If you were still online, you would be one year old! mass died on June 22nd, 2020 - and I find myself reminded of the project fairly often. It was important to me for a variety of reasons: it was a chance to work with some good friends, it was the first time I had released a game without the cushion of a jam for support, it was a chance for me to experiment with new skills, and on top of everything it was just… a weird project in general.

For the uninitiated, mass was an online-only game where the player (and any other players in the world) were responsible for performing maintenance on various life-support modules to keep mass alive. mass - referring to a large orb-like object in game - and also referring to the game itself. If mass’s life-support modules weren’t taken care of, their energy would drain and mass’s life would as well. To reiterate, mass is an online-only game and was designed to run off of a single centralized server. That means that there was only one mass and that everybody in the world who ever played mass was responsible for keeping it alive. If mass dies, then the game shuts down. Forever.

And that’s what happened - mass existed for a period of time and then one day it suddenly didn’t. I’m skipping over a lot here of course, along the way there were a lot of technical aspects, social aspects, and a few story aspects that evolved as mass lived - but at the end of the day, it’s over. Full stop.

One year later, I’d like to take some time to reflect on mass as a project, and share details on how it was made as well as share some stories about it’s creation!

The Jam

4:06 PM, April 18th, 2020. It’s midterms week for me and I’m incredibly frustrated with myself and my inability to stay focused through remote classes. While browsing Twitter, I catch wind that Ludum Dare 46 was currently ongoing, and that it finally had a decent theme: “Keep it alive”. On a whim I message my good friend Johnny Download.

e0191120b38784a08d2b1ca60d810dab.png
c0d0caff6a67d192d1a2febcd2147ea0.png

And like that, we were off!

This project started mainly as a collaboration between Johnny Download and myself. He had been learning how to use Blender, and I thought it would be fun to collaborate on something! Along the way, SJQ also helped us with some of the game’s sound design and video work, and Nesta tagged in to help us give each in-game character passive music. We signed up for LD46, and by the end of the jam we had… something. It was a functional project, but it wasn’t polished. I had been dealing with migraines every day throughout that month, and it was just not possible for me to finish the development side of the project in time for the jam.

The first working iteration of mass.

The first working iteration of mass.

We weighed our options - either we release a sub-par project so that we qualify for the jam and just go back and touch it up later, or instead exit the jam and focus on a more polished release at the risk of not having any players. Releasing a game for the Ludum Dare is a really beneficial experience from the sense that (as long as you’re playing and leaving feedback on other participant’s games) you’re basically guaranteed to have others play and review your game. Ultimately though, Mr. Download and I decided that it would make more sense to exit the jam, take it a bit slow, and focus on a more polished release.

After making this decision, we worried about how realistic it would be to expect people to keep mass alive. With no jam to cushion our initial release with players, would mass even last the first day without our help? In the period of time between the Ludum Dare ending and our initial launch, we ended up re-balancing the game’s life-support systems to account for this. If I remember correctly, I think we had planned on each node needing to be recharged every 6 hours or so - whereas in the released game it was about double that - every 12 hours or so. Even after this change, it was hard to shake the feeling of “did we just spend a week making something that’s going to disappear in a day?”. I can not stress this enough - our initial expectation was that Johnny Download and I would have to monitor & babysit mass to keep it alive. Our internal plan was that if mass didn’t gain any traction after a week or so, we’d let it die.

Luckily, that didn’t happen!

Launch & The Community

We launched mass at 8:18 PM on April 24th, 2020.

Fairly quickly a few players gave it a shot and wandered around. The specific details of our launch is pretty fuzzy now - but if there’s anything I remember, it was the quick traction that mass picked up. Within a few days Free Games Planet picked it up and wrote an article on it (thank you!!! You all rock!!) which gave us a great signal boost. We came across a few players that had grouped up online to check in on it. A few days later, I decided to finally commit to creating a public Discord for my content, and used it as a place to gather mass discussion into one place.

The Discord filled pretty fast. On day one of its existence, it burst into motion. Quickly a small but dedicated community formed around keeping mass alive. It was incredibly heartwarming to see, and it calmed my nerves about mass’s potential premature demise right away. Sure, mass could die still - but at the very least, a handful of people were passionate about keeping it alive - and that was probably one of the biggest things that mass as a project achieved.

NightmareFluid - ButterAleks kisses mass

NightmareFluid - ButterAleks kisses mass

Thank you to everybody - to people who logged on just to look around - to people who had a timer for LS2 set - to people who drew art - to people who archived their interactions - to everybody. Your involvement with and passion for mass was a necessary component to make mass work as an art piece. The only way something can be missed is if there is someone to miss it - and for that, I am grateful.

Visual Breakdown

mass has probably the most striking visuals of any project that I’ve put out. It’s sorta retro and sorta something new. I had been wanting to make a 3D project that made use of dither for a little while, and this was my chance to do it!

Post Processing Breakdown

We start with 3D geometry…

…which results in this scene when rendered.

Then, we add some really blue fog and some dust particles to drive in the sense of space and fill in the background.

Though - this isn’t entirely correct. Instead of what you see above, we actually render the game into a render texture at a much lower resolution with a separate camera then scale it up to the user’s screen size, which gives us…

…this! This gives us nice sharp pixels at whatever resolution the user is running at, and ALSO has the side effect of making the game easier to render (which was a VITAL component to rendering mass itself - more on that later!).

Then, we apply some generic post-processing effects (bloom, a tiny bit of grain, a tiny bit of ambient occlusion)

And to give it some flair, we apply some color dithering on top! I really like how the dithered bloom looks :D

mmmm… Dithered bloom…

For dithering, we used this simple but amazing shader from the Unity Asset Store called… Dithering. Simple shader, simple name. Straight and to the point! If you’re trying to achieve this look, I highly recommend it.

To render mass itself, we used a much more complex plugin - the Raymarching Toolkit for Unity.

I picked up this asset at its launch and had been looking for a use for it since. mass turned out to be the perfect fit. The raymarching toolkit is great at rendering constantly moving, twisting, altering shapes, but it comes at a performance cost. Raymarching is expensive, especially at high resolutions. But… mass is a really really low resolution! It was a good fit because mass’s internal resolution was 320x180 (literally a 6th of 1080p)! This meant that even players with a toaster of a laptop could play the game (which is ideal for a game with relatively short play sessions).

We rendered mass itself by rendering a raymarched sphere with a custom shader, and applying a ton of modifiers to it to make it move and react. On top of this, mass is the central light source for the whole scene - and that light’s brightness and color changes with mass’s state. In total, mass uses 6 different raymarched modifiers: three sine wave displacements, one noise displacement, one bend, and one twist modifier. These modifiers are all layered on top of the sphere, and produce its different movements. For each life-support module’s percentage, there is a set of parameters on these modifiers that it tweaks. This means that if LS1 goes down to 50%, then it’ll tweak a few of the parameters on these modifiers about half as much as it would at 0%. Each life-support module was tied to different parameters - which meant that mass would look visibly different when different life support modules were low. It also meant that when multiple modules were low in combination, those modifiers would overlap and interact with each other, creating more complex visuals states. Below is a video showing this in action.

Networking Intro

Okay.

A snippet of Incarta’95, 3D BLAST and I testing len.

Before mass, other multiplayer games that I had worked on (such as Dreams of Being and Eternal September) were based on Unity’s ancient and now deprecated built-in networking, UNet, which I had hosted by running the server on a spare computer. I had never worked on something that was meant to be online 24/7, and (ideally) last a long time. Additionally, I had never really written any backend code… When quarantine started in 2020, I started a side project on a whim - a virtual venue called len. It kickstarted me into learning how to use Photon PUN and NodeJS. When I decided to work on mass about a month later, Photon and Node were the tools I decided to use.

Photon Cloud is a service that allows you to write and host multiplayer games on Photon’s Cloud servers. All of the servers are managed automatically, and they’re spun up and spun down at a moment’s notice. All you have to do from a developer side is say “Hey! We want to join a room with this name” and you’re ready to go. They offer a free service where you can have up to 20 players online at the same time - which for massive games is pretty small, but for something like mass is perfect. Photon presents an issue though - it’s almost too easy. Photon Cloud servers are non-authoritative, meaning that all game logic is driven by the players connected.

nonauth.png

Non-authoritative servers are fine for certain types of games. For example - if you’re playing a short game with a few friends, it’s probably great! For a game like Tabletop Simulator it would probably work great. For something like VRChat, it would work great as well! But for something like playing a Counter-Strike match with some random people over the internet, it’s not great. From my understanding, non-authoritative means that the server doesn’t have authority over what the players are doing. A game client can say “Hey! I shot this guy and it did this much damage” and the server will be like “yeah, cool, sweet”. This is fine for a game where there’s not a ton going on, or where you trust the players involved - but otherwise, there are some issues. For example: what happens if somebody modifies the game client? They could make it so that the game client is sending “Hey! I shot this guy and it did this much damage” every FRAME for every person in the lobby - and the server would still be like “yeah, cool, sweet”.

This wasn’t going to cut it for mass. Imagine a scenario in which somebody modified their client to just kill it outright - or bring it back to life? Or worse, what if the game client bugged out and did one of those two things on accident? Not a fun time. To add to this, mass also needed to be persistent. If we had used only Photon Cloud to run mass, where would its data have been stored? We could use a database, but how would we allow the game client to pull from that database without giving it direct access to the database itself?

The Initial Backend Stack

In order to develop multiplayer quickly we used Photon Cloud to handle basic player synchronization. To hold mass’s state and give that information to the player, we used Heroku to host a NodeJS server and MongoDB to hold any data. To actually expose mass’s state we used… GET and POST requests.

All of mass’s multiplayer interactions other than player synchronization was handled through HTML requests. The game client was set to ping the server with a GET request every few seconds in order to get mass’s current state. When the client performed an action in-game such as recharging a node, it sent a POST request to the Node backend and also set an event through Photon Cloud in order to sync audio & tell the client to update mass’s state if they haven’t already.

Now - you may be thinking “how is that any different from a non-authoritative server”? …And you’d be right for thinking that!! Because it’s not!! It 100% was a non-authoritative server - though using a backend alongside Photon did make things less reckless than handling all of our netcode on the client side. For one, with the Node backend we never expose the database which is always a good thing. The Node backend also only really allowed you to do a few things, which was just getting the current state and recharging each node.

The hidden Life Support module viewer that we hid.

The hidden Life Support module viewer that we hid.

This backend solution was incredibly hacky, and by no means uncrackable - but it worked! Because mass’s state changes fairly slowly, we could get away with just firing off a GET request every few seconds. Events that needed to be communicated between players quickly (such as the sounds for a life-support module being recharged) were handled through Photon, because they were fairly innocent events.

This meant that mass as a game was essentially just a 3D interface for using an HTML API. Technically any program could get mass’s current state, or recharge its modules. To play with this idea, we made a life-support module viewer webpage so that we could check up on mass without logging in to the game. We created the web viewer when we had the mindset of “mass isn’t going to last a week” - and to access it all you needed to do was navigate to the Node server’s URL. It was super out in the open, and after mass started gaining traction we had to hide it for a while before I eventually removed it.

A Race Against The Players 

A god tier verification function

A god tier verification function.

While this system worked great, at the end of the day it wasn’t sustainable. While nobody could kill mass, people could still cheat in keeping it alive. For each GET request there was no verification. This meant that (for example) anyone could send an `/attempt-pulse-1` request to recharge LS1 at any time - no puzzle solving required. We released mass knowing that this was an issue, and prayed that nobody would find out about it.

This turned into a race against the players. After the game launched and a community started to form, the pressure was on for me to fix this. I knew that the more interest mass generated, the more likely it would become for a player to get curious and start poking around the mass client. During this period a player actually DID start poking around and would have discovered how dead easy the server was to manipulate - if it wasn’t for HTTPS. HTTPS encrypts the packets it sends which means that they were unable to read them (phew)! If they had decompiled the client though… that would have been it.

Futile attempt at trying to hide the Node server’s URL.

Futile attempt at trying to hide the Node server’s URL.

As I made changes and tweaks to mass and its server, I attempted to make the client’s networking implementation more esoteric. I recall implementing WebHooks with Photon & the Node server to create a sort of anonymous login system so that players would only be authorized to use GET methods if they had logged in through Photon. I also remember encoding the URL of each GET method in the code with some cipher, and having it decode it at runtime (lol). Stuff like that.

In the patches that followed after mass’s release, I slowly chipped away at solving this. Something that I couldn’t figure out during the game’s initial development was getting websockets to work with Unity. This was the most obvious next step to me at the time. Websockets allow for proper two-way communication between a webapp and a client. HTML requests only get you so far, because the server can’t send back a response unprompted. Websockets emulate proper sockets, which provide this capability! I used a library called NativeWebSockets to do this.

An intricate drawing of a websocket icon I found on Google Images.

An intricate drawing of a websocket icon I found on Google Images.

With websockets working, I entirely rewrote the server (because it was actual lasagna at this point) and ported each Life Support module’s logic over to run on the Node server. Up until this point, if one player started solving a puzzle and then left it, the other players wouldn’t see it - puzzles were entirely client-side. Using websockets I made them entirely server-side, which had the side effect of making the server a bit harder to cheat!

For example - before, to charge life-support module 3 the client would send a GET request to the node server at `/attempt-pulse-3`. With websockets implemented and the puzzles being server-side, the client needs to send a message to interact with individual parts of the puzzle - and only when the puzzle is seen as completed on the server-side does LS3 get recharged. This still isn’t cheat-proof - one could write a script to solve each puzzle - but it’s at least a little bit more difficult. In the future I think that the next level of this would have been actually having player state synchronization on the Node server, and using that to assist in verifying player actions. In the case of LS3, only accept a player’s interaction with the piece of the puzzle if they’re within a certain radius of the object they’re interacting with and facing its direction.

Addressable Assets

Something else that I worked hard on and was excited about using was Unity’s newer Addressable Asset system. In summary, Addressable Assets let you package assets together and easily load them asynchronously through code from different locations - whether that’s on your computer, or in the cloud.

Duct-tape fixing LS3’s broken pipe.

Duct-tape fixing LS3’s broken pipe.

Addressable Assets excited me particularly because of their ability to be loaded easily from a web server. This meant that I could use Addressables to load things like models, sounds, animations, etc from a webserver on the fly. Almost immediately I used this to make a dynamic playermodel system. Dynamic, meaning that I could create interchangeable playermodels that weren’t stored in the build at all. There’s so many possibilities for this, though we planned on using it to add new RMOs without updates and also did use it to hide the Iseeicy playermodel for the LS3 event.

We also planned on using this to make the mass environment change after events and have lasting effects. After the LS3-breaking event, this was showcased with the duct-tape left on one of its pipes.

The First (and Last) Event

screen_512x512_2020-04-24_18-03-29 1024x.png

I had planned mass version 1.3 to be a larger update. It brought huge changes to the server and client infrastructure, and also implemented a login system, the addressable asset system, and made all of the puzzles synced. While writing the update I had tinkered with the idea of having one of the modules fail, and running a small event around that. The purpose was to gauge interest on RP events like this and also showcase all of the changes the update brought. Initially, I had planned on running this event well after the update came out to make it more of a shock. So many people logged in for the update though, I just couldn’t hold back from running it that day. Let’s go over how I pulled it off!

Alongside version 1.3 I added support for life-support modules to have alternate states. This was simple - the backend just stored an extra number alongside each life-support module's state information. This number referred to what kind of alternate state the module was in. For every other module, this did nothing, but for LS3 it determined if it was broken or not.

To actually set LS3’s alternate state, I made a special developer console command for telling the backend server to do certain things - in this case, break or fix LS3. I also added a secret developer console command to login as an alternate user. By default, when a user connects to mass through photon, they’re given a random user ID, random RMO playermodel, and allowed to connect - UNLESS their client specifies a username and password. The mass client doesn’t do this by default, but like I mentioned above, I added an extra command to do this ingame. I set up some basic user accounts for authorized characters (such as Iseeicy) and also configured it so that those users got specific playermodels, and were authorized to run special commands on the server.

Video just after the LS3 event. Recorded by PurpGurp.

With all of these systems in place, I was able to:

  • Log into mass normally

  • Run a command to set LS3’s alternate state as broken

  • Log in later as Iseeicy

  • Run around in-game and interact with players, sending chat messages (another special developer command)

  • Run a command to set LS3’s alternate state as fixed

  • Update the Addressable Asset used for adding content to the scene in order to add duct-tape to LS3’s broken pipe

Conclusion

This wasn’t exactly a traditional game dev post-mortem, but I figure because it’s still technically a post-mortem (it’s after mass died!) I can get away with it. Whether you’re intimately familiar with mass or have never seen mass before this article, I hope that this served as an interesting insight into what we were thinking, and how things worked.

 

…and to those that were specifically part of the mass Experiment Uptime-Monitoring Effort and Legacy Support Team…

thanksText.png
 

P.S.

Keep an eye out for the mass OST+ VHS release! I haven’t forgotten about it - I’m still slowly chipping away at it, I promise!

 
sig.png