Masai Mahapa

30 days of Rust - Day Thirty - Game Of Life - continued

Day 30 of Rust The final day of the 30 day challenge. It's really amazing to see how quickly time goes by. There's a saying my late brother Lesiba always said to me.

Do or you don't, time will pass. The question is, 'What will you have achieved at the end of that time frame?'

So for me, I am certain that I will now be able to take on more projects that require Rust such as Blockchain development.

If you just got here, please check out my previous posts, from where I started building the Game of Life application from day 29.

Day 30 - Interactive Game Of Life

So from the previous post, the game of life already works well according to the rules. Now I have decided to improve 4 aspects of it. Namely;

  • Pause & Play Functionality
  • Control Play Speed
  • Randomize spawning of Alive Cells
  • Control the size of the Universe

The product of these changes now looks like this; game of life

Let's dive into the changes made.

Pause & Play

In order for the game to be able to pause and play at any given time, I added a button to toggle the state.

<button id="play-pause">Play</button>

Previously, the render_loop was called directly. Now I added a play function to call it. The requestAnimationFrame function returns an integer which identifies the callback. We can use this to cancel the animation later with the cancelAnimationFrame function. We can set animationId to null initially, and to the value returned by the animation inside the render_loop.

const isPaused = () => {
   return animationId === null;
}
 
const play = () => {
   playPauseButton.textContent = "⏸";
   renderLoop();
}
 
const pause = () => {
   playPauseButton.textContent = "▶";
   cancelAnimationFrame(animationId);
   animationId = null;
   userPressed = false;
}
 
let userPressed = false;
 
playPauseButton.addEventListener("click", event =>{
   userPressed = true;
   if(isPaused()){ 
       play();
   } else {
       pause();
   }
});

I then updated the render_loop method to only run if the user initiated the play by clicking the play button.

const renderLoop = () => {
   if (!userPressed){
       return;
   }
   drawGrid();
   drawCells();
 
   universe.tick();
   const fps = numframes.value;
 
  
   animationId = requestAnimationFrame(renderLoop);
}

Dictate speed

The user can now tell how fast the game plays by changing frames per second. I added a numerical input box which controls how many frames per second should be rendered.

<div>
     <label for="frames">Frames per second</label>
     <input id="frames" type="number" min="1" value="5"/>
</div>

Everytime the frame renders, It checks the value of the input box. I then apply a sleep method, which pauses execution for a set number of milliseconds. Since there are 1000 milliseconds in one second, I apply a waiting time of 1000/fps milliseconds. So the more the frames per second, the less the sleeping time and the faster the simulation is.

const numframes = document.getElementById("frames");
 
function sleep(ms) {
   return new Promise(resolve => setTimeout(resolve, ms));
}
 
const renderLoop = () => {
   //...
   const fps = numframes.value;
 
   sleep(1000/fps).then(()=>{
       animationId = requestAnimationFrame(renderLoop);
   })
}

Random Spawning

Everytime you play, I initialize the Cell states randomly. For this I had to add the rand crate. I added getrandom too in order to generate random numbers. This is because rand does not currently support web assembly, while getrandom has support for javascript.

rand = {version = "0.8.5"}
getrandom = { version = "0.2", features = ["js"] }

Now when the universe is initialized, a random number between 0 and 3 is generated. If the number is 3, then a cell comes to life, else it is initialized as a Dead cell.

pub fn new() -> Universe {
       //...
       let cells = (0..width * height)
           .map(|i| {
               let num = rand::thread_rng().gen_range(0..4);
               if num == 3{
                   Cell::Alive
               } else {
                   Cell::Dead
               }
           }).collect();
   //...
   }

Now you can see all sorts of new random patterns.

Increase universe Size

To have control of how big you want the universe to be, I created a slider to allow the user to control this.

<div id="slidecontainer">
    <label for="cell-range"> Universe Size</label>
     <input id="cell-range" type="range" min="1" max="100" value="20" class="slider" id="myRange">
     <span id="range-value"></span>
   </div>

Everytime the value of the slider changes, a new Universe is created. This new Universe has the width and height of whatever the value of the slider is. The size of the universe can be changed at any time.

const rangeValue = document.getElementById("range-value");
 
cellRange.oninput = function (){
   universe = Universe.new(parseInt(cellRange.value));
   let value = parseInt(cellRange.value);
   rangeValue.innerHTML = value;
 
   width = universe.width();
   height = universe.height();
   canvas.height = (CELL_SIZE +1 )* height +1;
   canvas.width = (CELL_SIZE +1) * width +1;
   drawGrid();
   drawCells();
}

Conclusion

I feel like I've come a long way. This application really taught me a lot about the inner workings of Web Assembly. Exposing our Rust code to Javascript works wonders.

The best part about building this game was, I actually got to play it for a couple of hours. Seeing all the beautiful patterns that can emerge from such simple rules. By merely changing the state of a cell from Alive to Dead, can change the course of the entire game.

Life is beautiful and I am grateful to be in this position and learning everyday.

Thank you so much for staying with me throughout this journey. I hope you learnt as much as I have. Please reach out to me on Instagram, Twitter or LinkedIn as to what you are learning next and I will share your journey too.

Until next time. Peace ✌🏼 .

Share