Birthday Sale! Up to 40% off unlimited courses & creative assets Birthday Sale! Save up to 40%!
Advertisement
  1. Game Development
  2. Programming
Gamedevelopment

Создание жизни: Игра жизни Конвея

by
Difficulty:IntermediateLength:LongLanguages:

Russian (Pусский) translation by Dima (you can also view the original English article)

Иногда даже простой набор основных правил может дать вам очень интересные результаты. В этом уроке мы с нуля создадим основной движок для Игра жизни Конвея.

Примечание: Хотя это руководство написано с использованием C и XNA, вы должны иметь возможность использовать одни и те же приемы и концепции практически в любой среде разработки 2D-игр.


Введение

Игра жизни Конвея - это клеточный автомат, разработанный в 1970-х годах британским математиком по имени Джон Конвей.

Учитывая двумерную сетку ячеек, с некоторыми "вкл" или "живыми" и другими "выключенными" или "мертвыми", и набором правил, которые управляют тем, как они оживают или умирают, мы можем иметь интересную "форму жизни" которая развернется прямо перед нами. Таким образом, просто рисуя несколько шаблонов на нашей сетке, а затем запуская симуляцию, мы можем наблюдать, как основные формы жизни развиваются, распространяются, отмирают и в конечном итоге стабилизируются. Загрузите окончательные исходные файлы или ознакомьтесь с демонстрацией ниже:

Теперь эта "Игра Жизни" не является просто "игрой" - это скорее машина, в основном потому, что в ней нет игрока и нет цели, она просто развивается на основе своих начальных условий. Тем не менее, с ней очень весело играть, и есть много принципов игрового дизайна, которые могут быть применены к ее созданию. Итак, без лишних слов, давайте начнем!

Для этого урока я пошел дальше и построил все в XNA, потому что это то, что мне больше всего нравится. (Здесь есть руководство по началу работы с XNA, если вам интересно.) Однако вы имеете возможность следовать любой среде разработки 2D-игр, с которой вы знакомы.


Создание клеток

Основной элемент в игре Конвея жизни это клетки, которые являются "формами жизни", которые формируют основу всего моделирования. Каждая клетка может находиться в одном из двух состояний: "жив" или "мертв". Для согласованности мы будем придерживаться этих двух названий состояний ячеек до конца этого урока.

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

Теперь, с точки зрения программирования их функциональности, мы должны дать им три схемы поведения:

  1. Они должны отслеживать свою позицию, границы и состояние, чтобы их можно было нажать и нарисовать.
  2. Они должны переключаться между живым и мертвым состоянием при нажатии, что позволяет пользователю делать интересные вещи.
  3. Они должны быть нарисованы как белые или черные, если они мертвы или живы, соответственно.

Все вышеперечисленное может быть достигнуто путем создания класса Cell, который будет содержать следующий код:


Сетка и ее правила

Теперь, когда каждая клетка будет вести себя правильно, нам нужно создать сетку, которая будет держать их всех, и реализовать логику, которая сообщает каждому, должна ли она ожить, остаться в живых, умереть или остаться мертвой (без зомби!).

Правила довольно просты:

  1. Любая живая клетка с менее чем двумя живыми соседями умирает.
  2. Любая живая клетка с двумя или тремя живыми соседями доживает до следующего поколения.
  3. Любая живая клетка с более чем тремя живыми соседями умирает, как будто из-за переполненности.
  4. Любая мертвая клетка с ровно тремя живыми соседями становится живой клеткой, как будто путем размножения.

Вот краткое визуальное руководство по этим правилам на изображении ниже. На каждую ячейку, выделенную синей стрелкой, будет действовать соответствующее ей пронумерованное правило выше. Другими словами, клетка 1 умрет, клетка 2 останется в живых, клетка 3 умрет, а клетка 4 оживет.

Таким образом, поскольку игровой симулятор запускает обновление с постоянными временными интервалами, сетка будет проверять каждое из этих правил для всех ячеек в сетке. Это можно сделать, поместив следующий код в новый класс, который я назову Grid:

Единственное, чего нам здесь не хватает, - это магический метод GetLivingNeighbors, который просто подсчитывает, сколько соседей текущей ячейки в данный момент живы. Итак, давайте добавим этот метод в наш класс Grid:

Обратите внимание, что в приведенном выше коде первый оператор if каждой пары просто проверяет, что мы не на краю сетки. Если бы у нас не было этой проверки, мы столкнулись бы с несколькими исключениями, выходящими за пределы массива. Кроме того, так как это приведет к тому, что счетчик никогда не будет увеличиваться при проверке за краями, это означает, что игра "предполагает", что края мертвы, поэтому это эквивалентно наличию постоянной границы белых мертвых ячеек вокруг наших игровых окон.


Обновление сетки в дискретных временных шагах

Пока что вся логика, которую мы реализовали, является здравой, но она не будет работать должным образом, если мы не будем осторожны, чтобы убедиться, что наше моделирование выполняется с дискретными временными шагами. Это просто причудливый способ сказать, что все наши ячейки будут обновлены в одно и то же время. Если бы мы не реализовали это, мы бы получили странное поведение, потому что порядок, в котором проверялись бы ячейки, имел бы значение, поэтому строгие правила, которые мы только что установили, развалились бы, и возник бы мини-хаос.

