Статьи

AI игры: боты наносят ответный удар!

Ниже приведен небольшой отрывок из нашей новой книги « Игры HTML5: от новичка до ниндзя» , написанной Эрлом Кастледином. Доступ к книге включен в членство SitePoint Premium, или вы можете получить копию в магазинах по всему миру. Вы можете проверить бесплатный образец первой главы здесь .

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

Искусственный интеллект — огромная и чрезвычайно сложная область. К счастью, мы можем получить впечатляющие результаты даже с гораздо более искусственным, чем интеллект . Пара простых правил (в сочетании с нашим старым другом Math.random ) может дать сносную иллюзию намерения и мысли. Он не должен быть слишком реалистичным, если он поддерживает нашу игровую механику и доставляет удовольствие.

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

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

Умышленное движение

Выбирать, как спрайты перемещаются в игре, очень весело. Функция update — ваш пустой холст, и вы получаете божественный контроль над своими сущностями. Что не нравится в этом!

То, как движется сущность, определяется тем, насколько мы изменяем ее координаты x и y каждом кадре («двигайте все чуть-чуть!»). До сих пор мы перемещали вещи в основном по прямым линиям с помощью pos.x += speed * dt . При добавлении скорости (умноженной на дельту) спрайт перемещается вправо. Вычитание перемещает его влево. Изменение координаты y перемещает его вверх и вниз.

Чтобы сделать прямые линии более увлекательными, добавьте немного тригонометрии. Используя pos.y += Math.sin(t * 10) * 200 * dt , спрайт качается вверх и вниз через синусоидальную волну. t * 10 — частота волны. Время в секундах от нашей системы обновлений, поэтому оно всегда увеличивается линейно. Предоставление этого Math.sin производит гладкую синусоидальную волну. Изменение множителя изменит частоту: меньшее число будет колебаться быстрее. 200амплитуда волн.

Вы можете комбинировать волны, чтобы получить еще более интересные результаты. Допустим, вы добавили еще одну синусоидальную волну в позицию y: pos.y += Math.sin(t * 11) * 200 * dt . Это почти точно так же, как первый, но частота изменяется очень незначительно. Теперь, когда две волны усиливают и компенсируют друг друга, когда они входят и выходят из фазы, сущность подпрыгивает вверх и вниз все быстрее и медленнее. Сдвиг частоты и амплитуды много может дать некоторые интересные образцы подпрыгивания. Измените положение x с помощью Math.cos и у вас Math.cos круги.

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

Waypoints

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

Бомбы, следующие за путевой точкой FlyMaze Франко Понтичелли

Чтобы мы могли сосредоточиться на концепциях путевых точек, мы представим летающего плохого парня, который не ограничен стенами лабиринта. Самый страшный летающий враг — это комар (это самое смертоносное животное в мире после людей). Но не очень жуткий . Мы пойдем с «летучей мышью».

Летучие мыши не будут сложными животными; они будут непредсказуемыми. У них будет просто одна путевая точка, к которой они летят. Когда они доберутся туда, они выберут новую путевую точку. Позже (когда мы пройдем по лабиринту) мы рассмотрим наличие нескольких структурированных путевых точек. На данный момент летучие мыши колеблются от точки к точке, как правило, неприятность для игрока.

Чтобы создать их, создайте новую сущность на основе TileSprite , названную Bat , в entities/Bat.js Летучим мышам нужно несколько умов, чтобы выбрать желаемую путевую точку. Это может быть функция, которая выбирает случайное место в любом месте экрана, но чтобы сделать их немного более грозными, мы дадим им функции findFreeSpot , поэтому путевая точка всегда будет плиткой, по которой может путешествовать игрок:

 const bats = this.add(new Container()); for (let i = 0; i < 5; i++) { bats.add(new Bat(() => map.findFreeSpot())) } 

У нас есть новый Container для летучих мышей, и мы создаем пять новых. Каждый получает ссылку на нашу функцию выбора путевых точек. При вызове он запускает map.findFreeSpot и находит пустую ячейку в лабиринте. Это становится новой точкой пути летучей мыши:

 class Bat extends TileSprite { constructor(findWaypoint) { super(texture, 48, 48); this.findWaypoint = findWaypoint; this.waypoint = findWaypoint(); ... } } 

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

 // Move in the direction of the path const xo = waypoint.x - pos.x; const yo = waypoint.y - pos.y; const step = speed * dt; const xIsClose = Math.abs(xo) <= step; const yIsClose = Math.abs(yo) <= step; 

