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

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

На этой неделе я работал над базовой структурой моей игры. Хотя на данный момент это выглядит довольно грубо, он сделан таким образом, что пользователи могут настраивать и изменять ИИ по своему усмотрению. Например, они могут захотеть, чтобы их турели атаковали точку:

Вместо этого они могут захотеть, чтобы турели атаковали только ближайших врагов.

Или они могут просто захотеть, чтобы их турели просто вращались, как вертолет, выпуская собственные пули по врагам.

Игроки также могут настраивать ИИ и переключаться между ними, нажимая на отдельные турели или нажимая кнопку. Кроме того, снаряды, загруженные в каждую турель, тоже полностью настраиваются.

Реализация: ООП и делегаты

Я подумал, что это идеальное время для практики ООП (объектно-ориентированного программирования). На данный момент у меня есть две отдельные базы данных JSON: одна для турелей, а другая для снарядов. Пользовательский интерфейс (который в настоящее время практически отсутствует) принимает пользовательские входные данные и запрашивает параметры в базах данных. Параметры передаются в класс Turret, и класс инициализируется с использованием этих параметров. В зависимости от параметров класс Turret добавляет к себе необходимые компоненты, такие как атака, обработка ИИ или вращение. Каждый компонент зависит только от класса турели и ничего больше.

Полагаю, вот как работает ООП? Большую часть недели у меня ушло на выяснение того, как будет работать система ИИ. Я хотел, чтобы игроки могли полностью настраивать ИИ своих турелей. Это означало, что каждое действие ИИ должно быть полностью независимым, чтобы каждая команда хорошо взаимодействовала друг с другом, независимо от того, в каком порядке они находятся.

Гибкая настройка AI

Поведение ИИ турели может в широком смысле относиться как к «нацеливанию», так и «атаке».

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

Найдя врага, башня тоже должна его атаковать. Однако есть разные варианты атакующего поведения. Например, когда игрок хочет, чтобы башня «нашла ближайшего врага, а затем атакует, пока он не умрет», тогда башня должна быть известна, чтобы переопределить команду «найти ближайшего врага» командой «Атака» до тех пор, пока враг не умрет.

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

Чтобы это стало возможным, я разбил команды на минимально возможные компоненты. Например, команда «найти ближайшего врага» была разделена на три функции: функция, которая находит всех врагов в зоне действия датчика, функция, которая находит всех видимых врагов, и функция, которая находит ближайшего врага. Каждый компонент полностью независим и может использоваться повторно. Если я хочу создать новую команду, например «найти случайного врага», единственное, что мне нужно сделать, это создать функцию, которая вызывает эти отдельные функции.

Я пробовал много (ошибочных) способов сделать все мои функции полностью независимыми, включая статические функции, списки, которые принимают список и возвращают список в сочетании с очередью сопрограмм (... другие различные глупые подходы ...) и, наконец, заканчивая делегатами со словарем счетчиков.

Списки внутри списков

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

  1. «Найди всех врагов в пределах досягаемости»
  2. «Найти всех врагов видимыми»
  3. «Найди ближайшего врага»

В цикле сопрограмм они будут загружены, используя что-то в строках: List [i] (List [i-1]).

Список один будет вызывать пустой список (или последний список цикла), возвращая список со всеми врагами в диапазоне. Список два будет вызывать список два, возвращая список со всеми видимыми врагами. Список три будет вызывать. в списке два, возвращая список с ближайшим противником как [0] списка. И так далее.

Пока результатом я во многом остался доволен. На тот момент у этого подхода было еще множество проблем.

  • Он все еще был недостаточно гибким. Не было возможности реализовать такие вещи, как «Ждать / Удерживать» ВНУТРИ самих функций, поскольку они должны были быть выполнены с использованием сопрограммы (или, может быть, они не должны были?), И код довольно быстро испачкался и….
  • Как перевести список команд плеера в список функций?

На тот момент я даже не знал о делегатах. Один из способов - просто использовать строки списка и операторов if / switch, но этот подход определенно не будет устойчивым в будущем. Не говоря уже об неэлегантности. фу.

Делегаты - колени пчелы

Я погуглил и обнаружил, что существуют Func и Action (которые являются типами делегатов). Узнав о них, мой код стал намного чище и эффективнее. Мне было весело переписывать другие старые коды с помощью делегатов. Если вы не знаете, что это такое, и хотели бы узнать о них больше, я настоятельно рекомендую посмотреть это отличное видео:

Https://www.youtube.com/watch?v=G5R4C8BLEOc

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

Поскольку я использую Ienumerators, все команды полностью содержатся внутри себя. Каждое состояние выполняет свою связанную функцию один раз, затем переключается на следующее состояние и так далее. Это означает, что реализовать переопределения ИИ, такие как «Атакуй, пока цель не умрет», очень просто. Все, что мне нужно было сделать, это создать функцию, которая создавала бы цикл сопрограмм внутри цикла сопрограмм до тех пор, пока условие не будет выполнено.

Объединение объектов

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

Http://catlikecoding.com/unity/tutorials/object-pools/

Https://unity3d.com/learn/tutorials/topics/scripting/object-pooling

Что дальше?

На следующей неделе я подумываю нарисовать концепт-арт и опробовать некоторые черновики пользовательского интерфейса. Я также подумываю о внедрении менеджеров одиночных игр, которые должны быть достаточно простыми и соответствующей системой сохранения. Если у вас есть какие-либо вопросы, не стесняйтесь задавать мне @LLHorizon в Твиттере!