Reputation: 33
so i tried to write the Game of Life in html canvas and JavaScript and with the help of many online tutorials i managed to write some code i still believe in. But when I launch the html page in the browser and start the game itself (that is, i was able to pick the starting cells), the site slows down incredibly. I checked how far does the code make it with console.log(...), so I found out it dies somewhere in the main loop. One thing I don't understand is that, when checking the values of some for loop variables, it seems like they are getting over the limit given in the for. Thank you for your help, it is possible I am missing something obvious.
// variables etc.
var pGame = 0;
var sGame = 0;
const sc = 20;
const c = document.getElementById("canvas");
c.addEventListener("mousedown", fillPixel);
const ctx = c.getContext("2d");
ctx.scale(sc, sc);
const columns = c.width / sc;
const rows = c.height / sc;
function createTable() {
return new Array(columns).fill(null)
.map(() => new Array(rows).fill(0));
}
var tableOne = createTable();
var tableTwo = createTable();
//functions
function fillPixel(event) {
if (sGame == 0) {
var x = Math.floor((event.clientX - canvas.offsetLeft - 5) / sc);
var y = Math.floor((event.clientY - canvas.offsetTop - 5) / sc);
if (tableOne[x][y] == 0) {
ctx.fillRect(x, y, 1, 1);
tableOne[x][y] = 1;
console.log("filled x" + x + " y" + y);
}else{
ctx.clearRect(x, y, 1, 1);
tableOne[x][y] = 0;
console.log("cleared x" + x + " y" + y);
}
}
}
function pauseGame() {
if (sGame == 1) {
if (pGame == 0) {
pGame = 1;
document.getElementById("b1").innerHTML = "resume";
}else{
pGame = 0;
document.getElementById("b1").innerHTML = "pause";
startGame();
}
}
}
function resetGame(){
sGame = 0;
pGame = 0;
document.getElementById("b1").innerHTML = "pause";
tableOne = createTable();
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function startGame() {
sGame = 1;
console.log("while");
while (pGame == 0) {
tableOne = createTable();
for (let col = 0; col < tableOne.length; col++){
for (let row = 0; row < tableOne[col].length; row++){
console.log("col" + col + " row" + row);
const cell = tableOne[col][row];
let neighbours = 0;
for (let i = -1; i < 2; i++){
for (let j = -1; j < 2; j++){
if (i == 0 && j == 0) {
continue;
}
const xCell = col + i;
const yCell = row + j;
if (xCell >= 0 && yCell >= 0 && xCell < 70 && yCell < 20) {
neighbours += tableOne[xCell][yCell];
}
}
}
console.log("applying rules");
if (cell == 1 && (neighbours == 2 || neighbours == 3)) {
tableTwo[col][row] = 1;
}else if (cell == 0 && neighbours == 3) {
tableTwo[col][row] = 1;
}
}
}
console.log("drawing");
tableOne = tableTwo.map(arr => [...arr]);
tableTwo = createTable();
for (let k = 0; k < tableOne.length; k++){
for (let l = 0; l < tableOne[k]length; l++){
if (tableOne[k][l] == 1) {
ctx.fillRect(k, l, 1, 1);
}
}
}
}
}
body {
background-color: #F1E19C;
margin: 0;
}
.button {
background-color: #2C786E;
color: #FFFFFF;
border: none;
padding: 10px 20px;
text-align: center;
font-size: 16px;
}
#header {
background-color: #2C786E;
font-family: 'Times New Roman';
padding: 10px 15px;
color: #FFFFFF;
font-size: 20px;
}
#footer {
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
text-align: center;
font-family: 'Roboto';
}
#canvas {
border: 5px solid #813152;
margin-top: 5px;
margin-left: auto;
margin-right: auto;
display: block;
cursor: crosshair
}
#btns {
text-align: center;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="tres.css">
</head>
<body>
<div id="header">
<h1>Game of Life</h1>
</div>
<p>
<canvas id="canvas" width="1400" height="400"></canvas>
</p>
<p id="btns">
<button class="button" onclick="startGame()"> start </button>
<button class="button" id="b1" onclick="pauseGame()"> pause </button>
<button class="button" onclick="resetGame()"> clear </button>
</p>
<div id="footer">
<p>©2020</p>
</div>
<script src="dos.js"></script>
<body/>
</html>
Upvotes: 3
Views: 114
Reputation:
As @Jacob pointed out you can't loop forever in JavaScript. JavaScript, in the browser, expects you to have code that responds to events and then exits so the browser can process more events. Events include the script loading, the page loading, timers, mouse events, keyboard events, touch events, network events etc..
So if you just do this
for(;;);
The browser will freeze for 10 to 60 seconds and then tell you the page is unresponsive and ask if you want to kill it.
There are a bunch of ways to structure your code to deal with that.
setTimeout
which calls the a function later (or more specifically it "queues a task to add an event later since we said above the browser just processes events") or setInterval
which calls a function at some interval.
function processOneFrame() {
...
}
setInterval(processOneFrame, 1000); // call processOneFrame once a second
or
function processOneFrame() {
...
setTimeout(processOneFrame, 1000); // call processOneFrame in a second
}
processOneFrame();
Use requestAnimationFrame
. This functions pretty similar to setTimeout except that it is aligned with the browser drawing the page and is generally called at the same speed as the your computer updates the screen, usually 60 times a second.
function processOneFrame() {
...
requestAnimationFrame(processOneFrame); // call processOneFrame for the next frame
}
requestAnimationFrame(processOneFrame);
You can also use modern async/await to still make your code looks like a normal loop
// functions you can `await` on in an async function
const waitFrame = _ => new Promise(resolve => requestAnimationFrame(resolve));
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
...
while (!done) {
... do game stuff...
await waitFrame();
}
}
So, using that last method
I changed function startGame
to async function startGame
. This way it is allowed to use the await
keyword.
At the top of startGame
I check if it's already started. Otherwise every time we click start we`d start another.
At the bottom of the while (pGame == 0)
loop I put
await wait(500);
Which waits 1/2 a second between iterations. You can lower it if you want things to run faster or change it to await waitFrame();
if you want to run at the 60 frames a second. For a small 70x20 field that seems a little too fast.
I changed the mouse conversion code to more correctly compute a canvas relative mouse position.
I fixed 2 typos of tableOne[k]length
that needed to be tableOne[k].length
At the top of the game loop the code was creating a new table. That meant the table being processed was always all 0s. So I got rid of that line.
The code drawing the cells never cleared the canvas so I added a line to clear the canvas.
I got rid of the magic numbers 70 and 20 when checking for out of bounds access
I got rid of the start button. There is just a run/pause button and a clear button. I also got rid sGame
and pGame
and instead use running
and looping
. looping
is true of the loop is still looping. running
is whether or not it should run. Confusing I suppose but the issue is without these changes, if you press "run" then "pause" the loop inside startGame might still be at the await
line (so the loop has no exited). If you were to press run again before the loop exits you'd start a second loop. So looping
makes sure there is only one loop.
Most importantly I removed all unnecessary code/css/html. You're supposed to make a minimal repo when asking for help.
// variables etc.
let running = false;
let looping = false;
const sc = 20;
const c = document.getElementById("canvas");
c.addEventListener("mousedown", fillPixel);
const ctx = c.getContext("2d");
ctx.scale(sc, sc);
const columns = c.width / sc;
const rows = c.height / sc;
function createTable() {
return new Array(columns).fill(null)
.map(() => new Array(rows).fill(0));
}
var tableOne = createTable();
var tableTwo = createTable();
//functions
function fillPixel(event) {
if (!running) {
const rect = canvas.getBoundingClientRect();
const canvasX = (event.clientX - rect.left) / rect.width * canvas.width;
const canvasY = (event.clientY - rect.top) / rect.height * canvas.height;
var x = Math.floor(canvasX / sc);
var y = Math.floor(canvasY / sc);
if (tableOne[x][y] == 0) {
ctx.fillRect(x, y, 1, 1);
tableOne[x][y] = 1;
//console.log("filled x" + x + " y" + y);
} else {
ctx.clearRect(x, y, 1, 1);
tableOne[x][y] = 0;
//console.log("cleared x" + x + " y" + y);
}
}
}
function pauseGame() {
if (running) {
running = false;
document.getElementById("b1").innerHTML = "run";
} else {
document.getElementById("b1").innerHTML = "pause";
startGame();
}
}
function resetGame() {
running = false;
document.getElementById("b1").innerHTML = "run";
tableOne = createTable();
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const waitFrame = _ => new Promise(resolve => requestAnimationFrame(resolve));
async function startGame() {
if (running || looping) {
return; // it's already started
}
running = true;
looping = true;
console.log("while");
while (running) {
for (let col = 0; col < tableOne.length; col++) {
for (let row = 0; row < tableOne[col].length; row++) {
//console.log("col" + col + " row" + row);
const cell = tableOne[col][row];
let neighbours = 0;
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
if (i == 0 && j == 0) {
continue;
}
const xCell = col + i;
const yCell = row + j;
if (xCell >= 0 && yCell >= 0 && xCell < columns && yCell < rows) {
neighbours += tableOne[xCell][yCell];
}
}
}
//console.log("applying rules");
if (cell == 1 && (neighbours == 2 || neighbours == 3)) {
tableTwo[col][row] = 1;
} else if (cell == 0 && neighbours == 3) {
tableTwo[col][row] = 1;
}
}
}
//console.log("drawing");
tableOne = tableTwo.map(arr => [...arr]);
tableTwo = createTable();
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let k = 0; k < tableOne.length; k++) {
for (let l = 0; l < tableOne[k].length; l++) {
if (tableOne[k][l] == 1) {
ctx.fillRect(k, l, 1, 1);
}
}
}
await wait(500); // wait 1/2 a second (500 milliseconds)
}
looping = false;
}
body {
background-color: #F1E19C;
margin: 0;
}
.button {
background-color: #2C786E;
color: #FFFFFF;
border: none;
padding: 10px 20px;
text-align: center;
font-size: 16px;
}
#canvas {
border: 5px solid #813152;
margin-top: 5px;
margin-left: auto;
margin-right: auto;
display: block;
cursor: crosshair
}
#btns {
text-align: center;
}
<p>
<canvas id="canvas" width="1400" height="400"></canvas>
</p>
<p id="btns">
<button class="button" id="b1" onclick="pauseGame()"> run </button>
<button class="button" onclick="resetGame()"> clear </button>
</p>
Upvotes: 4
Reputation: 78840
One thing you have to keep in mind with JavaScript is that it's a single-threaded language. What's more, when any JavaScript code is running, any interactivity on the page becomes impossible. JavaScript in a browser is mainly meant to be event driven, where you execute small pieces of code at a time then go idle; then, when an event takes place (button click, timer, HTTP response), you execute the handler for that event.
Constantly running code, like your game loop, won't work right. Although you have a variable to stop the loop, none of your event code like the button clicks will be able to run because the single JavaScript thread will never yield control back to the DOM.
What you'll want to do is convert your while loop to something event driven. One approach is to set periodic timers and then do the game updates on each tick. One approach I prefer is to use requestAnimationFrame
. Your while loop can become this instead:
function startGame() {
sGame = 1;
requestAnimationFrame(performUpdates);
}
function performUpdates() {
tableOne = createTable();
for (let col = 0; col < tableOne.length; col++){
// ...
}
// ...
if (sGame && !pGame) {
requestAnimationFrame(performUpdates);
}
}
After a call to performUpdates
completes, JavaScript will sit idle for a while, allowing your page the ability to respond to click events. Since at the end you've requested another animation frame, when your browser decides it makes sense, performUpdates
will be called again, and you'll get your next cycle.
Upvotes: 0