Например, наш цикл выше проверяет все ячейки слева направо, поэтому, если ячейка слева, которую мы только что проверили, ожила, это изменит счетчик для ячейки в середине, которую мы сейчас проверяем, и может оживить ее. Но если бы мы проверяли справа налево, то ячейка справа могла бы быть мертвой, а ячейка слева еще не ожила, поэтому наша средняя ячейка осталась бы мертвой. Это плохо, потому что это противоречиво! Мы должны иметь возможность проверять ячейки в любом произвольном порядке (например, по спирали!), И следующий шаг всегда должен быть одинаковым.

К счастью, это действительно довольно легко реализовать в коде. Все, что нам нужно, это иметь вторую сетку ячеек в памяти для следующего состояния ячеек. Каждый раз, когда мы определяем следующее состояние ячейки, мы сохраняем ее во второй сетке для следующего состояния всех ячеек. Затем, когда мы нашли следующее состояние каждой ячейки, мы применяем их все одновременно. Таким образом, мы можем добавить двумерный массив логических значений nextCellStates как частную переменную, а затем добавить этот метод в класс Grid:

Наконец, не забудьте исправить вышеописанный метод Update, чтобы он присваивал результат следующему состоянию, а не текущему, а затем вызывал SetNextState в самом конце метода Update, сразу после завершения циклов.


Рисование сетки

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

Фактическая сетка не должна ничего рисовать, но с точки зрения пользователя это будет намного понятнее, если мы добавим несколько линий сетки. Это позволяет пользователю легче видеть границы ячеек, а также передает ощущение масштаба, поэтому давайте создадим метод Draw следующим образом:

Обратите внимание, что в приведенном выше коде мы берем один пиксель и растягиваем его, чтобы создать очень длинную и тонкую линию. Ваш конкретный игровой движок может предоставить простой метод DrawLine, в котором вы можете указать две точки и провести линию между ними, что сделает это еще проще, чем выше.


Добавление игровой логики высокого уровня

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

Нам также нужно инициализировать многие из этих вещей, например, создать сетку, установить размер окна для игры и убедиться, что мышь видна, чтобы мы могли нажимать на ячейки. Но все эти вещи относятся к конкретному движку и не очень интересны, поэтому мы пропустим их и перейдем к хорошему. (Конечно, если вы следуете в XNA, вы можете скачать исходный код, чтобы получить все подробности.)

Теперь, когда у нас все настроено и готово к работе, мы можем просто запустить игру! Но не так быстро, потому что есть проблема: мы ничего не можем сделать, потому что игра всегда запущена. По сути, невозможно нарисовать определенные фигуры, потому что они будут разбиваться на части, когда вы рисуете их, поэтому нам нужно иметь возможность приостановить игру. Было бы также хорошо, если бы мы могли очистить сетку, если она станет беспорядочной, потому что наши творения часто выходят из-под контроля и оставляют беспорядок позади.

Итак, давайте добавим некоторый код для приостановки игры при каждом нажатии пробела и очистим экран, если нажата клавиша Backspace:

Также было бы полезно, если бы мы очень четко дали понять, что игра была приостановлена, поэтому, когда мы пишем наш метод Draw, давайте добавим некоторый код, чтобы фон стал красным, и напишем "Paused" в фоновом режиме:

Это оно! Теперь все должно работать, так что вы можете сделать это, нарисовать несколько форм жизни и посмотреть, что произойдет! Откройте для себя интересные шаблоны, которые вы можете сделать, снова обратившись к странице Википедии. Вы также можете поиграть с частотой кадров, размером ячейки и размерами сетки, чтобы настроить его по своему вкусу.


Добавление улучшений

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

Начнем с определения частоты обновления и частоты кадров отдельно в основном классе:

Теперь, при инициализации игры, используйте частоту кадров (FPS), чтобы определить, как быстро она будет считывать ввод с мыши и рисовать, что должно быть как минимум ровным 60 FPS как минимум:

Затем добавьте таймер в ваш класс Grid, чтобы он обновлялся только тогда, когда это необходимо, независимо от частоты кадров:

Теперь у вас должна быть возможность запускать игру с любой скоростью, которую вы хотите, даже с очень медленными 5 обновлениями в секунду, чтобы вы могли внимательно наблюдать за разворачиванием симуляции, в то же время имея возможность рисовать красивые плавные линии с постоянной частотой кадров.


Заключение

Теперь у вас в руках гладкая и функциональная Игра Жизни, но если вы захотите изучить ее дальше, вы всегда сможете добавить к ней больше настроек. Например, сетка в настоящее время предполагает, что за ее пределами все мертво. Вы можете изменить его так, чтобы сетка вращалась, так что планер летал бы вечно! В этой популярной игре нет недостатка в вариациях, так что дайте волю своему воображению.

Спасибо за прочтение, надеюсь, вы узнали что-то полезное сегодня!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.