Игра жизни в html и javascript не работает

поэтому я попытался написать Игру Жизни на 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>


person zubr    schedule 09.06.2020    source источник
comment
Когда вы говорите, что он умирает, это потому, что он говорит, что страница перестала отвечать на запросы или что-то в этом роде? Не могли бы вы опубликовать какие-либо ошибки, которые вы получаете?   -  person Jacob    schedule 09.06.2020
comment
Я вижу опечатку в вашем коде tableOne[k]length должно быть tableOne[k].length, что, вероятно, является причиной вашей синтаксической ошибки, но мой ответ также актуален.   -  person Jacob    schedule 09.06.2020


Ответы (2)


Как отметил @Jacob, вы не можете вечно зацикливаться на JavaScript. JavaScript в браузере ожидает, что у вас будет код, который реагирует на события, а затем завершает работу, чтобы браузер мог обрабатывать больше событий. События включают в себя загрузку сценария, загрузку страницы, таймеры, события мыши, события клавиатуры, события касания, сетевые события и т. д.

Итак, если вы просто сделаете это

for(;;);

Браузер зависнет на 10–60 секунд, а затем сообщит вам, что страница не отвечает, и спросит, хотите ли вы ее убить.

Есть куча способов структурировать код, чтобы справиться с этим.

  • setTimeout, который вызывает функцию позже (или, точнее, «ставит задачу в очередь, чтобы добавить событие позже, поскольку мы сказали выше, что браузер просто обрабатывает события») или setInterval, который вызывает функцию с некоторым интервалом.

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

    or

    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 секунды между итерациями. Вы можете уменьшить его, если хотите, чтобы все работало быстрее, или изменить его на await waitFrame();, если вы хотите работать со скоростью 60 кадров в секунду. Для небольшого поля 70x20 это кажется слишком быстрым.

  • Я изменил код преобразования мыши, чтобы более правильно вычислить положение мыши относительно холста.

  • Я исправил 2 опечатки в tableOne[k]length, которые должны были быть tableOne[k].length.

  • В начале игрового цикла код создавал новую таблицу. Это означало, что обрабатываемая таблица всегда состояла из нулей. Поэтому я избавился от этой линии.

  • Код, рисующий ячейки, никогда не очищал холст, поэтому я добавил строку для очистки холста.

  • Я избавился от магических чисел 70 и 20 при проверке доступа за границу

  • Я избавился от кнопки запуска. Есть только кнопка запуска/паузы и кнопка очистки. Я также избавился от sGame и pGame и вместо них использую running и looping. looping истинно, цикл все еще зацикливается. running должен ли он запускаться. Я полагаю, что это сбивает с толку, но проблема заключается в том, что без этих изменений, если вы нажмете «Выполнить», а затем «Пауза», цикл внутри startGame может по-прежнему находиться в строке await (поэтому цикл не завершился). Если бы вы снова нажали «Выполнить» до выхода из цикла, вы бы начали второй цикл. Таким образом, looping гарантирует наличие только одного цикла.

  • Самое главное, я удалил весь ненужный код/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>

person gman    schedule 09.06.2020

Одна вещь, которую вы должны иметь в виду при работе с JavaScript, заключается в том, что это однопоточный язык. Более того, при выполнении любого кода JavaScript любая интерактивность на странице становится невозможной. JavaScript в браузере в основном предназначен для управления событиями, когда вы выполняете небольшие фрагменты кода за раз, а затем переходите в режим ожидания; затем, когда происходит событие (нажатие кнопки, таймер, ответ HTTP), вы выполняете обработчик этого события.

Постоянно работающий код, такой как ваш игровой цикл, не будет работать правильно. Хотя у вас есть переменная для остановки цикла, ни один из ваших кодов событий, таких как нажатия кнопок, не сможет работать, потому что единственный поток 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 будет вызван снова, и вы получите следующий цикл.

person Jacob    schedule 09.06.2020
comment
вы можете использовать const waitFrame = _ => new Promise(resolve => requestAnimationFrame(resolve));, а затем вы можете использовать цикл async function doGame() { while(!exit) { ... do stuff...; await waitFrame(); } }; - person gman; 09.06.2020