Simple Enemy AI System for Unity3D
In this post I am going to explain how I have developed a simple AI system for the enemies of my final project on technical college. The idea is a simple script that made the enemy’s walks randomly on the on the scene, chase and attack the player.
The IA script that I developed is based on several games, especially in stealth games. The enemy will select a random destination when the game is started and will walking in this direction, when he comes close to the selected random destination, and another random destination is selected and it starts all the process again. If while he is walking toward a point he sees the player, he starts chasing the player and if he reaches the player, he is going to attack attack him.
For this example I will use Unity 5 game engine but you can easily make changes and use in other engines like Unreal.
I will not explain basics features of Unity in this post or programming logic, If you have any question please read the reference links or ask me here or on my Twitter.
First Step: Build Scene
For this example I have build a simple maze using 3D blocks and positioned the enemy(blue sphere) on the right top corner and our player on lower left corner (red sphere).
Our player its a simple FPSController (You can find this script on Unity Standard Assets), read more here.
Second Step: Create Navigation Mesh
For our enemy walk on the scene the easiest method is create a navigation mesh on the scene and add a Nav Mesh Agent on the components of our enemy (3D sphere).
“The navigation system allows you to create characters that can intelligently move around the game world, using navigation meshes that are created automatically from your Scene geometry.“
Pay atention on your navigation mesh, it must be bypassing all walls, if not your enemy will not work correctly.
This simple step will make the next things easy.
Third Step: Our script
To start we need to declare and define our random destination using Random.range function and get navMeshAgent component. After this our code looks like this:
If you will use annimator component to use animations on the enemy you should declare and use on the void start too, like this:
On void update the first thing we need to do is calculate player distance and the random point distance, for this we use Vector3.
And declare a RaycastHit, this raycasthit will will ensure that our enemy does not see the player behind walls.
You can read about Physics.Raycast here.
And our walk method is basically set navMesh acceleration, speed and destination, like this:
And now with RaycastHit, player distance and random point distance our script is making something like this:
The orange dotted thick line it’s our script calculating the player distance, the blue dotted line it’s the random point distance and the green arrow is our RaycastHit.
And here the other 3 behaviors of our enemy:
If you are working with animator and animations you can call your animator variables here, like this example:
The next steps is make a lot of checkings to attack, chase, stop chasing and all that stuffs, I will not going into details, but if you have doubts you can contact me as I told above.
Here’s the next checkings:
And the attack checking:
If you want to cause damage on player you should add something like this:
I really recommend your playerLife be a variable of an script of the player.
Finally, if you want that’s your player can attack the enemy you can add rigidBody component to your enemy and make this:
And now we finished our simple artificial intelligence script and you can use for your simple game, study and improve, in a few months I will post the second part explaining how to improve this script, adding field of vision, sounds and etc..
You can view the full script/project on my github, and sorry for the quality of the project/code, I was 16 years old when I wrote this.
Пишем ИИ для игры. Часть 1: Как найти противников в поле зрения
Всем привет. С сегодняшнего дня я решил начать написание цикла статей по работе с ИИ противника. В ходе данного цикла я хочу показать на простых примерах, как можно сделать ИИ в игре, который будет учитывать поле зрения противника, уровень издаваемого шума, умеет патрулировать местность, ставить приоритетные цели и обладать различными типами поведения.
В конце цикла статей наш ИИ будет уметь:
- Получать список целей по FOV;
- Искать путь до ближайшей цели, будь то враг или что-то другое;
- Обладать типом (враг, союзник, болванка);
- Обладать поведением (патруль, поиск цели, бой, следование, убегание);
- Учитывать уровень шума и освещенности;
- Работать с инверсной кинематикой (получать Impact конечностей, смотреть на врагов);
В сегодняшней статье я расскажу о простой реализации поля зрения противника. Дабы исключить архитектурные особенности — мы сделаем все на обычных монобехах.
Итак, начнем с того, что должно делать наше поле зрения:
- Находить цели, которые попадают в угол обзора;
- Ставить приоритет на ближайшую цель в области обзора;
- При выходе текущей цели из поля обзора — сохранять её до определенной дистанции;
- При полной потере цели — переключиться на другие;
Начнем с того, что нам нужен некий интерфейс, который сможет просто обрабатывать наше поле зрения:
Рассмотрим, что содержит наш интерфейс:
- Параметры Radius, Angle — для того, чтобы получить возможность узнать информацию о FOV;
- Параметр CurrentTarget (в моем случае для простоты используется Transform, но лучше сделать интерфейс ITarget и работать с ним);
- Методы для проверки целей — HasTargets, GetAllTargets, GetNearestTarget и ForceRecalculate.
Почему здесь нет метода поиска целей, а сделан только ForceRecalculate? Мы делаем просчет внутри самого компонента FOV, а все его данные получаем через его методы обработки целей. ForceRecalculate нужен нам только тогда, когда к примеру текущая цель умерла и хочет оповестить наш объект об этом.
Теперь приступим к самой реализации FOV. По своей сути он работает через оверлап коллайдера, однако вы можете использовать Raycast. Также у компонента FOV есть таймер пересчета целей и чем меньше он будет, тем больше будет нагрузки и тем выше точность поиска. Таймер полезен тогда, когда игрок непосредственно видит ИИ противника и для него выставляется наименьший таймер, а для противников в далеке — наибольший.
Теперь разберем подробнее составляющие кода:
- В методе Start() мы запускаем наш счетчик проверки FieldOfView через интервал. В моем случае используется UniRx, но вы можете сделать реализацию таймера по-другому.
- Метод FieldOfViewCheck() запускает процесс проверки целей внутри поля зрения. Если изначально у нас нет никаких целей — мы проверяем есть ли кто-то по нужному слою в физике, затем смотрим ближайшую цель и добавляем её в список. Если же цель есть — мы смотрим дистанцию до неё вне зависимости от поля зрения и если главная цель слишком далеко — пересчитываем снова список целей.
- Дополнительные методы GetAllTargets, GetNearestTarget, HasTargets и ForceRecalculate служат вспомогательными. Они могут использоваться в нашем контроллере ИИ.
Теперь мы научили нашего противника определять ближайшую цель в поле зрения:
Так же важно, что если цель покинет поле зрения, то герой останется на ней сфокусирован, пока её дистанция не увеличиться больше допустимой, в этом случае он опять будет переключаться на ближайшую цель в поле зрения. Так же нужно учесть, что пока здесь не реализовано поведение, которое учитывает урон от ближайших целей (о чем мы поговорим в следующих частях цикла).
Проверка поля зрения позволяет нам убедиться, что наш ИИ вообще видит кого-то перед собой, цель не перекрывается и задать приоритет по целям исходя из дистанции. В дальнейшем, мы будем комбинировать FieldOfView с показателями шума, издаваемого целями, а также задавать поведение для нашего ИИ.
Следующая часть статьи будет включать в себя обработку целей в зависимости от паттерна поведения нашего ИИ, а также же учитывать инверсную кинематику для того, чтобы наш герой поворачивал голову и туловище к ближайшей цели.
Буду рад пообщаться на эту тему и послушать о ваших реализациях FOV.
if (!Physics.Raycast(transform.position, directionToTarget, distanceToTarget,
_obstructionMask))
<
isSeeTarget = true;
>
else
isSeeTarget = false;
Ну серьезно?
isSeeTarget = !Physics.Raycast(transform.position, directionToTarget, distanceToTarget, _obstructionMask) же.
Там еще флаги изначально стояли, я их вырезал и забыл поменять
К коду в целом очень много вопросов. Если это написано чтобы любой джун понял — то ок, но если вы например Vector3.Distance используете только для сравнения в продакшене (вместо сравнения квадратов) то даже не знаю(
Естественно для упращения. А листы Transform вечно обновляемые не смущают? 😀
Если для упрощения, то возможно стоит добавить комментарии в код, потому что мы-то прочитаем, а целевая аудитория испугается, либо, чего хуже, скопипастит решение.
И без подсветки синтаксиса тяжеловато, но тут наверно ничего не поделать.
Интерфейс IFOV выполняет несколько задач. Там пробел в одну строку как раз там где следует его порезать на два разных интерфейса. Тот что является Fov реализацией и тот что отвечает за поиск целей в реализаций.
Есть версия overlap sphere non alloc. Текущаягенерит каждый раз новый лист, даже если он ре нужен. Еще, не уверен до конца, но вроде бы вызов этих методов следует в FixedUpdate засунуть, но это не точно.
IsSeeTarget просто не нужен — добавляй в список и все. Код чище будет.
Не нравится проверка на попадание в угол зрения — normalized — высчитывает корень квадратный, лучше бы этого избежать.
GetNearestTarget делает расчеты при каждом вызове. Следует хранить таргеты в отсортированном по дистанции виде и просто возвращать первый.
Со всем согласен, кроме GetNearestTarget. Позиция ближайшего может измениться на следующий вызов метода, так что его следует пересчитать. В целом это просто на понимание как это работает, код естественно написан за 5 минут без всякой оптимизации
Очень грубое решение задачи в лоб, но как пример для тех кто не делал ИИ никогда — сойдет. Такое можно использовать в первом приближении новичку понимая что это не оптимальный код и нужна оптимизация. Кстати делать какие либо проверки визуального контакта с игроком когда он на большем расстоянии чем может видеть ИИ не имеет смысла даже раз в минуту. В данном случае оверлап используется именно для определения того что игрок не спрятался и находится в поле зрения. Также скрипт который будут вешать на всех врагов должен использовать подсистему ИИ, которая и должна будет уже делать все самые сложные расчеты выбирая для них только те объекты которые могут встретится хотя бы теоретически. Но это уже способы оптимизации и они будут важны для конкретных случаев использования ИИ. Будем ждать продолжения. Спасибо за статью. Тем более тоже было желание подобное написать, но так и не определился для какой аудитории.
Рейкаст на проверку видимости кидается из пивота объекта, в данном случае из точки на земле (видно на скриншоте). Если персонаж будет стоять рядом с ящиком или забором, который ему по пояс, он "не увидит" врага, стоящего за ним, потому что рейкас из ног будет сразу упираться в препятствие.
Как вариант можно добавить чайлд-пустышку на уровне глаз персонажа и кидать рейкаст из него. При таком подходе пивот может быть где угодно и не повлияет на качество проверки рейкаста.
Создание простого ИИ на C# в Unity
Почти любой игре необходим взаимодействующий с пользователем искусственный интеллект (AI), чаще всего в виде враждебной игроку силы. В некоторых случаях ИИ должен помогать игроку, в других — бороться с ним, но у всех управляемых компьютером персонажей существует некоторое сходство. В зависимости от требований проекта ИИ может использовать простые или сложные поведения. Такими требованиями могут быть дипломатия с другим игроком или простое блуждание вперёд-назад по платформе. Как бы то ни было, необходимо сделать так, чтобы ИИ качественно выполнял свою работу.
В этом проекте я продемонстрирую очень простой искусственный интеллект. Допустим, мы создаём игру, в которой игрок должен тайком пробраться рядом с вражеским штабом. Когда игрока замечает камера слежения, рядом создаются враги и в течение короткого промежутка времени преследуют игрока. Именно это мы и реализуем в проекте на простейшем уровне. Завершив проект, вы получите управляемый объект игрока, круг, используемый в качестве камеры врага, и объект врага, который будет преследовать игрока, когда о его присутствии сообщит объект камеры.
Подготовка
Для начала нам нужно создать 3D-проект. Нажмите на кнопку New в верхней части окна после запуска Unity, как это показано на рисунке 1.

Рисунок 1: создание нового проекта
Назовите свой проект AI и убедитесь, что он является 3D-проектом. Выбрав место на компьютере для хранения проекта, нажмите на кнопку Create Project внизу, показанную на рисунке 2.

Рисунок 2: экран настройки проекта
После создания проекта нам первым делом нужно настроить папки в окне Assets, чтобы упорядочить свою работу. Нажмите правой кнопкой на окне Assets и выберите Create → Folder для создания новой папки. Назовите эту папку Materials. Затем создайте вторую папку и назовите её Scripts. На рисунке 3 показано, как это должно выглядеть.

Рисунок 3: создание новой папки
После всего этого окно Assets должно выглядеть так, как показано на рисунке 4.

Рисунок 4: окно Assets.
Далее создадим пол, на котором будут стоять все объекты. В окне Hierarchy выберите Create → 3D Object → Plane, чтобы создать объект-плоскость, которая будет использоваться в качестве пола.

Рисунок 5: создание объекта Plane.
Назовите этот объект Floor и измените его значение X Scale на 7, а значение Z Scale — на 3. После этого окно Inspector с выбранным объектом Floor должно выглядеть так, как показано на рисунке 6.

Рисунок 6: задание свойств объекта Floor.
Теперь нам нужно создать новый материал для Floor, чтобы отличать его от остальных объектов, которые будут размещены в сцене. В папке Materials окна Assets создайте новый материал, нажав правой кнопкой на окно Assets и выбрав Create → Material.

Рисунок 7: создание нового материала
После завершения назовите материал Floor.

Рисунок 8: материал Floor.
В верхней части окна Inspector с выбранным материалом Floor выберите color picker.

Рисунок 9: выбор color picker.
Разумеется, вы можете выбрать для пола любой цвет, но в этом примере я выбрал красно-коричневый цвет, как показано на рисунке 10.

Рисунок 10: color picker.
Выберите объект Floor в окне Hierarchy, и в компоненте Mesh Renderer выберите маленькую стрелку рядом с Materials.

Рисунок 11: подготовка к изменению материала.
Перетащите материал Floor из окна Assets в поле Element 0 компонента Mesh Renderer в окне Inspector.

Рисунок 12: задание материала Floor в качестве материала объекта Floor.
Закончив с объектом Floor, мы дожны создать вокруг области стены, чтобы игрок не мог свалиться с края. Снова заходим в Create → 3D Object → Plane для создания новой плоскости. Назовём эту плоскость Wall и выставим ей те же размеры, что и у Floor, то есть X Scale со значением 7 и Z Scale со значением 3. Затем создадим ещё три стены, выбрав объект и трижды нажав Ctrl + D. После этого разместим стены вокруг пола в соответствии с данными из таблицы.
| Название | Position X | Position Y | Position Z | Rotation X | Rotation Z |
| Wall | -35 | 21 | 0 | 0 | -90 |
| Wall (1) | -1 | 11 | -15 | 90 | 0 |
| Wall (2) | -1 | 11 | 13.5 | -90 | 0 |
| Wall (3) | 34 | 21 | 0 | 0 | 90 |
Таблица 1: позиции и повороты всех объектов Wall.
Завершив всё это, нужно изменить положение камеры, чтобы она смотрела на пол сверху. Выберите объект Main Camera и задайте для Y Position значение 30, для Z Position значение 0, а X Rotation — значение 80.

Рисунок 13: настройка объекта камеры.
Сцена подготовлена, поэтому настало время создания персонажа игрока. В окне Hierarchy нажмите на Create → 3D Object → Sphere, чтобы создать объект-сферу. Назовите этот объект Player, а затем нажмите на кнопку Add Component в нижней части окна Inspector.

Рисунок 14: добавление нового компонента.
Теперь найдите Rigidbody. После этого выберите из списка компонент Rigidbody и добавьте Rigidbody к объекту Player.

Рисунок 15: добавление компонента Rigidbody.
Далее нужно присвоить игроку тэг, который позже пригодится нам в коде. Нажмите на раскрывающееся меню Tag в левом верхнем углу окна Inspector и выберите тэг Player.

Рисунок 16: задание нового тэга.
Нам нужно задать позицию игрока, чтобы он не находился под объектом Floor. В примере я расположил игрока в левом верхнем углу с X position равным 26, Y Position равным 1, и Z position равным -9.

Рисунок 17: размещение игрока.
Чтобы наш будущий код работал правильно, нам, разумеется, нужно прикрепить его к объекту. Снова заходим в окно Hierarchy и на этот раз выбираем Create → 3D Object → Cube. Назовём этот куб Guard, добавим к нему компонент Rigidbody и компонент NavMesh Agent с помощью кнопки Add Component в окне Inspector. Далее поместим его где-нибудь в верхнем левом углу сцены. После этого окно Inspector объекта Guard будет выглядеть следующим образом:

Рисунок 18: объект Guard в окне Inspector.
И этот объект должен быть расположен так:

Рисунок 19: Размещение объекта Guard.
Наконец, нам потребуется объект, используемый в качестве «глаз» объекта Guard, который будет уведомлять Guard о том, что его касается игрок. В последний раз перейдите в окно Hierarchy и выберите Create → 3D Object → Sphere для создания ещё одного объекта-сферы. Назовите этот объект Looker. На этот раз нам не нужно добавлять к нему никаких других компонентов. Однако мы изменим размер объекта. Выбрав Looker, измените следующие переменные компонента Transform в окне Inspector.
- Scale Xна 9.
- Scale Y на 0.5.
- Scale Z на 9.

Рисунок 20: размещение объекта Looker.
Настало подходящее время для того, чтобы придать Looker уникальный материал, чтобы было заметно, что его стоит избегать. В папке Materials окна Assets нажмите правой клавишей мыши и создайте новый материал. Назовите его Looker и задайте ему ярко-красный цвет. После этого назначьте этот материал в качестве материала объекта Looker, чтобы изменить его цвет. После этого сцена должна выглядеть следующим образом:

Рисунок 21: объект Looker с новым материалом.
Единственное, что нам осталось — создать навигационный меш для Guard, по которому он сможет перемещаться. В верхней части редактора Unity есть меню Window. Выберите Window → Navigation, чтобы открыть окно Navigation, показанное на рисунке 22.

Рисунок 22: окно Navigation.
Выберите объект Floor в Hierarchy, а затем в окне Navigation поставьте флажок Navigation Static.

Рисунок 23: Navigation Static.
Далее выберем опцию Bake в верхней части окна.

Рисунок 24: переключение на меню Bake.
Откроется меню Bake, в котором можно изменять свойства навигационного меша, который мы собираемся создать. В нашем примере ничего изменять не требуется. Достаточно нажать на кнопку Bake в правой нижней части.

Рисунок 25: создание нового навигационного меша.
На этом этапе Unity попросит сохранить сцену. Сохраните её, после чего будет создан навигационный меш. Теперь сцена будет выглядеть так:

Рисунок 26: текущая сцена с добавленным навигационным мешем.
Теперь всё в Unity настроено, поэтому настало время для создания скриптов, необходимых для работы проекта. В окне Assets нажмите правой клавишей мыши и выберите Create → C# Script. Назовите этот скрипт Player. Повторите эту операцию ещё два раза, создав скрипты с названиями Guard и Looker.

Рисунок 27: создание нового скрипта.
После этого папка Scripts в окне Assets будет выглядеть так:

Рисунок 28: папка Scripts.
Первым мы начнём писать код скрипта Player. Дважды щёлкните по скрипту Player в окне Assets, чтобы открыть Visual Studio и приступить к созданию кода.
Скрипт Player достаточно прост, всё что он делает — позволяет пользователю перемещать объект-мяч. Под объявлением класса нам нужно получить ссылку на компонент Rigidbody, который мы ранее создали в проекте.
Сразу после этого в функции Start мы прикажем Unity сделать текущий компонент Rigidbody объекта Player значением rb.
После этого скрипт Player будет выглядеть так:

Рисунок 29: скрипт Player на текущий момент.
Теперь, когда значение rb присвоено, нам нужно позволить объекту Player двигаться при нажатии клавиш со стрелками. Для перемещения объекта мы будем использовать физику, применяя силу к объекту при нажатии пользователем клавиш со стрелками. Для этого достаточно добавить в функцию Update следующий код:
На этом мы завершили скрипт Player. Готовый скрипт будет выглядеть следующим образом:

Рисунок 30: готовый скрипт Player.
Сохраните свою работу и вернитесь в Unity. На этот раз выберите в окне Assets скрипт Guard. Чтобы заставить код для Guard работать, нужно добавить в верхнюю часть скрипта конструкцию using.
Далее объявим следующие переменные сразу после объявления класса.
В качестве значения переменной player объекта Guard используется объект Player. Она пригодится нам позже, когда мы прикажем объекту Guard преследовать игрока. Затем объявляется переменная navmesh для получения компонента NavMeshAgent объекта. Её мы используем позже, когда Guard начнёт преследовать игрока после того, как узнает о том, что игрок касается объекта Looker. В функции Start нам нужно задать в качестве значения переменной navmesh компонент NavMesh Agent объекта:
Затем в функции Update мы добавим единственную строку кода:
Эта строка задаёт точку назначения для объекта Guard. В нашем случае она будет брать текущую позицию объекта Player и перемещаться к этой точке. После срабатывания объект будет постоянно преследовать игрока. Вопрос в том, как выполняется процесс срабатывания? Он будет закодирован не в скрипте Guard, а в скрипте Looker. Прежде чем переходить к скрипту Looker, посмотрите на рисунок 31, чтобы сверить свой код скрипта Guard.

Рисунок 31: готовый скрипт Guard.
Внутри Looker нам снова нужно объявить следующие переменные:
После этого закомментируем функцию Start, которая в этом скрипте нам не нужна. Перейдём к функции Update и добавим следующий код:
Именно здесь происходят основные действия проекта, поэтому давайте проанализируем код. Во-первых, в зависимости от значения булевой переменной movingDown, объект, к которому прикреплён этот скрипт, будет двигаться вверх или вниз. Как только он достигнет определённой точки, то изменит направление. Далее Looker снизит значение сброса на основании реального времени. Как только таймер станет меньше нуля, он возьмём скрипт Guard из объекта Guard и отключит его, после чего объект Guard начнёт перемещаться к последней известной до этого момента позиции игрока, а затем остановится. Looker также снова включает его коллайдер, чтобы весь процесс мог начаться заново. Теперь наш скрипт выглядит следующим образом:

Рисунок 32: скрипт Looker.
Кстати о коллайдерах: настало время создать код коллизии, сбрасывающий таймер Looker и включающий скрипт Guard. В функции Update создайте следующий код:
OnCollisionEnter Unity автоматически распознаёт как код коллизии, а поэтому выполняет его при возникновении коллизии с другим объектом. В нашем случае он сначала проверяет, имеет ли столкнувшийся объект тэг Player. Если нет, то он игнорирует остальную часть кода. В противном случае он включает скрипт Guard, задаёт таймеру reset значение 5 (то есть пять секунд), и отключает его коллайдер, чтобы игрок по-прежнему мог двигаться сквозь объект и случайно не застрял в объекте Looker. Функция показана на рисунке 33.

Рисунок 33: код коллизии для Looker.
На этом весь код проекта готов! Можно сделать ещё пару вещей, прежде чем закончить проект. Сохраните всю работу и вернитесь в Unity.
Завершение проекта
Для завершения проекта нам достаточно прикрепить скрипты к соответствующим объектам и задать несколько переменных. Во-первых, перейдите из окна Navigation в окно Inspector:

Рисунок 34: переход в окно Inspector.
После этого начнём с объекта Player. Выберите его в окне Hierarchy, а затем в нижней части окна Inspector нажмите на кнопку Add Component и добавьте скрипт Player. На этом объект Player завершён.

Рисунок 35: компонент скрипта Player.
Далее выберите объект Guard. Как и раньше, прикрепим скрипт Guard к объекту. На этот раз нам понадобится сообщить Guard, кто является игроком. Для этого перетащите объект Player из Hierarchy в поле Player компонента скрипта Guard, как показано на рисунке 36.

Рисунок 36: делаем объект Player значением поля Player.
Также нам нужно отключить скрипт Guard. В нашем проекте Guard будет преследовать игрока после включения его скрипта. Этот скрипт Guard должен включаться только после того, как игрок коснётся объекта Looker. Всё, что нужно сделать — снять флажок рядом с текстом Guard (Script) в компоненте:

Рисунок 37: отключение скрипта Guard.
Наконец, перейдём к объекту Looker и прикрепим к нему скрипт Looker. На этот раз объекту Looker потребуется объект Guard в качестве значения его переменной Guard. Так же, как мы назначали объект Player переменной Player скрипта Guard, мы сделаем то же самое с объектом Guard и скриптом Looker. Перетащите Guard из Hierarchy в поле Guard скрипта Looker. И на этом проект завершён! Нажмите на кнопку Play в верхней части редактора Unity, чтобы проверить свой проект.

Рисунок 38: тестирование проекта.
Попробуйте переместить объект Player в объект Looker (не забывайте, что перемещение выполняется стрелками!). Заметьте, что после этого объект Guard начнёт преследовать игрока. Он будет продолжать преследование примерно 5 секунд, после чего сдастся.

Рисунок 39: полностью готовый проект в действии.
Заключение
Этот ИИ очень прост, но его запросто можно расширить. Допустим, если мы представим, что объект Looker — это камера, а охранник смотрит через неё, чтобы найти вас, то будет логично дать объекту Guard собственную пару глаз. Игрок может проходить рядом с камерами, но они должны учитывать и глаза охранника. Также можно скомбинировать этот проект с концепцией поиска пути: дать охраннику путь, по которому он будет следовать, создав таким образом более интересную для игрока среду.
Подобный простой ИИ можно развить множеством разных способов. Возможно, вы не захотите делать ничего вышеизложенного и решите сделать что-то своё. Советую вам экспериментировать, возможно, у вас появится идея интересного проекта, который стоит довести до конца.
Creating an A.I. (with Unity)
This tutorial will focus on the conception and creation of an Artificial Intelligence. While the implementation is aimed for Unity, the whole theory can be applied for any other game.
All concepts discussed here have been learnt by our team during the prototyping stage of Radiant Blade and has reached a production-ready state.
Why an artificial intelligence?
Before diving into the technicalities, it can be good to take a step back and to wonder why we actually want an A.I. for our game.
For a long time, I imagined making the most astonishing A.I. for a game, the kind of A.I. that would awe the player. You know, this kind of A.I. that would anticipate your every move and would be impossible to beat? Well, tell you what, this kind of A.I. is just no fun to play against.
A worthy game is a game that you can play with and that deliver entertainment. So our A.I. has to be playable with. It has to be a companion, something the player can interact with in a valuable manner.
Now, obviously, a game that is only fun won’t be an amazing game. A game will also need to be cool, or meaningful, or artistic. But for our A.I., we want entertainment, so let’s narrow this concept further.
Game Design
So, what is entertainment? And more specifically, what is entertainment in a game?
The team has spent some time thinking about it, and our conclusion can be summarized by a single word: learning. An entertaining game is a game where the player gets to learn concepts and gets to exploit them.
Entertainment is born from that little spark of curiosity that bugs your mind once you notice something new, and it growths over and over until you fully understand the novelty.
Otherwise said, an entertaining A.I. must be learnable. Simple as that.
From this small idea suddenly emerges a wide understanding of all A.I. in all video games, from Mario to DOOM, from World of Warcraft to Binding of Isaacs.
If you analyse them, you’ll find out that they are all predictable. Not necessarily deterministic as some randomness may be involved, but predictable nonetheless.
And from that, a new question quickly arises. How do you create a predictable A.I.?
Short answer: with state machines.
State machines
A state machine is a mathematical tool that contains states and transitions.

A basic state machine.
Within a deterministic state machine, you are at one specific state, and when comes the time to move, you follow one of the possible transitions to then arrive into a new state. Transitions may be subject to conditions, with a state being reachable only if, say, you have a specific spell available.
The good thing about state machines is that they are both expressive and predictable. Say that your states are concepts such as “attack”, “be hit”, “run to target” and “flee”, you could create a state machine that modelizes the basic behaviour of an A.I with a few transitions:

An example of a simple A.I..
Notice that the A.I. we just crafted can be described with a few sentences:
- The A.I. flees if it has under 10% of life;
- The A.I. can be hit;
- The A.I. runs toward the player and hit him/her when in range.
It’s a good thing, it means that the A.I. is simple. Simple is good. If at some point you start to fail describing your A.I. simply, it probably is a hint that you need to take a step back.
States machines and Unity
Alright, so state machines are cool. Can we have state machines within Unity?
Short answer: yes.
There are three broad approaches for this:
- Do it yourself;
- Use the Animator;
- Get an asset from the Unity store.
Since state machines are a very well known tools for video games, I would not recommend doing it yourself. It has already been done over and over, and except if you want to learn how to code one, you can just get something already working.
An alternative is to use the the built-in Animator of Unity. While the name may be confusing, it’s actually a state machine that happens to play animations. But guess what? You are not forced to have animations in the Animator, and if you don’t, it behaves exactly as a state machine would.
So, it’s both really fast and ergonomic to use. We approve.

The A.I. for “the Archer” of Radiant Blade, using Unity’s Animator.
The last possibility is to get an asset from the Unity Store. We personally didn’t try, and didn’t feel the need to, but we are convinced that there should be many assets that would do just as good as Unity’s Animator.
If you personally use one that has notable advantages over the Animator, don’t hesitate and tell us about it!
The Animator
You may already have been using the Animator for standard animation within Unity, but here we are going to rework the different concepts for our purpose.
Let’s go through it.
The states
Usually, the states of the Animator contain an animation. It won’t be our case, it will instead be linked to a piece of code describing a behaviour.
To illustrate it, let’s take a look at the Game Object defining our archer:
The children of “Behaviours” are A.I. behaviours. Basically, they are small controllers that will take ownership of the Archer when their corresponding state is active.
When the “Shoot” state is active, so is the “Shoot Behaviour” script for our Archer.
Don’t forget that this is state-driven. It’s the “Shoot Behaviour” that notifies the Animator when it is finished. The built-in blue progression bar of the Animator can be confusing, but it’s really only visual.
The variables
An A.I. is a responsive system. Something that adapts. But adapts to what? Well, to the player and to the environment of course!
The Animator variables will be used to describe our game’s state and will be used to make informed decisions.

The variables used by “the Archer”. They describe everything we need to know in order to modelize our A.I.
Now, an important conception point. When using the Animator in a traditional way, most of the state transitions are done because the associated animation ends. For an A.I., a state is a behaviour, it holds game-logic for a non-defined duration.
To compensate, we are using two variables whose purpose is to notify the end of a state: “behaviour_ended” and “behaviour_error”. It’s basically an output from the states that says if they ended with success or in error.
The transitions
The transitions define the change of behaviour for our A.I: “Once I’m done walking to the target, what do I do?”

Example of transition: “If the target is in melee range, I strike”.
One thing you may not know about Unity’s Animator: the transitions are ordered. One specific transition will be evaluated first, and only if its associated condition is false will the second transition be evaluated.
While the “Neutral” state is selected, we have access to its transitions’ priorities.
This small feature is wonderful, because it allows us to design the A.I. as a central brain that make an educated choice based on prioritized possibilities.
Remember the Archer A.I. that we showed some chapters ago? Notice how orderly and centric it looks? Well, the “Neutral” node that you see is what we call a decision hub, here is the story that this hub describes:
- If there is no player, I stop fighting;
- Else if the player is far away, I go closer to be in range to shoot;
- Else if the player is not in my line of sight, I go closer to be able to shoot;
- Else if I’m in melee range, I strike;
- Else if the player is too close, I may choose to back away a little;
- Else I may randomly choose to turn around the player;
- Else I shoot at the player.
The wonderful thing is that each individual transition is extremely simple: it sums up to a single test, or even not at all. This is possible because all the information is carried by the fact that the previous transitions were necessarily false!
Implementation
From this point forward, you should have a good grasp of what we are doing. Notice how I still haven’t given a single line of code?
That’s a very good thing, because it means that our framework is high-level enough to be fully explained without any technicality. Once the coding part is done, designing an A.I. feels very plug-and-play.
What do we need?
Here are the tasks at hand to get our A.I. working:
- Coding A.I. behaviours;
- Linking our Animator with our available behaviours;
- Keeping a list of game related variables up-to-date for our Animator.
Behaviours
Before jumping in, let’s recap what a behaviour does.
- It works with the character controller of our game;
- It can be identified;
- It can be enabled;
- It can end on success;
- It can end on error;
- It can be interrupted;
- … And, that’s it.
Now that we know what we actually want, let’s write it down as an API:
For the enabling and disabling part, we simply take advantage of Unity built-in methods, no need to reinvent the wheel.
That leaves us with a simple and concise API. Neat.
For the identifier, I created a method with a peculiar name: “GetBehaviourHash”. This is because of the way the Animator states are identified: the identifier of a state is the hash of its name.
So for the state “Shoot”, the corresponding identifier is “Animator.StringToHash(“Shoot”)”.
In order to clarify things and to avoid computing the same hash again and again, we can simply store them as static variables somewhere:
With this in mind, an implementation of our AbstractAIBehaviour would have something in the like in it:
We may store each hash in its own corresponding script, so “ROAM_STATE” could be stored in the “RoamBehaviour” class.
The only problem is, since we are implicitly linking each behaviour to a name, it can be bothersome to open every behaviour classes to gather the authorized names for the Animator states.
From this point forward, it’s all about writing the actual code for real behaviours, but this part is up to you, since you’re doing it for your game. All you need to do is implement subclasses of AbstractAIBehaviour
Linking the Behaviours with the Animator
We have A.I. behaviours that can be identified, listened at and enabled/disabled. Let’s make use of it.
We are going to start with a controller. Since we have several entities that are independent from each other, we need to synchronize them to get things working smoothly.
This controller purpose is to make sure that only one behaviour is enabled at a time, and to provide an entry-point to change the current behaviour.
As an aside, I know that some developers struggle to know when they should throw-in a new controller class in their game. It’s generally a good habit to think of controllers as code used to synchronize multiple smaller concepts.
This class is a tad more longer, but the code is really simple:
- A dictionary contains our known behaviours;
- A method makes it possible to activate one specific behaviour;
- Two events are used to notify the Animator when a behaviour ends.
Now that we have our entry point, we can connect our Animator with it. How do we do that? Well, with a feature that you may or may not know, as it’s not widely used: with StateMachineBehaviour.

With an Animator selected, if you left click in the middle of nowhere, you get a focus on the Animator itself and its hidden sneaky inspector.
What does this do? Well, it allows us to plug custom code into the Animator. And guess what we are going to do with that?
We are simply going to call our AIBehaviourController when the Animator state changes.
This code is extremely straight forward, except that it deals with one oddity from Unity: an Animator cannot process more than one state per frame, causing a small delay when you have to traverse a decision hub.
Fortunately, the solution is as simple as forcing the Animator to go through it anyway by forcing an Update.
With our new class, we can finally glue things together, simply by adding it to our A.I. Animator.

Our A.I. Animator will now call the AIBehaviourController upon entering a new state.
In the end, we have three classes part of the framework, and subclasses plus a character controller to include the actual game logic.

A small class diagram to synthetize our A.I. framework.
Including your game logic
All-in-all, the technical solution sums up to three classes, each one very concise.
What more do we need? Well, the game itself indeed! But unfortunately, this part has to be your own.
So, what more do you need to get your own A.I.?
- A character controller, responsible for the actual logic of the character and its rendering;
- Your variables and the code keeping them up-to-date for your A.I. Animator;
- Your own behaviours such as “Strike”, “Move”.
But at this point, it’s standard Unity code as you are used to.
Everything working together, variables, Animator and behaviours.
Last words
If you liked this article, follow us on twitter (https://twitter.com/synnaxium) to be notified when a new article is available!
Any question? Any comment?
Don’t hesitate, we are available and will answer you gladly
