Как закодировать «Игра жизни» с помощью React менее чем через час

1656625453 kak zakodirovat igra zhizni s pomoshhyu react menee chem cherez

от Чарли Ли

FulTW1yEZ0dyY2-fTYD03JwmH1by-hR0p-6M

Недавно я просмотрел известное видео, создающее игру со змеями менее чем за 5 минут (на Youtube). Выглядело довольно интересно сделать этот тип быстрой кодировки, поэтому я решил сделать его сам.

Когда я в детстве начал изучать программирование, я изучил игру под названием Game of Life. Это отличный пример сотовой автоматизации и того, как простые правила могут привести к сложным шаблонам. Представьте себе какую-нибудь форму жизни, которая живет в мире. На каждом повороте они соблюдают несколько простых правил, чтобы решить, жива ли мертва жизнь.

Игра жизни Конвея.
С момента публикации «Игра жизни Конвея» вызвала большой интерес из-за удивительных способов, которыми…en.wikipedia.org

Потому я решил закодировать эту игру. Поскольку он не включает слишком много графики – только сетку и несколько блоков – я решил, что React будет хорошим выбором, и его можно использовать как краткое руководство для React. Давайте начнем!

Настройка React

Сначала нам нужно настроить React. Чрезвычайное create-react-app инструмент очень удобен для запуска нового проекта React:

$ npm install -g create-react-app$ create-react-app react-gameoflife

Меньше чем через одну минуту, react-gameoflife будет готов. Теперь все, что нам нужно сделать, это запустить его:

$ cd react-gameoflife$ npm start

Это запустит сервер разработчика по адресу, и по этому адресу откроется окно обозревателя.

Выбор дизайна

Окончательный экран, который мы хотим сделать, выглядит так:

VdXPrJSLu6cEFtDpryJmUPMfHTfVUCT6i9H4
Игра жизни Конвея

Это просто доска с сеткой и некоторыми белыми плитками (ячейками), которые можно разместить или удалить, нажав на сетку. Кнопка «Выполнить» запускает итерацию с заданным интервалом.

Выглядит довольно просто, да? Давайте подумаем, как это сделать в React. Прежде всего, это React нет графический фреймворк, поэтому мы не будем думать об использовании canvas. (Вы можете посмотреть на PIXI или Phaser, если вы заинтересованы в использовании canvas.)

Доска может быть компонентом и может быть воспроизведена с помощью одного <div>. Как насчет сетки? Невозможно нарисовать сетку with

s, а поскольку сетка статическая, она также не нужна. В самом деле, мы can use CSS3 linear-градиент для сетки.

Что касается клеток, то мы можем использовать <div> чтобы нарисовать каждую ячейку. Сделаем это отдельной составляющей. Этот компонент accепtsx, y как входные данные, чтобы плата могла указать свое положение.

Первый шаг: доска

Давайте сначала создадим доску. Создайте файл под названием Game.js под src каталог и введите следующий код:

import React from 'react';import './Game.css';
const CELL_SIZE = 20;const WIDTH = 800;const HEIGHT = 600;
class Game extends React.Component {  render() {    return (      <div>        <div className="Board"          style={{ width: WIDTH, height: HEIGHT }}>        </div>      </div>    );  }}
export default Game;

Нам тоже нужны Game.css файл для определения стилей:

