Добавление противников с AI
Enemies are the important part of any shooter. We are going to create an enemy that moves around the scene chasing the player, starts firing at a certain distance from the player, and gets killed (deleted) if hit by the player's bullets.Противники — важная часть любого шутера. Мы собираемся создать противника, который перемещается по сцене, преследуя игрока, начинает стрелять на определенном расстоянии от него и убивается (удаляется) при попадании одной пули игрока.
Before adding an enemy model, you should create it in a 3D modeling software.Прежде чем добавлять модель врага, вы должны создать ее в каком-то стороннем приложении 3D-моделирования.
Find our ready-to-use robot_enemy.node enemy template in the data/fps/robot folder and place it in the scene.Найдите наш шаблон врага robot_enemy.node в папке data/fps/robot и добавьте его в сцену.
Applying a Finite-State Machine for AIПрименение конечного автомата для AI#
To be a strong opponent, your enemy must have a certain level of intelligence. A simple AI can be implemented using a Finite-State Machine — a concept allowing you to describe the logic in terms of states and transitions between them.Чтобы быть достойным противником, ваш враг должен обладать определенным уровнем интеллекта. Простую модель “разумного” поведения можно реализовать с использованием Конечного автомата — концепции, позволяющей описывать логику в терминах состояний и переходов между ними.
For simplicity, consider three states: Idle, Chase, and Attack/Fire.Для простоты рассмотрим три состояния: Бездействие (Idle), Преследование (Chase) и Атака (Attack/Fire).
The following diagram describes what the enemy should do in each state, and how it will switch different states. The typical transitions would be from Idle to Chase, from Chase to Attack, and vice versa.Следующая диаграмма описывает, что враг должен делать в каждом состоянии и как он будет переходить из одного состояния в другое. Типичными будут переходы от Idle к Chase, от Chase к Attack и наоборот.
Raycasts to Determine VisibilityРейкасты для определения видимости#
How will the enemy "see" us? This can be implemented with the help of raycast (Intersections), which we have already used to determine the bullet hits. The algorithm is simple: we shoot a ray from the enemy's location in the direction he is looking at, detect the first object intersected by the ray and check if it is the player. All this can be described using the following function:Как враг будет нас “видеть”? Реализовать это можно при помощи рейкастов (Intersections), которые мы уже использовали для определения попадания пули. Алгоритм простой: из точки расположения врага стреляем лучом в направлении его взгляда, ищем первый пересеченный лучом объект и проверяем, не игрок ли это? Все это можно описать в виде такой функции:
bool isTargetVisible()
{
Vec3 direction = (player->getWorldPosition() - intersectionSocket->getWorldPosition());
Vec3 p0 = intersectionSocket->getWorldPosition();
Vec3 p1 = p0 + direction;
Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, playerIntersectionMask.get(), hitExcludes, hitInfo);
if (!hitObject)
return false;
return player->getID() == hitObject->getID();
}
To implement transition between states in each frame, we are going to do the following:Для реализации переходов между состояниями в каждом кадре будем выполнять следующее:
void EnemyLogic::update()
{
// обновляем информацию о цели, путь до нее и ориентацию
updateTargetState();
updateOrientation();
updateRoute();
// переключение состояний врага
switch (currentState)
{
case EnemyLogicState::Idle: processIdleState(); break;
case EnemyLogicState::Chase: processChaseState(); break;
case EnemyLogicState::Attack: processAttackState(); break;
}
}
void EnemyLogic::processIdleState()
{
// если видна цель (игрок) - переход Бездействие -> Преследование (Chase)
if (targetIsVisible)
currentState = EnemyLogicState::Chase;
}
void EnemyLogic::processChaseState()
{
// Перерасчет координат направления и ускорения
// если цель не видна - переход Преследование -> Бездействие
if (!targetIsVisible)
currentState = EnemyLogicState::Idle;
// проверка дистанции и переход Преследование -> Атака
else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
{
currentState = EnemyLogicState::Attack;
// начинаем стрельбу
}
// движение к цели
}
void EnemyLogic::processAttackState()
{
// проверка дистанции и переход Атака -> Преследование
if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
{
currentState = EnemyLogicState::Chase;
// прекращаем стрельбу
}
}
Using NavigationИспользование навигации#
Just seeing the object is not enough, one must also get within shooting distance (within the attack radius). The enemy should be able to chase the player correctly — build a route from its current position to the player's current position, without walking through obstacles or getting stuck halfway. To give the enemy additional knowledge about how it can navigate through the level, you can use navigation. The PathRoute class in the UNIGINE API is responsible for finding path on the plane or in three-dimensional space. Pathfinding is performed only within the Navigation Area, which can be either of the following two types:Мало увидеть объект, нужно еще подобраться к нему на расстояние выстрела (в пределах радиуса атаки). Наш враг должен уметь правильно преследовать игрока – построить маршрут от своей текущей позиции до текущей позиции игрока, не проходя при этом сквозь препятствия и не застревая на полпути. Чтобы дать врагу дополнительные знания о том, как можно перемещаться по уровню можно использовать навигацию. За нахождение путей на плоскости или в трехмерном пространстве в API UNIGINE отвечает класс PathRoute. Поиск пути осуществляется только в рамках области навигации (Navigation Area), которые бывают двух типов:
- Navigation Sector is used to search for a path both in three-dimensional space (a multi-story house, for example) and on the plane — in the sector projection area (in this case the Z coordinate is ignored). Sectors can be combined to build complex areas — a set of intersecting sectors forms a single navigation area.Navigation Sector – используется для поиска пути, как в трехмерном пространстве (многоэтажный дом, например), так и на плоскости, в области проекции сектора (при этом координата Z игнорируется). Секторы можно объединять между собой, для построения сложных областей – множество пересекающихся секторов образует единую область навигации.
- Navigation Mesh is used for pathfinding only on the plane at a specified height above the mesh polygons — i.e. polygons in this case show where you can walk. Unlike sectors, Navigation Mesh is always on its own, i.e. you cannot create areas by combining several meshes or a mesh and sectors.Navigation Mesh is used for pathfinding only on the plane at a specified height above the mesh polygons — i.e. polygons in this case show where you can walk. Unlike sectors, Navigation Mesh – используется для поиска пути только на плоскости в области заданной высоты над полигонами меша – т.е. полигоны в этом случае показывают, где можно ходить. В отличие от секторов, Navigation Mesh – одиночка, т.е. нельзя создавать области, комбинируя несколько мешей или же меш с секторами.
In our case, since our characters move in a relatively simple environment, we will use Navigation Mesh to define the navigation area.В нашем случае, поскольку персонажи у нас перемещаются в относительно простом окружении, для определения области навигации будем использовать Navigation Mesh.
Such a mesh can be generated based on the FBX model of the scene using special tools, for example, RecastBlenderAddon.Сгенерировать такой меш можно на основе FBX модели сцены с использованием специальных инструментов, например, RecastBlenderAddon.
To place the mesh in the scene, click Create -> Navigation -> NavigationMesh in the Menu Bar and select the core/meshes/plain.mesh file. Align the mesh with the area to cover all areas where walking is allowed.Чтобы поместить меш в сцену, выберите в меню Create -> Navigation -> NavigationMesh и укажите файл core/meshes/plain.mesh. Выровняйте меш так, чтобы он отмечал области доступные для перемещения.
In the Parameters window, set the Height of the navigation mesh to 3 for proper route calculation.Также в окне Parameters для Navigation Mesh установите значение Height равное 3 для правильного расчета маршрута.
Now that we have a navigation area, we can start pathfinding. In the Chase state, our enemy, instead of rushing to the last visible position of the player along a straight line, will follow the path using the Navigation Mesh we added. The path consists of a queue of route points calculated using the functionality of the PathRoute class. It looks something like this:Теперь, когда у нас есть область навигации, можно приступать к поиску пути. В состоянии Chase наш враг, вместо того, чтобы устремиться к последней видимой позиции игрока по прямой, будет следовать по пути, используя добавленный нами Navigation Mesh. Путь состоит из очереди точек маршрута, вычисленных с использованием функционала класса PathRoute. Выглядит это примерно так:
void EnemyLogic::updateRoute()
{
if (Game::getTime() - lastCalculationTime < routeRecalculationInterval)
return;
if (shouldUpdateRoute)
{
// рассчитываем путь до игрока
route->create2D(node->getWorldPosition(), lastSeenPosition, 1);
shouldUpdateRoute = false;
}
// если расчет пути окончен
if (route->isReady())
{
// проверяем, не достигнута ли целевая точка
if (route->isReached())
{
// очищаем очередь точек пути
calculatedRoute.clear();
// добавляем все корневые точки в очередь
for (int i = 1; i < route->getNumPoints(); ++i)
calculatedRoute.append(route->getPoint(i));
shouldUpdateRoute = true;
lastCalculationTime = Game::getTime();
}
else
// пересчитываем путь, если целевая точка не была достигнута
shouldUpdateRoute = true;
}
}
Teaching the Enemy to ShootУчим врага стрелять#
After teaching the enemy to chase the player, we need to teach it to shoot. You don't want to strangle the player, do you?После обучения врага преследованию игрока, надо еще научить его стрелять, не душить же игрока в самом деле.
To implement the shooting ability, we need a bullet NodeReference that will be created at the moment of shooting when the robot is in the Attack state.Чтобы реализовать возможность стрельбы, нам нужен шаблон (NodeReference) пули, которая будет создаваться в моменты выстрелов, когда робот находится в состоянии атаки (Attack).
Let's add the shooting logic in the EnemyFireController component to make the robot shoot alternately from the left and right muzzle. The positions of their muzzles where bullets will be spawned are defined by the positions of two Dummy Nodes that are assigned to the Left Muzzle and Right Muzzle fields of the component.В компоненте EnemyFireController мы добавим логику стрельбы, предусматривающую попеременную стрельбу из левого и правого ствола. Позиции дул этих стволов, где будут создаваться пули, будем определять позициями двух Dummy Nodes, которые мы назначим в поля Left Muzzle и Right Muzzle компонента.
-
Create the EnemyFireController component and paste the following code into it:Создайте компонент EnemyFireController и вставьте туда следующий код:
EnemyFireController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineGame.h> #include <UnigineWorld.h> class EnemyFireController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(EnemyFireController, Unigine::ComponentBase); PROP_PARAM(Node, leftMuzzle, nullptr); PROP_PARAM(Node, rightMuzzle, nullptr); // параметры стрельбы PROP_PARAM(File, bulletPrefab, ""); PROP_PARAM(Float, shootInterval, 1.0f); // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); void startFiring(); void stopFiring(); protected: float currentTime = 0.0f; bool isLeft = false; bool isFiring = false; // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); };
EnemyFireController.cpp
#include "EnemyFireController.h" REGISTER_COMPONENT(EnemyFireController); using namespace Unigine; using namespace Math; void EnemyFireController::startFiring() { isFiring = true; } void EnemyFireController::stopFiring() { isFiring = false; } void EnemyFireController::init() { // сброс таймера currentTime = 0.0f; // переключаем стрельбу на правый ствол isLeft = false; } void EnemyFireController::update() { // если робот не в состоянии атаки (Бездействие или Преследование), то ничего не делаем if (!isFiring || bulletPrefab.nullCheck()) return; // обновляем таймер currentTime += Game::getIFps(); // проверка интервала стрельбы if (currentTime > shootInterval) { // сброс таймера currentTime -= shootInterval; // создаем пулю из ассета назначенного в bulletPrefab NodePtr bullet = World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(bulletPrefab.getRaw()))); // устанавливаем положение пули в зависимости от того, с какой стороны стреляем bullet->setWorldTransform((isLeft) ? leftMuzzle->getWorldTransform() : rightMuzzle->getWorldTransform()); // меняем ствол для следующего выстрела isLeft = !isLeft; } }
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, в которые вы внесли изменения, а затем соберить и запустите приложение, нажав в IDE Ctrl + F5, чтобы Компонентная система сгенерировала property для связи компонента с нодой. После запуска приложения закройте его и вернитесь в UnigineEditor.
- If necessary, enable editing of the robot_enemy node and assign the EnemyFireController component to the robot_root Dummy Object.Включите при необходимости редактирование ноды robot_enemy и назначьте компонент EnemyFireController ноде robot_root (Dummy Object).
-
Drag and drop the LeftGunMuzzle and RightGunMuzzle Dummy Nodes to the corresponding fields of the EnemyFireController component.Перетащите Dummy Node LeftGunMuzzle и RightGunMuzzle в соответствующие поля компонента EnemyFireController.
-
Drag and drop data/fps/bullet/bullet.node to the Bullet Prefab field.Перетащите data/fps/bullet/bullet.node в поле Bullet Prefab.
After spawning, the bullet should move in the appropriate direction changing its position in the world. If the bullet intersects with an object, a hit effect should be spawned at the point of impact. And if this object can take damage (i.e., it has a Health component, we'll do that a bit later), its health should be decreased by a certain value. Also, you can make the bullet apply an impulse to physical objects.После появления пуля должна двигаться в соответствующем направлении, меняя свое положение в мире. Если пуля пересекается с объектом, в точке попадания должен отобразиться соответствующий эффект. И если у объекта, в который попала пуля, есть такая характеристика, как “здоровье” (т.е. ему назначен компонент Health, его мы сделаем чуть позже), то это здоровье уменьшается на определенное значение.
- Add the data/fps/bullet/bullet.node asset to the scene.Добавьте ассет data/fps/bullet/bullet.node в сцену.
-
Create the Bullet component and copy the following code:Создайте компонент Bullet и скопируйте следующий код:
Bullet.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineWorld.h> class Bullet : public Unigine::ComponentBase { public: COMPONENT_DEFINE(Bullet, Unigine::ComponentBase); PROP_PARAM(File, hitPrefab, ""); PROP_PARAM(Float, speed, 10.0f); PROP_PARAM(Int, damage, 1); PROP_PARAM(Mask, intersectionMask, ~0); // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_UPDATE(update); protected: Unigine::WorldIntersectionNormalPtr hitInfo = Unigine::WorldIntersectionNormal::create(); // объявление методов, вызываемых на соответствующих этапах World Logic void update(); };
Bullet.cpp
#include "Bullet.h" #include "PlayerLogic.h" #include <UnigineGame.h> REGISTER_COMPONENT(Bullet); using namespace Unigine; using namespace Math; void Bullet::update() { // устанавливаем текущую позицию пули Vec3 currentPosition = node->getWorldPosition(); // устанавливаем направление движения пули вдоль оси Y vec3 currentDirection = node->getWorldDirection(Math::AXIS_Y); // обновляем положение пули вдоль траектории в соответствии с заданной скоростью node->setWorldPosition(node->getWorldPosition() + currentDirection * speed * Game::getIFps()); // ищем пересечение траектории пули с каким-либо объектом Unigine::ObjectPtr hitObject = World::getIntersection(currentPosition, node->getWorldPosition(), intersectionMask, hitInfo); // если пересечений не найдено, ничего не делаем if (!hitObject) return; // иначе загружаем NodeReference с эффектом попадания NodePtr hitEffect = World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(hitPrefab.getRaw()))); // устанавливаем NodeReference в точку попадания и ориентируем его по нормали к поверхности hitEffect->setParent(hitObject); hitEffect->setWorldPosition(hitInfo->getPoint()); hitEffect->setWorldDirection(hitInfo->getNormal(), vec3_up, Math::AXIS_Y); // удаляем пулю node.deleteLater(); }
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, в которые вы внесли изменения, а затем соберить и запустите приложение, нажав в IDE Ctrl + F5, чтобы Компонентная система сгенерировала property для связи компонента с нодой. После запуска приложения закройте его и вернитесь в UnigineEditor.
- Enable editing of the bullet node and assign the bullet component to its Static Mesh node.Включите редактирование ноды bullet и назначьте ее дочерней ноде Static Mesh компонент bullet.
-
Drag data/fps/bullet/bullet_hit.node to the Hit Prefab field.Перетащите data/fps/bullet/bullet_hit.node в поле Hit Prefab.
- Assign the LifeTime.cs component to the bullet (Static Mesh) node and set its Life Time value to 5 seconds.Назначьте компонент LifeTime.cs ноде bullet (Static Mesh) и установите для него значение Life Time равным 5 секундам.
- Select the bullet Node Reference and click Apply to save changes and remove the bullet node from the scene.Выберите ноду bullet (Node Reference) и нажмите Apply чтобы сохранить изменения, затем удалите ноду bullet со сцены.
Putting All TogetherСобираем все вместе#
Now summarizing the above, let's create the EnemyLogic component with the following code:Итак, резюмируя все вышесказанное, создадим компонент EnemyLogic с таким кодом:
EnemyLogic.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
#include "EnemyFireController.h"
// определяем состояния врага
class EnemyLogic :
public Unigine::ComponentBase
{
public:
COMPONENT_DEFINE(EnemyLogic, Unigine::ComponentBase);
enum EnemyLogicState
{
Idle,
Chase,
Attack,
};
PROP_PARAM(Node, player, nullptr);
PROP_PARAM(Node, intersectionSocket, nullptr);
PROP_PARAM(Mask, playerIntersectionMask, ~0);
PROP_PARAM(File, hitPrefab, "");
PROP_PARAM(Float, reachRadius, 0.5);
PROP_PARAM(Float, attackInnerRadius, 5.0f);
PROP_PARAM(Float, attackOuterRadius, 7.0f);
PROP_PARAM(Float, speed, 1.0f);
PROP_PARAM(Float, rotationStiffness, 8);
PROP_PARAM(Float, routeRecalculationInterval, 3.0f);
PROP_PARAM(Int, damage, 1);
PROP_PARAM(Mask, intersectionMask, ~0);
// регистрация методов, вызываемых на соответствующих этапах World Logic
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
protected:
// объявление методов, вызываемых на соответствующих этапах World Logic
void init();
void update();
bool isTargetVisible();
void updateRoute();
void updateTargetState();
void updateOrientation();
void processIdleState();
void processChaseState();
void processAttackState();
private:
// инициализируем состояние врага
EnemyLogicState currentState = EnemyLogicState::Idle;
bool targetIsVisible;
Unigine::Math::Vec3 lastSeenPosition;
Unigine::Math::vec3 lastSeenDirection;
float lastSeenDistanceSqr;
Unigine::BodyRigidPtr bodyRigid = nullptr;
Unigine::WorldIntersectionPtr hitInfo = Unigine::WorldIntersection::create();
Unigine::Vector<Unigine::NodePtr> hitExcludes;
EnemyFireController *fireController = nullptr;
// создаем очередь для точек пути
Unigine::Vector<Unigine::Math::Vec3> calculatedRoute;
Unigine::PathRoutePtr route = Unigine::PathRoute::create();
bool shouldUpdateRoute = true;
float lastCalculationTime = 0.0f;
};
EnemyLogic.cpp
#include "EnemyLogic.h"
REGISTER_COMPONENT(EnemyLogic);
using namespace Unigine;
using namespace Math;
bool EnemyLogic::isTargetVisible()
{
Vec3 direction = (player->getWorldPosition() - intersectionSocket->getWorldPosition());
Vec3 p0 = intersectionSocket->getWorldPosition();
Vec3 p1 = p0 + direction;
Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, playerIntersectionMask.get(), hitExcludes, hitInfo);
if (!hitObject)
return false;
return player->getID() == hitObject->getID();
}
void EnemyLogic::updateRoute()
{
if (Game::getTime() - lastCalculationTime < routeRecalculationInterval)
return;
if (shouldUpdateRoute)
{
// рассчитываем путь до игрока
route->create2D(node->getWorldPosition(), lastSeenPosition, 1);
shouldUpdateRoute = false;
}
// если расчет пути окончен
if (route->isReady())
{
// проверяем, не достигнута ли целевая точка
if (route->isReached())
{
// очищаем очередь точек пути
calculatedRoute.clear();
// добавляем все корневые точки в очередь
for (int i = 1; i < route->getNumPoints(); ++i)
calculatedRoute.append(route->getPoint(i));
shouldUpdateRoute = true;
lastCalculationTime = Game::getTime();
}
else
// пересчитываем путь, если целевая точка не была достигнута
shouldUpdateRoute = true;
}
}
void EnemyLogic::updateTargetState()
{
// обновляем текущее состояние видимости
targetIsVisible = isTargetVisible();
// если игрока видно, запоминаем его последнее зарегистрированное положение
if (targetIsVisible)
lastSeenPosition = player->getWorldPosition();
lastSeenDirection = (vec3)(lastSeenPosition - node->getWorldPosition());
lastSeenDistanceSqr = lastSeenDirection.length2();
lastSeenDirection.normalize();
}
void EnemyLogic::updateOrientation()
{
vec3 direction = lastSeenDirection;
direction.z = 0.0f;
quat targetRotation = quat(Math::setTo(vec3_zero, direction.normalize(), vec3_up, Math::AXIS_Y));
quat currentRotation = node->getWorldRotation();
currentRotation = Math::slerp(currentRotation, targetRotation, Game::getIFps() * rotationStiffness);
node->setWorldRotation(currentRotation);
}
void EnemyLogic::processIdleState()
{
// если видна цель (игрок) - переход Бездействие -> Преследование (Chase)
if (targetIsVisible)
currentState = EnemyLogicState::Chase;
}
void EnemyLogic::processChaseState()
{
vec3 currentVelocity = bodyRigid->getLinearVelocity();
currentVelocity.x = 0.0f;
currentVelocity.y = 0.0f;
if (calculatedRoute.size() > 0)
{
float distanceToTargetSqr = (calculatedRoute.first() - node->getWorldPosition()).length2();
bool targetReached = (distanceToTargetSqr < reachRadius* reachRadius);
if (targetReached)
calculatedRoute.removeFirst();
if (calculatedRoute.size() > 0)
{
vec3 direction = calculatedRoute.first() - node->getWorldPosition();
direction.z = 0.0f;
direction.normalize();
currentVelocity.x = direction.x * speed;
currentVelocity.y = direction.y * speed;
}
}
// если цель не видна - переход Преследование -> Бездействие
if (!targetIsVisible)
currentState = EnemyLogicState::Idle;
// проверка дистанции и переход Преследование -> Атака
else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
{
currentState = EnemyLogicState::Attack;
currentVelocity.x = 0.0f;
currentVelocity.y = 0.0f;
// начинаем стрельбу
if (fireController)
fireController->startFiring();
}
bodyRigid->setLinearVelocity(currentVelocity);
}
void EnemyLogic::processAttackState()
{
// проверка дистанции и переход Атака -> Преследование
if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
{
currentState = EnemyLogicState::Chase;
// прекращаем стрельбу
if (fireController)
fireController->stopFiring();
}
}
void EnemyLogic::init()
{
// инициализируем параметры точки, движущейся по пути в пределах навигационного меша
route->setRadius(0.0f);
route->setHeight(1.0f);
route->setMaxAngle(0.5f);
bodyRigid = node->getObjectBodyRigid();
hitExcludes.append(node);
hitExcludes.append(node->getChild(0));
targetIsVisible = false;
// получаем компонент EnemyFireController
fireController = ComponentSystem::get()->getComponent<EnemyFireController>(node);
shouldUpdateRoute = true;
lastCalculationTime = Game::getTime();
}
void EnemyLogic::update()
{
updateTargetState();
updateOrientation();
updateRoute();
// переключение состояний врага
switch (currentState)
{
case EnemyLogicState::Idle: processIdleState(); break;
case EnemyLogicState::Chase: processChaseState(); break;
case EnemyLogicState::Attack: processAttackState(); break;
}
// переключение цвета в зависимости от текущего состояния
vec4 color = vec4_black;
switch (currentState)
{
case EnemyLogicState::Idle: color = vec4_blue; break;
case EnemyLogicState::Chase: color = vec4(1.0f, 1.0f, 0.0f, 1.0f); break;
case EnemyLogicState::Attack: color = vec4_red; break;
}
// визуализируем состояния врага
Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 2.0f, 0.25f, color);
Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 3.0f, 0.25f, isTargetVisible() ? vec4_green : vec4_red);
Visualizer::renderPoint3D(lastSeenPosition, 0.1f, vec4(1.0f, 0.0f, 1.0f, 1.0f));
// визуализируем радиус атаки
Visualizer::renderSphere(attackInnerRadius, node->getWorldTransform(), vec4_red);
Visualizer::renderSphere(attackOuterRadius, node->getWorldTransform(), vec4_red);
// визуализируем точки маршрута
for(Vec3 route_point: calculatedRoute)
Visualizer::renderPoint3D(route_point + vec3_up, 0.25f, vec4_black);
}
-
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, в которые мы внесли изменения, а затем соберить и запустите приложение, нажав в IDE Ctrl + F5, чтобы Компонентная система сгенерировала property для связи компонента с нодой. После запуска приложения закройте его и вернитесь в UnigineEditor.
-
Enable editing of the robot_enemy node and assign the new component to the robot_root Dummy Node in the Parameters window.Включите редактирование ноды robot_enemy и назначьте новый компонент ноде robot_root (Dummy Node) в окне Parameters.
- Drag and drop the player_hit_box node to the Player field of the EnemyLogic component. This node imitates the player body and is used in calculations. Make sure that the Intersection option is checked for player_hit_box.Перетащите ноду player_hit_box в поле Player компонента EnemyLogic, эта нода имитирует тело игрока и используется в вычислениях. Убедитесь, что у player_hit_box включена опция Intersection.
-
Drag and drop the robot_intersection_socket node of the robot_enemy node to Intersection Socket field. This is the node from which the robot will do intersection checks.Перетащите ноду robot_intersection_socket ноды robot_enemy в поле Intersection Socket. Это нода, от которой робот будет выполнять проверку пересечений.
For debugging, you can enable Visualizer that will display the inner and outer attack radius, as well as the colored squares above the robot indicating:Для отладки вы можете включить Visualizer, который будет отображать внутренний и внешний радиус атаки, а также цветные квадраты над роботом, указывающие:
- The state of the robot: Idle — BLUE, Chase — YELLOW, Attack — RED.Состояние робота: Idle — СИНИЙ, Chase — ЖЕЛТЫЙ, Attack — КРАСНЫЙ.
- If the target is visible: Yes — GREEN, No — RED.Видна ли цель: Да — ЗЕЛЕНЫЙ, Нет — КРАСНЫЙ.
And the points of the calculated path:А также точки построенного пути: