We’re in strange times right now and as a way to help people a good friend and colleague created the Alone Together Jam to give folks a creative outlet. The jam would run for the entire month of April to let folks work at a relaxed pace. As a prompt the jam provided three words: Horizon, Twain and Ambedo. Fortunately they also included links to the words … ambedo was both a completely new one for me and also the cornerstone of my idea.
So what did I make and what am I going to talk about? I made an MMO … an MMO Screensaver. The game, Through my window, doesn’t have any tasks for the player to complete; there isn’t anywhere for them to go or to be; there’s just a street on a rainy night and a window to watch it through. And as for what I’ll be talking about? I’m going to talk through how I came up with the idea and how I designed and built the key systems.
Developing an idea
Thinking of ideas is never the hard part. The hard part is picking one idea and developing it into an interesting concept that is achievable within the time constraints. The prompt words helped a lot with idea generation particularly ‘ambedo’ and in particular this part of the definition “A kind of melancholic trance in which you become completely absorbed in vivid sensory details—raindrops skittering down a window ...”
That description of watching rain drops on the window immediately conjured up for me a memory of one of my favourite things about where I live. My main windows look out onto an intersection. At night, and particularly on a rainy night, it’s incredibly peaceful to lookout and just watch the street through the rain on the window. The drops slowly rolling down the window making all the lights flare when they intersect. With that I had the base of my idea. I knew it would be at night, it would be raining and a core aspect would be looking out the window. What else would you do though?
I didn’t want the player to leave the apartment and I wanted to keep their eyes largely fixed on the window so whatever they did had to be through that window. I also wanted there to be an interaction with other people. Which gave rise to the idea of maybe … maybe … I could have multiple people be in this world at the same time, each in their own apartment, isolated but also connected. I was familiar enough with databases that I understood how to do that from a technical point of view. And I ran the numbers to work out the worst case bandwidth usage. It all fit. I worked out I could have a shared world with around 200 people in it. I had the core of my idea, a shared world where every player was focussed on their own window onto a rainy street at night. The next step was to make sure the player had a rainy window worthy of their attention.
Making a rainy window
I knew that I wanted a window with rain running down it and I knew I would need a shader to do it. I also knew that while I can make basic shaders and I knew that given time I can make ones with some complexity I also knew that this was many many levels above anything I had tried to make before. I started some research on it and was very fortunate early on to find these video tutorials from The Art of Code.
The effect was exactly what I was wanting and the tutorials were excellent. The techniques and solutions used in the tutorials were not ones I could have come to on my own. The gaps in my shader knowledge were simply too large. But after the tutorials it all made sense. I can understand what the code is doing and why. For me that’s the mark of a great tutorial. I learned how to do something I had never done before and I understand how the pieces fit together and how I might use them in future.
I had my rainy window, the next step was to work out what the player could and would do.
Keeping the interactions simple
As I started detailing out the rest of the game I was very adamant about keeping the interactions simple. Trying to create a game with a shared world for 200 people would be ambitious enough at the best of times. In the midst of a pandemic, while still working full time, and while trying to make sure the trimester closed out smoothly for my students and my team …. I needed to keep them very very simple. Where possible I like to put myself in the player avatar’s shoes to work out what they would do.
If it’s a rainy night how would I try and communicate with someone in the building opposite me that I didn’t know. I can’t call, text or email them. I can’t hold up a sign, no one could read it through the rain. And yelling would be rude. I could turn my lights off and on though. And maybe, just maybe, if both of us knew Morse Code (I don’t) or could at least recognise and find a guide (that I could do) then we could communicate. I had my player verb … my only player verb: toggle lights.
I also decided to only allow the player to look around (ie. no movement). I wanted to keep people focused on the window so I anchored them in that spot. I also removed all of the furniture apart from a single rug. Looking back into an almost entirely empty apartment would look quite strange though. I did consider restricting the camera angles but unless I heavily restricted them the player would always be able to see some of the apartment. Instead what I did was setup a custom shader to darken the environment behind the player.
The way the shader works is:
- The game passes the player’s location and the forward vector (Vforward) of their spawn point (that way it’s consistent) to the shader.
- For every vertex the shader calculates a vector from the player to the vertex Vp2v
- The shader calculates the dot product of the vector from the player with the forward vector Projection = Vforward • Vp2v
- The first thing done with the Projection is to flip the sign of it (so positive values were behind the player and negative in front).
- The shader then used two distance thresholds: Start and End and converted the Projection into a percentage between them (with the values clamped into a 0 to 1 range).
- That percentage was then used to scale the normal map strength (from full strength at the start to zero at the end).
- It was also used to scale the colour from the texture lookup (from full colour at the start to black at the end).
The end result was the apartment behind the player is fully hidden and also looks a bit ominous in contrast to the more inviting view out the window. I now had a player that was locked in position, could look around, toggle the lights and their apartment being empty was largely hidden. Now I just needed a world that felt alive.
Creating a synchronised world
If player’s were going to be spending the majority of their time looking out of the window then there needed to be a reason to. Sure, there might be other players but there was no guarantee of numbers and folks might be playing in offline mode. I needed there to be things happening regardless. I also wanted to try and achieve a level of the shared experience even if you were in offline mode. That meant I needed a way to synchronise, as best as possible, everyone playing the game at the same point in time … without the need for any communications.
Using time, in particular UTC time, was a logical starting point. It would, assuming folks had the correct time and timezone setting, mean that multiple people scattered around the world would have the same UTC time. So I had a way to synchronise everyone regardless of where they were or what mode they were playing in. I still wanted there to be variation though. I didn’t want the same thing to always happen at the same time. If you ran the game for an entire week I wanted the world to be changing throughout that time. You might see the same event multiple times but not in the same way. So I still needed randomness, it just needed to be synchronised.
The solution was to use different components of the UTC time to seed the random number generator … and … to very carefully control when segments of the code were allowed to use randomness. How this unfolded from an implementation point of view was each variant of an event has:
- A rigid set of day, hour and minute changes in which it can occur (so that I can permit or inhibit overlapping events).
- A mix of components of UTC time (day in week and/or hour) as well as additional custom values that are combined to seed the random number generator.
- A setup pass that when activated seeds the random number generator and performs all random rolls for the event in one go so that I can rely on consistency with the random rolls.
That setup worked out well, the events would activate consistently and the setup made it straightforward to configure a range of ones including:
- Thunder from different locations (no visible forks of lightning but you do get the preceding flash of light).
- Cars driving by (different vehicles and tracks).
- The procedurally controlled buildings will put on a light show occasionally showing an image or text.
- The lights in the procedurally controlled buildings can also change every hour on the hour.
- And one final event inspired by someone in my building trying to organise everyone in the building to sing YMCA from their balconies at the same time (it did not get high participation but folks walking by seemed to love it).
There were enough events that every few minutes you should see or hear something and you may get overlapping ones as well. I also wanted the intensity of the rain and wind to vary with time but very smoothly. That was one of the easier problems to solve due to having done similar things in the past. I created an intensity parameter and fed that through to the audio engine (WWise) and set it up to blend between different looping audio based on that parameter. Now I just needed to generate my intensity value (from 0 to 100).
For that I combined a couple of sine waves based around the UTC seconds in the day (including milliseconds). The first (long period) wave would complete a full cycle over a 24 hour period. I also wanted some short term variation so I layered on a second (90 minute period, low amplitude) wave to modulate the value of the first one. The end result was an intensity parameter that varied continually throughout a 24 hour period with local variations in intensity throughout the day. With all of the pieces in the world finally felt the right level of ‘alive’. Things weren’t happening rapidly but they were happening consistently and regularly enough that a few minutes of watching would result in you seeing an event. Now the ridiculous-for-a-game-jam part … adding multiplayer.
Making sure multiplayer was feasible
Having multiple people be able to inhabit the same world was one of the design goals I had for Through my window from the very beginning. My previous experience with the multiplayer side of games is very limited. I’ve worked on projects that had multiplayer but I’ve never worked on the networking aspects. What I have done previously is have some of my games communicate with a server to store and retrieve statistics in one case and in another to store and retrieve messages.
For Through my window I felt that I could get away with a similar process so I ran the maths on what the data transfer would be to see if it was feasible:
- First the amount of data:
- There were 200 apartments and for each apartment the state was a single boolean (the light). That meant I needed 200 bits (ie. 25 bytes) to convey the entire state.
- If I went for a simple (but less efficient) method of encoding that data I could transmit it as a hexadecimal string. So my 25 bytes now becomes 50.
- As a bit of future proofing I also wanted to add in a version number and some padding taking the total number of bytes for the world state to 54 (worst case).
- Next was looking at the data rate:
- I wanted any changes from a user to be visible within a few seconds. I decided to go for a conservative limit of 5s.
- That meant the state would be sent 12 times in a minute or 720 times per hour
- Combining the information meant (assuming worst case of continuous use) I’d be sending roughly 39 KB an hour per user.
- Hosts typically give bandwidth limits in per month so looking over the course of a month:
- Assuming worst case of continuous use I’d be transferring 28 MB per user in a month for sending just the world state.
- The other communications from the user to and from the server are small and low rate so will be much less than the 28 MB.
Assuming I had 200 users continuously for the month (nice if it happened but unlikely) I’d be looking at 5-6 GB per month which was well within the bandwidth permitted. So bandwidth wise even with a simple encoding the bandwidth requires for myself, and even for end users, were very manageable. To the extent that if needed I could ramp the refresh rate up to every second and still remain comfortably within bandwidth limits. The maths checked out, now came the (genuinely) fun part … making it work.
Stored procedures as far as the eye can see
Seeing how the maths worked out gave me a lot of confidence that I could proceed using a similar setup to previous solutions: a MySQL database with a PHP script as the public interface. Both were tools I was familiar with and I had a reasonable understanding for how I wanted to approach it. There were only a limited number of operations that I needed to do:
- Request a lease
- Retrieve the state of all lights
- Set the state of my light
- Renew my current lease
Retrieving or setting the light state and renewing a lease were straightforward. They were simple operations (SELECT/UPDATE) and would use a server issued unique ID when requesting the operations. Requesting the least was the trickier one. The operation needed to be atomic (ie. so it can’t lease the same apartment to multiple people); it needed to pick a random apartment; and it needed to provide back the details of the selected apartment. All of which were a bit more complex than things I’d previously worked with.
Fortunately, I managed to find some helpful resources that let me put together a single SQL statement which would pick a random apartment (by ordering by RAND() and limiting to 1 result). The same statement let me retrieve the apartment info (by using a nested SELECT statement into a variable). I wrapped all of that up into a stored procedure giving me a single function that would find a random apartment, lease it, turn on the lights and pass back all of the information. The end result looked like this:
# find a random apartment in the first free realm # - also sets the lease GUID # - also turns on the lights # - apartment expiry set to 2 minutes from now # - also retrieves the apartment ID and realm ID UPDATE `world` SET lease_expiry_time=DATE_ADD(UTC_TIMESTAMP(), INTERVAL 120 SECOND), light_state=1, lease_guid=@lease_unique_id, apartment_id = (SELECT @leased_apartment := apartment_id), realm_id = (SELECT @leased_realm := realm_id) WHERE lease_guid IS NULL ORDER BY realm_id, RAND() LIMIT 1; # populate the apartment ID, lease GUID and realm ID output variables SELECT @leased_apartment INTO leased_apartment; SELECT @lease_unique_id INTO lease_unique_id; SELECT @leased_realm INTO leased_realm; # populate the building name output variables SELECT b1_name, b2_name INTO building1Name, building2Name FROM `realms` WHERE realm_id=@leased_realm;
One thing you’ll notice in the code above is the mention of realms. Although it was unlikely I had to consider the possibility of if the game took off and more than 200 people tried to play it at the same time. I had designed the game side to quietly fall back to offline mode so worst case that would work. However, I wanted to go all in on this and decided to setup multiple realms.
I was surprised at how little needed to be changed when adding in realm support.
- On the leasing side it sorts first by realm index and then by the random value to avoid players being fragmented across the realms.
- No changes were needed for setting lights as those already work on the unique ID issued for the lease.
- Retrieving the lights did need to change.
- At first it was sending through every light … for every realm and was making the game very unhappy (plus sending a lot of unnecessary data).
- That was easily fixed by the light request command sending through the player’s realm ID which returned the data transfer to the normal levels.
- Creation of the realms was handled by stored procedure so that if I needed to add or remove a realm it was doable in seconds with a single command.
The server side was now fully up and running and able to scale up if the demand was there. Not only that but I had a really solid set of infrastructure worked out that I could use for future games.
The Alone Together Jam for May is running now and I had considered joining in again. Once again the prompt words evoked a lot of interesting ideas. I needed to take a break and finish some other projects though. Which is fortunate because my idea was going to be a VR art gallery …. that was also massively multiplayer. It’s probably for the best that I’m not doing that :)
I do plan to re-use and expand the infrastructure that I’ve put together. At the moment it is very tailored to the game. I’d like to redesign it so that it can be used in a more generic way and so that it is easier to add commands. The end goal being an infrastructure where from the Unity side it is no different to calling a function and supplying a delegate for handling the response. That might need to wait till the next MMO I make :)
If you want to check out Through my window it's available here.