
Содержание статьи
от Чарли Ли

Недавно я просмотрел известное видео, создающее игру со змеями менее чем за 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
Это запустит сервер разработчика по адресу, и по этому адресу откроется окно обозревателя.
Выбор дизайна
Окончательный экран, который мы хотим сделать, выглядит так:

Это просто доска с сеткой и некоторыми белыми плитками (ячейками), которые можно разместить или удалить, нажав на сетку. Кнопка «Выполнить» запускает итерацию с заданным интервалом.
Выглядит довольно просто, да? Давайте подумаем, как это сделать в React. Прежде всего, это React нет графический фреймворк, поэтому мы не будем думать об использовании canvas. (Вы можете посмотреть на PIXI или Phaser, если вы заинтересованы в использовании canvas.)
Доска может быть компонентом и может быть воспроизведена с помощью одного <d
iv>. Как насчет сетки? Невозможно нарисовать сетку with
an use CSS3 lin
ear-градиент для сетки.
Что касается клеток, то мы можем использовать <d
iv> чтобы нарисовать каждую ячейку. Сделаем это отдельной составляющей. Этот компонент acc
епt
sx, 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>
Обновите браузер, и вы увидите хорошую сетку.

Создайте ячейки
Следующий шаг – позволить пользователю взаимодействовать с доской для создания ячеек. Мы будем использовать 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, <d
iv> можно подключить 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;}
Обновите браузер и попытайтесь нажать на доску. Клетки можно разместить или удалить сейчас!

Запустите игру
Теперь нам нужны помощники для запуска игры. Сначала добавим несколько контролеров.
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> ... ); }}
Этот код добавит один интервальный вход и одну клавишу в нижней части экрана.

Обратите внимание, что нажатие кнопки «Выполнить» не имеет никакого эффекта, поскольку мы ничего не написали для запуска игры. Давайте сделаем это сейчас.
В этой игре состояние доски обновляется на каждой итерации. Поэтому нам нужен метод 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; } } }}
Перезагрузите браузер, разместите начальные ячейки и нажмите кнопку «Выполнить». Вы можете увидеть удивительные анимации!

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