Game of life ∈ artus9033 (original) (raw)
The game presents a PoC that was fun to make, utilizing state-of-the-art technologies available. The renderer used is WebGL, with
abstraction on top of it. As the website is written in React, the game scene is built up declaratively in JSX using
and finally, it utilizes
to dynamically synthesize polyphonic sound based on the quantity of alive cells in each sector of the map. All components displaying equations on this page use react-latex & katex for rendering LaTeX code.
The logic of the game is classical
rules. To optimize performance, the logic was implemented in Rust that is compiled to WASM with
. The renderer, as described, is a React component utilizing
using the
React abstraction. Moreover, all cells are just
THREE.instancedMeshes so as to be performant enough. For a slightly more artsy experience, I also created a ‘vanishing’ effect for cells that were alive the round before, but are dead now - for just one render, they are semi-transparent (or, more precisely, ‘more transparent’ than cells being currently alive), making a pleasant animation effect.
The arena (map) is generated dynamically by dividing it into a grid and placing items picked from a set of pre-defined structures (e.g.
,
,
,
,
) that have a pre-set probability weight index using
rand::distributions::WeightedIndex
that differentiates the probability of picking a specific structure. The templates for these structures are defined as 2D matrices and cached to be generated just once with the
wrapper.
This implementation features a controlled frameloop that executes both the game logic and rendering sequentially at a given rate specified in the floating game settings. The game logic is implemented in
and uses a reference to the
THREE.InstancedMesh
that is bound with
Map::bind_js_cells_instanced_mesh
to a
.
Before the game loop starts, the WASM module is initialized, bound to the JS objects (THREE.InstancedMesh
instance and cell colors for cell state lookup table object), the
THREE.InstancedMesh
is populated with a proper quantity of instances, for each of them a static transform is set to appropriately position them on the map, and
THREE.InstancedMesh.setColorAt(0, ...)
is called to initialize the
THREE.InstancedMesh.instanceColor.needsUpdate
property (as THREE.js requires it to be done).
Each game round, the Rust/WASM game logic:
- Mutates the map. For a nicer look, apart from Cell::Dead and Cell::Alive, there exist 3 levels of vanishing: Cell::Vanishing{1,2,3}. Each of these
Vanishingtypes, however, is treated just as Cell::Dead in game logic. - Sets proper colors in the
@react-three/fiberJS-initialized THREE.InstancedMesh instance by calling its setColorAt method via the bound object ref directly from WASM code using js_sys::Function::call2 (to be specific: also withwasm-bindgenautogenerated code glue). - Informs
THREE.jsthat the scene needs to be re-rendered by setting THREE.InstancedMesh.instanceColor.needsUpdate to true directly from WASM code using js_sys::Reflect::set. The render will be performed right after the game loop updates, since Map::morph_map_next_round is called synchronously from useFrame (which is in fact the abstraction's interface for requestAnimationFrame).
A special feature of this implementation is polyphonic sound generated in a specific manner. The map is divided into cells of an arbitrary-sized grid (for the purpose here, I chose a
3×33 \times 3
grid). Each cell of such a grid contains part of the game map, therefore a checksum of its cells can be calculated - for simplicity, I decided to simply take the sum of cells that are alive.
That sum is afterwards turned into a cyclic index used for picking a note from the
, utilizing it's even parity. For an index - the sum of alive cells in a ‘sound grid’ cell
SijS_{ij}
- with a cell of size
x×yx \times y - being:
∣∣Sij∣∣=∑p=i⋅x(i+1)⋅x∑q=j⋅y(j+1)⋅yMpq||S_{ij}|| = \sum_{p = i \cdot x}^{(i + 1) \cdot x}\sum_{q=j \cdot y}^{(j + 1) \cdot y} M_{pq}
where
MM is the matrix containing the map, such that Mpq={0if cell (p,q) is dead1if cell (p,q) is aliveM_{pq} = \begin{cases} 0 & \text{if cell }(p, q)\text{ is dead} \\ 1 & \text{if cell }(p, q)\text{ is alive} \end{cases}, is a cyclic key to index a tones array. The tones array is chosen based on the key's even parity: either it is the minor or major subset TT from the circle of fifths: T[(∣∣Sij∣∣mod ∣∣T∣∣)+∣∣T∣∣]mod ∣∣T∣∣T_{[(||S_{ij}|| \mod ||T||) + ||T||] \mod ||T||}. Such tones for all the sound grid cells are then played simultaneously on the polyphonic synthesizer provided by Tone.js, uniformly distributed over the time foreseen for a single audio loop (a value of
23\frac{2}{3}s), with an additional oscillator to modulate signal frequency and an
. Additionally, a slight
is applied afterwards so as to make the transition between synthesized sounds smoother. The computed notes are visualized in the table above, marked with
colors associated with them.
Consecutive tones are mixed one-after-another using a polyphonic mixer, and are synthesized using either of voice synthesizers:
or
, which is configurable and can be picked in the floating settings menu.