Conway's Game of Life

Conway's Game of Life written to a Fragment shader. Surrounding application written in Typescript with the Next.js framework, XState, and three.js.

  • TypeScript logoTypeScript
  • Next.js logoNext.js
  • React logoReact
  • Vercel logoVercel
  • WebGL logoWebGL
The initial state of the board has the pixels arranged to say "Conway's Game of Life" in 3 rows. The user can enter in any text they want in the form above and it will be rendered as a Game of Life board.
Initial state for Game of Life

Purpose

Conway's Game of Life is a great exercise to learn about basic techniques for rendering graphics such as double buffering. However, the goal of this version was to implement the Game of Life while exploring more advanced techniques. I originally chose to implement the game using finite state machines with XState but expanded the scope to include writing the game to a fragment shader.

Features

Three.js and GLSL used to write the game in a fragment shader. All of the state for the game is actually stored on textures. The GPU can parallelize shader calculations and provide a smooth animation even at high resolutions.

Rather than initialize the game board with randomized squares or classic shapes, user entered text can be rendered.

Knowledge Gained

Rules of the Game

For each colored pixel on the canvas, sum its eight neighbors.

  • If the pixel is alive and does not have exactly 2 or 3 neighbors, then it dies.
  • If the pixel is dead and has exactly 3 neighbors, then it comes to life.

This simple rule set allows for pixels or squares to animate on their own. There are a vast number of patterns that can appear.

Three.js and Shaders

Prior to this project, I had only known shaders in relation to video games and graphics cards marketing material. The main thing I've learned is that there is an endless amount to learn about graphics programming and it's amazing to see what other people come up with. There was a lot of time spent on Shadertoy checking out the crazy things programmers come up with.

As much as I love functional programming and a higher level of abstraction that languages like TypeScript deliver, it is clear that there huge performance gains to be made by dropping down to a lower level. Writing a shader in GLSL is not easy but it feels like a miracle to watch the shader come to life when it works.

State Machines and XState

The original idea for this project was to use XState for the actual game implementation. The rules are perfect for implementation with a state machine. As soon as I finished it, I was unhappy with the number of generations I was getting each second. I wanted something that was fast and smooth but I wasn't getting that with what I had implemented. I ended up going as far as I could for performance without changing the actual algorithm, but I still retained XState for managing the state in the rest of my application.

I really like how stable and predictable my application was with using XState and I'll continue using it when I can in the future. The ability to generate a state chart based on my application was really useful for understanding how everything fit together.

Challenges

In Over My Head

The features I had planned for the project initially were not really doable with a shader and I should have re-evaluated the scope of the project at that point. It was frustrating to feel like I was hitting a new wall each time I wanted to do something additional. Originally, I wanted more interactivity with the game. For example, manually adding gliders or other patterns manually.

I managed to make it so pixels could be turned on and off one-by-one with the mouse but I should have re-scoped the project to remove that kind of interactivity once I moved onto the shader idea.

What Could Have Been Different?

Canvas

Just using the canvas would have been the easiest to implement if I were looking for good performance without advanced game algorithms. It was a great experience learning something so far outside of what I had done before.

Next.js

I used Next.js as a way to bootstrap a React project because it's quick but none of its features were used on this project. It added little hiccups to development with no benefit. I love using Next.js but there wasn't any reason for it here. On that note...

React

This should have been done with vanilla TypeScript. All of the logic for the shader was dumped into a useEffect with a ref to a DOM element. Not a good use of React at all.

Screenshots

The initial game state can be rendered with custom text. The initial state affects all future generations so it's a great way to seed the game.
Game of Life initialized with "Draw Whatever You Like"
The game speed and resolution can be set with sliders below the board. This is a sample of the game at high speed and high resolution.
Game state at 281 generations with high resolution
The pixels that will be "dead" in the next generation have a less saturated shade of blue. This gives the game a more organic looking animation.
Low resolution game board