HTML和JavaScript编写的生命游戏无法运行。

3

我尝试使用HTML画布和JavaScript编写生命游戏,通过许多在线教程的帮助,我成功编写了一些代码。但是当我在浏览器中启动HTML页面并开始游戏(也就是说,我能够选择起始单元格)时,网站变得非常缓慢。我使用console.log(...)检查代码的执行进度,发现它在主循环的某个位置崩溃了。有一件事我不太明白,即当我检查一些for循环变量的值时,它们似乎超过了for循环中给定的限制。感谢您的帮助,可能我忽略了一些显而易见的东西。

// 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>&copy;2020</p>
 </div>
 <script src="dos.js"></script> 
  <body/>
</html>


当你说它正在死亡,是因为它显示页面已经无响应或类似的情况吗?你能发布一下你收到的任何错误吗? - Jacob
我在你的代码中看到了一个错别字,tableOne[k]length 应该是 tableOne[k].length,这可能是你的语法错误的原因,但我的答案也是相关的。 - Jacob
2个回答

4

正如 @Jacob 指出的那样,你不能在JavaScript中无限循环。在浏览器中,JavaScript 希望你有响应事件的代码,然后退出,以便浏览器可以处理更多的事件。事件包括脚本加载、页面加载、定时器、鼠标事件、键盘事件、触摸事件、网络事件等。

因此,如果你只是这样做:

for(;;);

浏览器会冻结10到60秒,然后告诉您页面无响应,并询问您是否要终止它。

有很多方法可以构建您的代码来处理这个问题。

  • 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();
       }
    }
    

所以,使用上述的最后一种方法

  • 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>


0
JavaScript 相关内容:需要记住的一件事是,JavaScript 是单线程语言。此外,当任何 JavaScript 代码正在运行时,页面上的任何交互都将变得不可能。浏览器中的 JavaScript 主要是以事件驱动的方式运行的,其中您每次执行小段代码然后进入空闲状态;然后,当发生事件(按钮单击、计时器、HTTP 响应)时,您会执行该事件的处理程序。
像游戏循环这样不断运行的代码将无法正常工作。虽然您有一个变量来停止循环,但您的按钮单击等事件代码将无法运行,因为单个 JavaScript 线程永远不会将控制权返回给 DOM。
您需要做的是将 while 循环转换为事件驱动的形式。一种方法是设置定期计时器,然后在每个 tick 上进行游戏更新。我更喜欢的一种方法是使用 requestAnimationFrame。您的 while 循环可以改为以下形式:

function startGame() {
  sGame = 1;
  requestAnimationFrame(performUpdates);
}

function performUpdates() {
  tableOne = createTable();
  for (let col = 0; col < tableOne.length; col++){
    // ...
  }

  // ...

  if (sGame && !pGame) {
    requestAnimationFrame(performUpdates);
  }
}

在调用 performUpdates 后完成后,JavaScript 将会空闲一段时间,使您的页面能够响应点击事件。由于最后您请求了另一个动画帧,当您的浏览器决定有意义时,performUpdates 将再次被调用,然后您将获得下一个周期。


1
你可以使用 const waitFrame = _ => new Promise(resolve => requestAnimationFrame(resolve));,然后你可以使用一个循环 async function doGame() { while(!exit) { ... 做一些事情...; await waitFrame(); } } - gman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接