Как мы «движемся» к чему-то и как мы узнаем, достаточно ли мы «близки»? Чтобы ответить на оба эти вопроса, мы сначала найдем разницу между местоположением путевой точки и битой. Вычитание значений x и y путевой точки из положения летучей мыши дает нам расстояние по каждой оси. Для каждой оси мы определяем «достаточно близко», чтобы обозначить Math.abs(distance) <= step . Использование step (которое основано на speed ) означает, что чем быстрее мы путешествуем, тем дальше мы должны быть «достаточно близко» (чтобы мы не пролетали вечно).

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

 if (!xIsClose) { pos.x += speed * (xo > 0 ? 1 : -1) * dt; } if (!yIsClose) { pos.y += speed * (yo > 0 ? 1 : -1) * dt; } 

Чтобы двигаться в направлении путевой точки, мы разделим движение на две части. Если мы не слишком близки в направлениях x или y , мы перемещаем объект к путевой точке. Если призрак находится над путевой точкой ( y > 0 ), мы перемещаем его вниз, в противном случае мы перемещаем его вверх — и то же самое для оси x . Это не дает нам прямой линии (которая появляется, когда мы начинаем стрелять в игрока), но она приближает нас к путевой точке каждого кадра.

 if (xIsClose && yIsClose) { // New way point this.waypoint = this.findWaypoint(); } 

Наконец, если горизонтальные и вертикальные расстояния достаточно близки, летучая мышь прибыла к месту назначения, и мы переназначаем this.waypoint в новое место. Теперь летучие мыши бессмысленно бродят по залам, как и следовало ожидать.

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

Перемещение и стрельба по направлению к цели

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

Предоставить информацию о местоположении игрока плохим парням довольно просто: это просто player.pos ! Но как мы можем использовать эту информацию, чтобы посылать вещи в определенном направлении? Ответ, конечно, тригонометрия!

 function angle (a, b) { const dx = ax - bx; const dy = ay - by; const angle = Math.atan2(dy, dx); return angle; } 

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

Таким же образом, как мы реализовали math.distance , нам сначала нужно получить разницу между двумя точками ( dx и dy ), а затем мы используем встроенный арктангенсный математический оператор Math.atan2 чтобы получить угол, созданный между двумя векторами. , Обратите внимание, что atan2 принимает разницу y как первый параметр, а x как второй. Добавьте функцию angle в utils/math.js

Большую часть времени в наших играх мы будем искать угол между двумя объектами (а не точками). Поэтому нас обычно интересует угол между центром сущностей, а не их верхние левые углы, как определено pos . Мы также можем добавить функцию угла в utils/entity.js , который сначала находит центры двух сущностей, а затем вызывает math.angle :

 function angle(a, b) { return math.angle(center(a), center(b)); } 

Функция angle возвращает угол между двумя положениями в радианах. Используя эту информацию, мы можем теперь вычислить суммы, которые мы должны изменить, чтобы изменить позицию x и y объекта, чтобы двигаться в правильном направлении:

 const angleToPlayer = entity.angle(player.pos, baddie.pos); pos.x += Math.cos(angle) * speed * dt; pos.y += Math.sin(angle) * speed * dt; 

Чтобы использовать угол в своей игре, помните, что косинус угла — это то, как далеко вдоль оси x вам нужно двигаться при перемещении одного пикселя в направлении угла. А синус угла — это то, как далеко вдоль оси y нужно двигаться. Умножая на скалярное ( speed ) количество пикселей, спрайт перемещается в правильном направлении.

Знание угла между двумя вещами оказывается очень важным в игре. Зафиксируйте это уравнение в памяти, так как вы будете его часто использовать. Например, теперь мы можем стрелять прямо в вещи — так что давайте сделаем это! Создайте спрайт Bullet.js будет действовать как снаряд:

 class Bullet extends Sprite { constructor(dir, speed = 100) { super(texture); this.speed = speed; this.dir = dir; this.life = 3; } } 

Bullet будет небольшим спрайтом, который создается с позицией, скоростью (скоростью и направлением) и «жизнью» (по умолчанию это три секунды). Когда жизнь достигнет 0, пуля станет dead … и мы не закончим с миллионами пуль, летящих к бесконечности (точно так же, как наши пули из Главы 3).

 update(dt) { const { pos, speed, dir } = this; // Move in the direction of the path pos.x += speed * dt * dir.x; pos.y += speed * dt * dir.y; if ((this.life -= dt) < 0) { this.dead = true; } } 

Отличие от наших пуль в главе 3 состоит в том, что они теперь движутся в направлении, указанном при его создании. Поскольку x и y будут представлять угол между двумя объектами, пули будут стрелять по прямой линии к цели — которая будет нами.

Пули не будут загадочно появляться из воздуха. Что-то должно их уволить. Нам нужен еще один новый плохой парень! Мы развернем пару часовых в виде тотемов . Тотемы — стражи подземелий, которые следят за миром из центра лабиринта, уничтожая любых главных героев, крадущих сокровища.

Тотемы цилиндра: Топемы.

Totem.js генерирует Bullets и запускает их в сторону Player . Поэтому им нужна ссылка на игрока (они не знают, что это игрок, они просто думают об этом как о target ) и функция, которую нужно вызвать, когда пришло время генерировать пулю. Мы назовем это onFire и передадим его с GameScreen чтобы GameScreen не нужно было беспокоиться о Bullets :

 class Totem extends TileSprite { constructor(target, onFire) { super(texture, 48, 48); this.target = target; this.onFire = onFire; this.fireIn = 0; } } 

Когда создается новый Totem , ему назначается цель и ему предоставляется функция для вызова при стрельбе из Bullet . Функция добавит пулю в основной игровой контейнер, чтобы ее можно было проверить на столкновения. Теперь Bravedigger должен избегать Bats and Bullets . Мы переименуем контейнер в baddies потому что логика столкновений одинакова для обоих:

 new Totem(player, bullet => baddies.add(bullet))) 

Чтобы вывести сущность на экран, она должна войти в Container для включения в наш граф сцены. Есть много способов сделать это. Мы могли бы сделать наш основной объект GameScreen глобальной переменной и вызывать gameScreen.add из любого места. Это будет работать, но это не хорошо для инкапсуляции информации. Передавая функцию, мы можем указать только те способности, которые мы хотим, чтобы Totem выполнял. Как всегда, все зависит от вас.

Предупреждение: в нашей логике Container есть скрытый гоч. Если мы добавим сущность в контейнер во время собственного вызова update этого контейнера, сущность не будет добавлена! Например, если Totem был внутри baddies и он пытался добавить новую пулю и к baddies , пуля не появлялась. Посмотрите код для Container и посмотрите, можете ли вы понять, почему. Мы рассмотрим этот вопрос в главе 9, в разделе «Циклирование по массивам».

Когда тотем должен стрелять в игрока? Случайно, конечно! Когда пришло время стрелять, переменная fireIn будет установлена ​​на обратный отсчет. Во время обратного отсчета тотем имеет небольшую анимацию (переключение между двумя кадрами). В игровом дизайне это называется телеграфированием — тонкое визуальное указание игроку, что ему лучше быть на высоте. Без телеграфа наши тотемы внезапно и случайно стреляли бы в игрока, даже когда они действительно близко. У них не будет шансов увернуться от пуль, и они будут чувствовать себя обманутыми и раздраженными.

 if (math.randOneIn(250)) { this.fireIn = 1; } if (this.fireIn > 0) { this.fireIn -= dt; // Telegraph to the player this.frame.x = [2, 4][Math.floor(t / 0.1) % 2]; if (this.fireIn < 0) { this.fireAtTarget(); } } 

В каждом кадре, который тотем будет стрелять, есть шанс один на 250. Когда это правда, обратный отсчет начинается на одну секунду. После обратного fireAtTarget метод fireAtTarget выполнит тяжелую работу по вычислению траектории, необходимой для fireAtTarget снаряда в цель:

 fireAtTarget() { const { target, onFire } = this; const totemPos = entity.center(this); const targetPos = entity.center(target); const angle = math.angle(targetPos, totemPos); ... } 

Первые шаги — получить угол между целью и тотемом, используя math.angle . Мы могли бы использовать вспомогательный entity.angle (который вызывает нас для entity.center ), но нам также нужна центральная позиция тотема, чтобы правильно установить начальную позицию маркера:

 const x = Math.cos(angle); const y = Math.sin(angle); const bullet = new Bullet({ x, y }, 300); bullet.pos.x = totemPos.x - bullet.w / 2; bullet.pos.y = totemPos.y - bullet.h / 2; onFire(bullet); 

Получив угол, мы используем косинус и синус, чтобы вычислить компоненты направления. (Хм, опять: возможно, вы хотели бы превратить это в другую математическую функцию, которая сделает это за вас?) Затем мы создадим новую Bullet которая будет двигаться в правильном направлении.

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

Примечание: в этой книге мы видели много маленьких механик, которые иллюстрируют различные концепции. Не забывайте, что игровая механика гибкая. Их можно повторно использовать и комбинировать с другими механиками, элементами управления или графикой, чтобы создавать еще больше игровых идей и игровых жанров! Например, если вы комбинируете «щелчок мышью» с «путевыми точками» и «огнем в сторону», у нас есть базовая игра защиты башни! Создайте путь для врагов, по которому нужно следовать: щелкнув мышью, вы math.distance турель (которая использует math.distance для поиска ближайшего врага), а затем выстрелите в нее.

Умные Плохие Парни: Нападение и Уклонение

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

Скелеты на работе - и отдых - в Mozilla BrowserQuest

Один из способов моделирования этих желаний — через конечный автомат . Конечный автомат управляет изменениями поведения между заданным числом состояний. Различные события могут вызвать переход из текущего состояния в новое состояние. Состояния будут характерны для игры, такие как «бездействие», «прогулка», «атака», «остановка для мороженого». Вы не можете атаковать и останавливаться для мороженого. Реализация конечных автоматов может быть такой же простой, как и сохранение переменной состояния, которую мы ограничиваем одним элементом из списка. Вот наш начальный список возможных состояний летучей мыши (определенный в файле Bat.js ):

 const states = { ATTACK: 0, EVADE: 1, WANDER: 2 }; 

Примечание. Нет необходимости определять состояния в таком объекте, как этот. Мы могли бы просто использовать строки «ATTACK», «EVADE» и «WANDER». Использование такого объекта позволяет нам просто организовать свои мысли — перечисляя все возможные состояния в одном месте — и наши инструменты могут предупредить нас, если мы допустили ошибку (например, присвоение несуществующего состояния). Строки в порядке, хотя!

В любой момент летучая мышь может находиться только в одном из состояний ATTACK , EVADE или WANDER . Атакующий будет летать на игрока, уклоняться — летать прямо от игрока, а блуждающий мелькает в случайном порядке. В конструкторе функции мы назначим начальное состояние ATTACK ing: this.state = state.ATTACK . Внутри update мы переключаем поведение в зависимости от текущего состояния:

 const angle = entity.angle(target, this); const distance = entity.distance(target, this); if (state === states.ATTACK) { ... } else if (state === states.EVADE) { ... } else if (state === states.WANDER) { ... } 

В зависимости от текущего состояния (и в сочетании с расстоянием и углом к ​​игроку) Bat может принимать решения о том, как она должна действовать. Например, если он атакует, он может двигаться прямо к игроку:

 xo = Math.cos(angle) * speed * dt; yo = Math.sin(angle) * speed * dt; if (distance < 60) { this.state = states.EVADE; } 

Но оказывается, что наши летучие мыши частично заняты: когда они подходят слишком близко к цели (в пределах 60 пикселей), состояние переключается в state.EVADE . state.EVADE . Уклонение работает так же, как атака, но мы уменьшаем скорость, чтобы они улетали прямо от игрока:

 xo = -Math.cos(angle) * speed * dt; yo = -Math.sin(angle) * speed * dt; if (distance > 120) { if (math.randOneIn(2)) { this.state = states.WANDER; this.waypoint = findFreeSpot(); } else { this.state = states.ATTACK; } } 

Уклоняясь, летучая мышь постоянно обдумывает свой следующий ход. Если он находится достаточно далеко от игрока, чтобы чувствовать себя в безопасности (120 пикселей), он переоценивает свою ситуацию. Возможно, он хочет атаковать снова, или, возможно, он хочет уйти в сторону случайной точки пути.

Когда летучие мыши нападают

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

Примечание: пример этого в Minecraft. Животные предназначены, чтобы уклоняться после получения урона. Если вы атакуете корову, она будет бежать всю жизнь (так что охота более сложна для игрока). Волки в игре также имеют состояние АТАКА (потому что они волки). Непреднамеренный результат этих конечных автоматов заключается в том, что иногда можно увидеть волков, вовлеченных в стремительную охоту на овец! Это поведение не было явно добавлено, но оно возникло в результате объединения систем.

Более величественный государственный аппарат

Конечные автоматы часто используются при организации игры — не только в искусственном интеллекте. Они могут контролировать синхронизацию экранов (например, диалогов «GET READY!»), Устанавливать темп и правила игры (например, управлять временем охлаждения и счетчиками) и очень полезны для разбивки любого сложного поведения на маленькие, многоразовые кусочки. (Функциональность в разных состояниях может совместно использоваться разными типами объектов.)

Работа со всеми этими состояниями с независимыми переменными и предложения if … else могут стать громоздкими. Более мощный подход состоит в том, чтобы абстрагировать конечный автомат в его собственный класс, который можно повторно использовать и расширять дополнительными функциями (например, запоминанием того состояния, в котором мы находились ранее). Это будет использоваться в большинстве игр, которые мы делаем, поэтому давайте создадим для него новый файл с именем State.js и добавим его в библиотеку Pop:

 class State { constructor(state) { this.set(state); } set(state) { this.last = this.state; this.state = state; this.time = 0; this.justSetState = true; } update(dt) { this.first = this.justSetState; this.justSetState = false; ... } } 

Класс State будет содержать текущее и предыдущее состояния, а также помнит, как долго мы были в текущем состоянии . Он также может сказать нам, является ли это первый кадр, в котором мы были в текущем состоянии. Это делается с помощью флага ( justSetState ). Каждый кадр мы должны обновлять объект state (так же, как мы делаем с нашими MouseControls ), чтобы мы могли делать расчеты времени. Здесь мы также устанавливаем first флаг, если это первое обновление. Это полезно для выполнения задач инициализации состояния, таких как сброс счетчиков.

 if (state.first) { // just entered this state! this.spawnEnemy(); } 

Когда состояние установлено (через state.set("ATTACK") ), свойство first будет установлено в true . Последующие обновления будут сбрасывать флаг на false . Дельта-время также передается в update чтобы мы могли отслеживать время, в течение которого текущее состояние было активным. Если это первый кадр, мы сбрасываем время на 0; в противном случае мы добавляем dt :

 this.time += this.first ? 0 : dt; 

Теперь мы можем модифицировать наш пример chase-evade-wander, чтобы использовать конечный автомат, и удалить наше гнездо из if s:

 switch (state.get()) { case states.ATTACK: break; case states.EVADE: break; case states.WANDER: break; } state.update(dt); 

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

 case states.WANDER: if (state.first) { this.waypoint = findFreeSpot(); } ... break; } 

Обычно хорошей идеей является выполнение задач инициализации в кадре state.first , а не при переходе из предыдущего кадра. Например, мы могли бы установить путевую точку, как мы сделали state.set("WANDER") . Если логика состояния автономна, ее проще проверить. Мы можем установить для Bat значение по умолчанию this.state = state.WANDER и знать, что путевая точка будет установлена ​​в первом кадре обновления.

Есть несколько других удобных функций, которые мы добавим в State.js для запроса текущего состояния:

 is(state) { return this.state === state; } isIn(...states) { return states.some(s => this.is(s)); } 

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

 if (state.isIn("EVADE", "WANDER")) { // Evading or wandering - but not attacking. } 

Состояния, которые мы выбираем для объекта, могут быть настолько детальными, насколько это необходимо. У нас могут быть состояния «BORN» (когда объект создается впервые), «DYING» (когда он поражен и оглушен) и «DEAD» (когда все кончено), что дает нам дискретные места в нашем классе для обработки логики и анимационный код.

Управление ходом игры

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

GameScreen автомат может разбить основную логику в обновлении GameScreen на части, такие как «ГОТОВ», «ВОСПРОИЗВЕДЕНИЕ», «ИГРА». Это проясняет, как мы должны структурировать наш код и как будет развиваться общая игра. Нет необходимости обрабатывать все в функции update ; оператор switch может отправлять другие методы. Например, весь код состояния «PLAYING» может быть сгруппирован в функцию updatePlaying :

 switch(state.get()) { case "READY": if (state.first) { this.scoreText.text = "GET READY"; } if (state.time > 2) { state.set("PLAYING"); } break; case "PLAYING": if (entity.hit(player, bat)) { state.set("GAMEOVER"); } break; case "GAMEOVER": if (controls.action) { state.set("READY"); } break; } state.update(dt); 

GameScreen запустится в состоянии READY и отобразит сообщение «GET READY». Через две секунды ( state.time > 2 ) он переходит в «PLAYING» и игра продолжается. Когда игрок получает удар, состояние переходит в «GAMEOVER», где мы можем подождать, пока не будет нажата клавиша пробела, прежде чем начинать заново.