.Board {  position: relative;  margin: 0 auto;  background-color: #000;}

Обновление App.js импортировать наши Game.js и разместить Game компонент на экране. Теперь мы видим совершенно черную игровую доску.

Наш следующий шаг – создать сетку. Сетку можно создать только с одной строки linear-gradient (добавьте это к Game.css):

background-image:    linear-gradient(#333 1px, transparent 1px),    linear-gradient(90deg, #333 1px, transparent 1px);

Собственно надо уточнить background-size стиль, а также, чтобы он работал. Но с тех пор CELL_SIZE константа определяется в Game.js, мы указываем размер фона с помощью встроенного стиля. Изменить style линия в Game.js:

<div className="Board"  style={{ width: WIDTH, height: HEIGHT,    backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}></div>

Обновите браузер, и вы увидите хорошую сетку.

HcVZMkOP0xyAXJs6kXciMYrzgnraPUf-ZZVQ
Сетка создана с помощью градиента CSS3.

Создайте ячейки

Следующий шаг – позволить пользователю взаимодействовать с доской для создания ячеек. Мы будем использовать 2D-массив this.board для сохранения состояния платы и списка ячеек this.state.cells чтобы сохранить положение клеток. Как только состояние платы обновлено, метод this.makeCells() будет вызван для создания списка ячеек из состояния платы.

Добавьте эти методы к Game класс:

class Game extends React.Component {  constructor() {    super();    this.rows = HEIGHT / CELL_SIZE;    this.cols = WIDTH / CELL_SIZE;    this.board = this.makeEmptyBoard();  }
  state = {    cells: [],  }
  // Create an empty board  makeEmptyBoard() {    let board = [];    for (let y = 0; y < this.rows; y++) {      board[y] = [];      for (let x = 0; x < this.cols; x++) {        board[y][x] = false;      }    }    return board;  }
  // Create cells from this.board  makeCells() {    let cells = [];    for (let y = 0; y < this.rows; y++) {      for (let x = 0; x < this.cols; x++) {        if (this.board[y][x]) {          cells.push({ x, y });        }      }    }    return cells;  }  ...}

Далее мы позволим пользователю нажать на доску, чтобы разместить или удалить ячейку. в React, <div> можно подключить with an oОбработчик событий nClick, который может получить координату щелчка через событие щелчка. Однако координата относится к клиентской области (видимой области браузера), поэтому нам нужен дополнительный код, чтобы превратить ее в координату, которая есть относительно доски.

Добавьте обработчик события в render() метод. Здесь мы также сохраняем ссылку на элемент доски, чтобы позже получить местоположение доски.

render() {  return (    <div>      <div className="Board"        style={{ width: WIDTH, height: HEIGHT,          backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}        onClick={this.handleClick}        ref={(n) => { this.boardRef = n; }}>      </div>    </div>  );}

И вот еще несколько способов. Здесь getElementOffset() рассчитает положения элемента платы. handleClick() получит позицию щелчка, затем превратит ее в относительную позицию и вычислит столбцы и строки ячейки, на которую щелкают. Затем состояние клетки возвращается.

class Game extends React.Component {  ...  getElementOffset() {    const rect = this.boardRef.getBoundingClientRect();    const doc = document.documentElement;
    return {      x: (rect.left + window.pageXOffset) - doc.clientLeft,      y: (rect.top + window.pageYOffset) - doc.clientTop,    };  }
  handleClick = (event) => {    const elemOffset = this.getElementOffset();    const offsetX = event.clientX - elemOffset.x;    const offsetY = event.clientY - elemOffset.y;        const x = Math.floor(offsetX / CELL_SIZE);    const y = Math.floor(offsetY / CELL_SIZE);
    if (x >= 0 && x <= this.cols && y >= 0 && y <= this.rows) {      this.board[y][x] = !this.board[y][x];    }
    this.setState({ cells: this.makeCells() });  }  ...}

На последнем этапе мы отразим клетки this.state.cells к доске:

class Cell extends React.Component {  render() {    const { x, y } = this.props;    return (      <div className="Cell" style={{        left: `${CELL_SIZE * x + 1}px`,        top: `${CELL_SIZE * y + 1}px`,        width: `${CELL_SIZE - 1}px`,        height: `${CELL_SIZE - 1}px`,      }} />    );  }}
class Game extends React.Component {  ...  render() {    const { cells } = this.state;    return (      <div>        <div className="Board"          style={{ width: WIDTH, height: HEIGHT,            backgroundSize: `${CELL_SIZE}px ${CELL_SIZE}px`}}          onClick={this.handleClick}          ref={(n) => { this.boardRef = n; }}>          {cells.map(cell => (            <Cell x={cell.x} y={cell.y}                key={`${cell.x},${cell.y}`}/>          ))}        </div>              </div>    );  }  ...}

И не забудьте добавить стили для Cell компонент (в Game.css):

.Cell {  background: #ccc;  position: absolute;}

Обновите браузер и попытайтесь нажать на доску. Клетки можно разместить или удалить сейчас!

haXwArXI6t0pARVfN7IMbkUTKLkjexifGZCR
Клеточки можно размещать или удалять, щелкнув по доске.

Запустите игру

Теперь нам нужны помощники для запуска игры. Сначала добавим несколько контролеров.

class Game extends React.Component {  state = {    cells: [],    interval: 100,    isRunning: false,  }  ...
  runGame = () => {    this.setState({ isRunning: true });  }
  stopGame = () => {    this.setState({ isRunning: false });  }
  handleIntervalChange = (event) => {    this.setState({ interval: event.target.value });  }
  render() {    return (      ...        <div className="controls">          Update every <input value={this.state.interval}              onChange={this.handleIntervalChange} /> msec          {isRunning ?            <button className="button"              onClick={this.stopGame}>Stop</button> :            <button className="button"              onClick={this.runGame}>Run</button>          }        </div>      ...    );  }}

Этот код добавит один интервальный вход и одну клавишу в нижней части экрана.

5IdZVY7GD1McREviUPScFMMMX7U0SGmMgnqn
Контроллеры

Обратите внимание, что нажатие кнопки «Выполнить» не имеет никакого эффекта, поскольку мы ничего не написали для запуска игры. Давайте сделаем это сейчас.

В этой игре состояние доски обновляется на каждой итерации. Поэтому нам нужен метод runIteration() называться каждую итерацию, скажем, 100 мс. Этого можно добиться с помощью window.setTimeout().

Когда нажата кнопка Выполнить, runIteration() будет вызван. Перед тем как он закончится, он позвонит window.setTimeout() чтобы организовать еще одну итерацию через 100 мс. Таким образом, runIteration() будет вызываться неоднократно. Когда будет нажата кнопка Остановить, мы отменяем устроенный тайм-аут, позвонив по телефону window.clearTimeout() чтобы можно было остановить итерацию.

class Game extends React.Component {  ...  runGame = () => {    this.setState({ isRunning: true });    this.runIteration();  }
  stopGame = () => {    this.setState({ isRunning: false });    if (this.timeoutHandler) {      window.clearTimeout(this.timeoutHandler);      this.timeoutHandler = null;    }  }
  runIteration() {    console.log('running iteration');    let newBoard = this.makeEmptyBoard();
    // TODO: Add logic for each iteration here.
    this.board = newBoard;    this.setState({ cells: this.makeCells() });
    this.timeoutHandler = window.setTimeout(() => {      this.runIteration();    }, this.state.interval);  }  ...}

Перезагрузите обозреватель и нажмите кнопку «Выполнить». В консоли мы увидим сообщение журнала «выполняется итерация». (Если вы не знаете, как показать консоль, попробуйте нажать Ctrl-Shift-I.)

Теперь нам нужно добавить правила игры runIteration() метод. Согласно Википедии, Игра жизни имеет четыре правила:

1. Любая живая клетка с менее чем двумя живыми соседями погибает, словно вызванная недопопуляцией.

2. Любая живая клетка с двумя-тремя живыми соседями живет до следующего поколения.

3. Любая живая клетка с более чем тремя живыми соседями погибает, словно от перенаселения.

4. Любая мертвая клетка с ровно тремя живыми соседями становится живой клеткой, словно путем размножения.

Мы можем добавить метод calculateNeighbors() вычислить количество соседей данного (x, y). (Исходный код calcualteNeighbors() будет опущен в этой публикации, но вы можете найти его здесь.) Тогда мы сможем применить правила простым способом:

for (let y = 0; y < this.rows; y++) {  for (let x = 0; x < this.cols; x++) {    let neighbors = this.calculateNeighbors(this.board, x, y);    if (this.board[y][x]) {      if (neighbors === 2 || neighbors === 3) {        newBoard[y][x] = true;      } else {        newBoard[y][x] = false;      }    } else {      if (!this.board[y][x] && neighbors === 3) {        newBoard[y][x] = true;      }    }  }}

Перезагрузите браузер, разместите начальные ячейки и нажмите кнопку «Выполнить». Вы можете увидеть удивительные анимации!

Dzw9iypiLw8DF4gtJCTpUPNTMVDjDmQF9DSQ
Планер Госпер

Вывод

Чтобы сделать игру веселее, я также добавил кнопки Случайные и Очистить, чтобы помочь с размещением ячеек. Полный исходный код можно найти на моем GitHub.

Спасибо за чтение! Если вы считаете эту публикацию интересной, поделитесь ею с другими людьми, рекомендуя ее.

Добавить комментарий

Ваш адрес email не будет опубликован.