html 和 javascript 中的生活游戏无法运行

Game of life in html and javascript not working

所以我尝试在 html canvas 和 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>

使用 JavaScript 需要记住的一件事是它是一种单线程语言。更重要的是,当任何 JavaScript 代码被 运行ning 时,页面上的任何交互都变得不可能。浏览器中的 JavaScript 主要是 事件 驱动,您一次执行一小段代码然后空闲;然后,当事件发生时(按钮点击、计时器、HTTP 响应),您将执行该事件的处理程序。

总是 运行ning 代码,例如您的游戏循环,将无法正常工作。尽管您有一个变量来停止循环,但您的事件代码的 none(例如按钮点击)将能够 运行,因为单个 JavaScript 线程永远不会将控制权交还给 DOM.

您要做的是将 while 循环转换为事件驱动的东西。一种方法是设置周期性计时器,然后在每次更新时更新游戏。我更喜欢的一种方法是使用 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 将再次被调用,你将获得下一个循环。

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

所以如果你这样做

for(;;);

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

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

  • setTimeout 稍后调用函数(或更具体地说是 "queues a task to add an event later since we said above the browser just processes events")或 setInterval 在某个时间间隔调用函数。

    function processOneFrame() {
        ...
    }
    setInterval(processOneFrame, 1000); // call processOneFrame once a second
    

    function processOneFrame() {
        ...
        setTimeout(processOneFrame, 1000); // call processOneFrame in a second
    }
    processOneFrame();
    
  • 使用requestAnimationFrame。这个函数与 setTimeout 非常相似,除了它与绘制页面的浏览器对齐并且通常以与您的计算机更新屏幕相同的速度调用,通常每秒 60 次。

    function processOneFrame() {
        ...
        requestAnimationFrame(processOneFrame); // call processOneFrame for the next frame
    }
    requestAnimationFrame(processOneFrame); 
    
  • 您也可以使用现代 async/await 让您的代码看起来像一个普通的循环

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

所以,使用最后一种方法

  • 我把function startGame改成了async function startGame。这样就可以使用await关键字了。

  • startGame 的顶部,我检查它是否已经启动。否则每次我们点击开始我们都会开始另一个。

  • while (pGame == 0) 循环的底部我放了

    await wait(500);
    

    在迭代之间等待 1/2 秒。如果你想让事情 运行 更快,你可以降低它,或者如果你想 运行 每秒 60 帧,可以将它更改为 await waitFrame();。对于看起来有点太快的 70x20 小场。

  • 我更改了鼠标转换代码以更正确地计算 canvas 相对鼠标位置。

  • 我修正了 2 个错别字 tableOne[k]length 需要 tableOne[k].length

  • 在游戏循环的顶部,代码正在创建一个新的 table。这意味着正在处理的 table 总是全 0。所以我摆脱了那条线。

  • 绘制单元格的代码从未清除 canvas 所以我添加了一行来清除 canvas.

  • 我在检查越界访问时去掉了幻数 70 和 20

  • 我摆脱了开始按钮。只有一个 run/pause 按钮和一个清除按钮。我也摆脱了 sGamepGame,而是使用 runningloopinglooping 是真的循环还在循环。 running是要不要运行。我想很困惑,但问题是没有这些更改,如果你按 "run" 然后 "pause" startGame 内的循环可能仍然在 await 行(因此循环没有退出)。如果您在循环退出之前再次按 运行 ,您将开始第二个循环。所以looping确保只有一个循环。

  • 最重要的是我删除了所有不必要的 code/css/html。寻求帮助时,您应该制作一个 最小 回购协议。

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