Skip to content

Post Mortem

Franck Ly edited this page Sep 28, 2020 · 2 revisions

Post-Mortem

Forgetful God is a game prototype created by Franck Ly (Twitter / LinkedIn) for JS13kGames. (Source code)

MIT License, Franck Ly, 2020.

🧠 The journey - Brainstorming with my wife

When the jam started, I initially wondered what type of game I would want to make this year.

I looked into previous entries to see what they were and then started to bounce ideas off my wife!

We based our ideas off 404 meaning "Not found / Missing" and brainstormed a few themes:

  • Missing ingredient (Cooking game where you have to find the missing ingredient)
  • Missing person (Where's Waldo? But with Emoji)
  • Game level designer (With a list of cell type, you need to choose the correct one so that the AI can finish the level)
  • Game level designer with camera play (Same as above, but with a free form camera to see all angles in a 3D scene)

At that point, I knew what kind of game I wanted to build: a somewhat 3D game with missing assets and some camera logic.

I remembered in 2017, a game called LOSSST made by xem had very appealing graphics! I decided to check out how CSS3D Framework works and would utilize it for my project. I also saw that xem had a similar idea with a train and already made a lot of progress there.

So back to brainstorming... A few days later, while working on a soundtrack, I decided to go with a Sokoban type of puzzle game, using geometry and color as the missing assets. The player will need to create these assets by mixing existing ones together.

This is how the concept of Forgetful God came to life!

⚙️ Content

📝 Minify your project (Script)

In 2017, one of my biggest mistakes was being overconfident about minifying my project. Turns out, it was not an easy task! So I spent a little more time on this.

This year, I started to create the script at the beginning since I wanted to be done with it for the rest of the jam. I went through past entries to see if anyone was building / minifying the project on Windows and then adjusted to make it fit my project.

I stumbled upon xx142-b2.exe - they had a nice little script with some stats about how much it reduced the size of the project. I liked the script and adjusted it for my project, et voila! Script part done.

🔊 How does this sound? (Music / Sound effect)

In 2017, the other biggest mistake was to leave almost no room for the sound... This year, I started off with creating background music that is catchy and fun!

I spent a good week on Youtube learning how people are doing it. Basically, most of the videos were doing the same first steps: a recurrent beat, a build up, repeat the rhythm, and build back down.

I looked at js13k resources and found ZzFX: a nice and tiny library that can handle my sound effect... A few hours later, I stumbled upon ZzFXM (made by Keith Clark), which can handle music!

Frank Force also created this simple sequencer that turned out to be good enough for my needs!

After playing with the sequencer for a while, I realized I wanted to create my own musical tool after the jam to ease the process of creating background music!

Make me pretty (Assets)

Knowing how well LOSSST was able to pull off such appealing graphics, my goal this year was (fake) 3D. I wanted to see how well I could get a 3D-feel game with some kind of physics and appealing graphics, thanks to CSS3D Framework.

I am also not a great artist and wanted to rely on something well-designed.

During the brainstorming, I wanted to use emojis to create the Missing Person (Where's Waldo?) game... so I adapted it to this new game concept: "what if there is an emoji suitable to be my main character?" I added twemoji to my project and told myself "I'll figure that out later!"

📓 Code

Organization of the code was important to me. I wanted to maintain some sort of separation between features to make it easier for others to navigate through the code.

Implementation

The source code for the game itself can be found under src and the game level editor can be found under editor.

Details of each file from src:

  • Constant value in constants
  • The different cell type mapping in cell (although they are constants, I wanted them to be separated for the editor)
  • Console screen in console_screen (Mainly for text screen when not in game)
  • Text constant in console (Mainly text that the console will use / although it's constant I separated it because it's just a chunk of text)
  • Main game loop in main (handle key inputs / sound)
  • Game rules (mixing asset / colors) in game_rules
  • Player logic with the scene in game_screen (collision logic + map interaction logic, and triggers)
  • Map logic map (map initialization + asset collision handler (merge of asset logic basically))
  • Levels maps (contains all the different levels and initialization of a level)
  • Monetization features monetization (using color accessibility from accessibility)
  • Sound definition + Handler sound (music / sound effect)
  • All other helpers function utils (cache loading/saving, darkening/lightening colors for the assets faces, mobile checking logic, DOM creation, sound icon)
  • Library in their own file zzfx and css3d (applied minor modifications to adapt it to this project)

Editor was fairly simple (and I didn't want to spend too much time on the game level editor) so everything is squished in a single file! (index.html)

I will detail Player and Map logic because they are the core of this game.

🕹️ Player logic

It's very simple: the player is allowed to move only if the new location is a free cell OR if the asset on that cell is movable.

Otherwise, the player would just rotate to face the direction the player is trying to go.

So in pseudo-code:

Wait for user input
Get new cell position based on user input (and update direction where the character is facing)
Check if new cell is a valid position (free cell, exit, or check if map allows the asset to be moved and is not a wall or a hole)
  if new cell is a valid position: update player position
  else: *do not* update player position
Update player direction to the new direction (even if the cell wasn't a valid position)

Source Code

🛠️ Map initialization

The level of the game is represented by a one-dimensional array that I convert to a two-dimensional matrix.

The algorithm to do that is very simple. First, we need to get the map height and width. Since we are building a square-based map then width = height (let's call this size), we can get the size of a map by doing the square root of the array length.

Now that we have the size of our map, we can compute the 2D coordinate for any index of the array by doing, (we will call i as index of the array and s as size of the map):

[x, y] = [i % s, Math.floor(i / s)]

Since we know how to compute coordinates, we then need to associate the value of the cell to a type, which is done here.

Lastly, we need to distinguish missing assets from regular moveable assets, and what color each asset is. To achieve this goal, we will encode this information into the cell type (suffix the cell type with a d if it's a missing asset, and hyphen separate the color from the cell type). The final cell value looks like: {cell_type}d?(-{color})?, an example of that would be 2d-#aa3f09 or 2-#aa3f09 or just 0.

Once all of this is defined, we can now fully parse the map and render it. You can find the code here.

Although this is a nice loop to build the level, I had some performance issues where I would have too many DOMs rendered.

In order to fix that, I merged continuous walls together, which means I would have a single DOM to build a continuous wall (column- or row-wise). For that, I used a DFS algorithm to check whether the long side is on the x axis or y axis; from there, I could mark the cell as visited and go on to the next coordinate. Checkout the source here.

(11 vs 3 assets to build a wall for this simple map)

🗺️ Map Logic

Slightly more complex than the player logic, the map will ensure that assets are being pushed or will prevent the player from moving to the location.

The reasons why an asset would not be able to move are:

  • Asset is on the edge of the map (can't move outside of the map; this should never happen since I'm wrapping the level into wall)
  • Direction where the asset needs to be pushed is directly a wall (no free cell in between)
  • Missing asset and pushed asset aren't matching (in this case the missing asset act like a wall)

If you push the asset in any direction, the below happens:

  • If there is at least one free cell between the asset and a wall, the asset gets moved to the last free cell before the wall
  • Asset gets destroyed if pushed into a hole
  • If there is another asset, both of them merge (at the new position)
  • If missing asset and pushed asset are matching, both disappear

Pseudo code:

Wait for user input
Get moved to cell (new position [NP])
If NP is not empty:
  If wall, hole: prevent movement
  Else if asset:
    ToCell = NP (cell to be moved onto)
    For each cell where the asset will be pushed:
      If cell is free:
        ToCell = cell
        continue
      If cell is a wall:
        break
      Else if cell is a "missing asset" and "missing asset" != "asset":
        break
      Else if cell is hole:
        destroy(asset)
        break
      Else if cell is asset:
        ToCell = cell
        run(mergeAsset)
        break
  if ToCell == NP:
    return prevent player movement
  else:
    return update(cell render)

Source Code

🤖 Unit Testing

Instead of creating a level to test new additions (it takes time to test each combination), I went with creating unit tests to test out the game rules.

Although the rules are fairly simple (mixing colors / merging geometries), I wanted to make sure that everything was still working as expected after adding a new cell type, using a new mixing algorithm.

Since asset and player movement are the core gameplay, in retrospect, I should have added some unit tests around these.

🚧 Obstacles (Bugs / Issues) to overcome 🚧

Player direction

Static degrees (90/180/-90/0) don't really work all the time... It wasn't obvious when implemented, but was pretty intuitive when I saw the animation.

To fix* that, I am computing the new direction based on the current direction; the character animation looks smoother and more logical this way.

*Note: The fix introduced a bug (accepted as easter egg) - watch my little guy spinning!

Merging asset bug

When I was merging assets, we couldn't see any movement because they were disappearing before they could move. I had to set a timer to let them move before they actually disappeared.

I think if I used requestAnimationFrame() instead of updating render inside my game loop, it would have been handled in a better way (Youtube video explaining how requestAnimationFrame() works). But I realized that after submitting my project. :(

Merging geometry bug

Although merging geometry was simple, adding new geometry and making sure it was mergeable with the rest wasn't as straightforward (I often forgot one or more combination(s))... and when it happened, it resulted in an invisible asset!

Instead of spending hours fixing these invisible assets, I decided to write unit tests to make sure everything was covered!

Mixing color bugs (/ feature?)

There are two ways to mix colors together:

  • Additive: Add R + G + B = White
  • Subtractive: Add R + G + B = Black

Starting with additive color, it seemed pretty straightforward but mathematically, it wasn't - it was way harder to predict a result this way than with real life painting mixture. There were also fewer combination amounts compared to mixing paint in real life (for example, we could end up with white very easily).

So I fixed color mixing by using subtractive colors, or acrylic painting logic (real life logic) to mix colors. This allowed me to get more color variety and a much easier way to predict the mixture of the color. If the player enables color accessibility in the game, they will realize that, mathematically, it's really easy to get there.

Performance issue

There were three different issues:

  • Too much DOM: It seems like more than ~200 DOMs is too much for Chrome to handle - so removing the non-visible side of an asset will reduce the amount of DOM to render!
  • Asset overlap: It seems like Chrome doesn't like to render multiple asset at the same place (reducing asset size to avoid wall overlap helped)
  • Lastly, scaling: Your asset shouldn't be huge then scaled back to fit the scene... if it's possible, it should just be scaled directly to fit the scene!

I also think if I used requestAnimationFrame() instead of updating my render from the game loop, it would have been better!

🍎 Apple against Web Norm

First issue is that twemoji font wouldn't load properly. CSS font-load are handled differently than on webkit based browser. At some points, I made it work on Chrome but then Safari refused to take it... and later, I did the reverse (Safari accepted but Chrome refused), and ultimately decided to just fall back on the default Apple font.

The next issue is that CSS layers are handled differently. It wouldn't want to display any of my assets at the beginning. After playing awhile with the z-axis (for the different asset) and some z-index (for the modal and overlay), I was finally able to see them on the browser!

I thought that after fighting with all these graphics issues, I overcame everything... but when I started to play on Safari, I realized that javascript events are also different for Apple browsers.

First off, onkeypress doesn't seem to work for some keys, so I used onkeydown. Then, it seems like the event loop doesn't trigger at the same time. When comparing gameplay on Windows versus Mac, sometimes Mac renders a change one tick later, but since ticks usually happen very quickly, it is unnoticeable.

Since these issues are not that harmful, I didn't want to spend time fixing them so... I left them in the final build.

To sum up Apple doesn't like to follow the same convention as webkit based browser. That means in some cases, the CSS won't work or some javascript event wouldn't be triggered as expected.

This also means that my game will look and feel slightly different on Apple, but it still works perfectly!

In retrospective?

🌟 What went well

I have a fully developed game prototype with 13 levels and a lot of features! Including sounds and assets! Plus, the background music is pretty catchy! 🎶🎵🎶

Outside of working hours since I have a full-time job, I only took 13 days (what a funny coincidence!) of coding to make this game! And it is running pretty smoothly (adapted my 13 levels around my DOMs constraint)!

The game is available on desktop, mobile, Mac, and Windows.

What went wrong

There are still some performance issues - I should probably move to requestAnimationFrame() in the future to minimize those.

I spent more than a week on brainstorming! Maybe sketching on paper would help gather my ideas faster next time!

I spent a week creating music and sound effects for my game.

I left some bugs in the final build by rushing the last feature. I should probably leave a day of code freeze for bug fixing and fully test all the different features.

I didn't spend enough time testing the different browsers and platforms (mobile / Mac / Windows) while I was developing the game, so I ended up spending a whole day debugging to make sure everything is working properly.

🎓 What did I learn

CSS3D Framework is nice to play with if there are not too many assets on the screen (or if I can merge them together properly).

Emojis makes awesome assets! I hope twemoji will have a solution for Apple based browser so that game stay the same on every platform!

Creating sound is HARD, but I made it through. I feel like if I make another soundtrack, it wouldn't be that bad next time... 🙉 I will also probably make a tool to create music soon!

There is a limit in terms of how many DOMs the browser can support without suffering from performance issues. Plus, DOMs overlap can cause performance issues.

Separate game logic and render logic will help the game be smoother (graphically)! So I will be using requestAnimationFrame() to update my render for my future projects!

Unit tests on features are very important. They help reduce regression bugs and give more time to test new features instead of speedrunning through every level.

📅 Timeline + Screenshots

  • Day 1 - 10 (8/14 - 8/23)

    Brainstorming (seems like a lot of time, but I didn't have many ideas and I wasn't sketching anything this year)

  • Day 11 - 18 (8/24 - 8/31)

    Adjusting minified script (not this year, minified script, not this year!)

    Music (game concept is done, so we can work on the music right away and put this aside for the rest of the jam)

    • Sound effect (move, push, destroy)
    • Music (background sound, winning sound)

    Testing CSS3D Framework (testing the framework and see the limits)

    • Floor & Walls rendering
    • Camera movement
    • Player movement

    Testing twemoji (pretty straightforward - it worked right away... on Windows browsers at least...)

  • Day 19 - 22 (9/1 - 9/4)

    First prototype of the project (performance test - seems laggy, too much DOMs => need smaller maps)

  • Day 23-24 (9/5 - 9/6)

    Adding game loop + rules (merging asset + level win)

    Adding level editor + creating tutorial level

    Adding new geometry (Reduced performance issue but still existing)

  • Day 25-26 (9/7 - 9/8):

    Adding FOG (makes it harder for non-monetized user) Removing FOG for premium users (quality of game!)

    Adding Colors + Mixing Colors Logic + Unit test (mixing assets wasn't as intuitive as expected!) Mixing Color V1: colors are additives (adding all the colors results in white)

    Adding mobile controls

  • Day 27 (9/9)

    Update mobile controls (Too many buttons!)

    Adding new cell type: hole (throw extra assets in there if they annoy you!)

    Update wall asset (+ Final Fix of the performance issue)

  • Day 28 (9/10)

    Adding tips to help user understand how to merge geometry

    Adding monetization feature (cheatsheet)

  • Day 29 (9/11)

    Updated editor:

    • Map loading
    • Use cell type from the game instead of rebuilding the mapping

    Adding a few more levels

    Mixing colors v2: Colors are subtractive, not additive (adding all colors results in black, like mixing paint colors) Updated unit tests on colors mixing logic

    Respond to early feedback:

    • Add mobile button on desktop (to remind player of the different buttons)
  • Day 30 (9/12)

    Snuck in a last feature to solve color blindness

    Bug fixings:

    • Color accessibility doesn't disappear
    • Monetization doesn't start at the beginning as expected
    • Apple's browsers fix (Safari / Chrome / Firefox on iOS / mac laptop)
  • Day 31 (9/13 00:00 EST)

    Project submitted to JS13kGames!

  • Day 31 (9/13 07:00 EST)

    Entry accepted!

🙏 Special Thanks

Credits

Sources
Beta-tester (before submission)

Packages

  • archiver - Compress the project in a zip file
  • cheerio - Helper to inject Javascript and CSS code to index.html pre-minification (to avoid having to load them externally)
  • csso - CSS optimizer & minifier
  • eslint - Coding style helper
  • eslint-config-prettier - Coding style helper
  • html-minifier - HTML minifier
  • mkdirp - Create output directory
  • mocha - Unit testing Framework
  • prettier - Coding style helper
  • prettysize - To make the summary human-friendly (instead of big numbers)
  • terser - Javascript minifier