Здоровье и урон при попадании
The player and enemies should have a health level that will decrease each time they are hit by a bullet. We'll store the health information in the Health component.Игрок и враги должны иметь уровень здоровья, который будет уменьшаться каждый раз, когда в них попадает пуля. Информацию о здоровье мы будем хранить в компоненте Health.
-
Create the Health component and copy the following code into it:Создайте компонент Health и скопируйте следующий код:
Health.h
#pragma once #include <UnigineComponentSystem.h> class Health : public Unigine::ComponentBase { public: COMPONENT_DEFINE(Health, Unigine::ComponentBase); // начальный уровень здоровья PROP_PARAM(Int, health, 5); // метод, проверяющий, не достиг ли текущий уровень здоровья 0 bool isDead() { return health <= 0; } // метод реализующий нанесение ущерба void takeDamage(int damage); };
Health.cpp
#include "Health.h" REGISTER_COMPONENT(Health); using namespace Unigine; using namespace Math; void Health::takeDamage(int damage) { // применяем ущерб health = max(health - damage, 0); }
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.
-
Add it to the visuals node of the robot_enemy node.Добавьте его в ноду visuals ноды robot_enemy.
- Add it to the player_hit_box node of the player node.Добавьте его в ноду player_hit_box ноды player.
-
In order to use the Health component logic, we need to modify number of components.Чтобы использовать логику компонента Health. надо внести изменения в код ряда компонентов.
In WeaponController code, add the following several lines to the part detecting that the player has hit an object:В код WeaponController.cs добавим несколько строчек в блок обнаружения попадания игрока в объект:
WeaponController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineVisualizer.h> #include "ShootInput.h" ${#HL}$ #include "VFXController.h" ${HL#}$ class WeaponController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(WeaponController, Unigine::ComponentBase); PROP_PARAM(Node, shooting_camera, nullptr); PROP_PARAM(Node, shoot_input_node, nullptr); PROP_PARAM(Node, weapon_muzzle, nullptr); PROP_PARAM(Node, vfx_node, nullptr); Unigine::NodeDummyPtr weaponMuzzle; VFXController* vfx; Unigine::PlayerDummyPtr shootingCamera = nullptr; ShootInput *shootInput = nullptr; ${#HL}$ int damage = 1; ${HL#}$ // маска Intersection чтобы определить, в какие объекты могут попадать пули int mask = ~0; // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); void shoot(); protected: // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); };
WeaponController.cpp
#include "WeaponController.h" ${#HL}$ #include "Health.h" ${HL#}$ REGISTER_COMPONENT(WeaponController); using namespace Unigine; using namespace Math; void WeaponController::shoot() { if (weaponMuzzle) vfx->onShoot(weaponMuzzle->getWorldTransform()); // задаем начало отрезка (p0) в позиции камеры и конец (p1) - в точке удаленной на 100 единиц в направлении взгляда камеры Vec3 p0 = shootingCamera->getWorldPosition(); Vec3 p1 = shootingCamera->getWorldPosition() + shootingCamera->getWorldDirection() * 100; // создаем объект для хранения intersection-нормали WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create(); // ищем первый объект, который пересекает отрезок (p0, p1) Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo); // если пересечение найдено if (hitObject) { // отрисовываем нормаль к поверхности в точке попадания при помощи Visualizer Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f); // генерируем в точке попадания NodeReference визуального эффекта vfx->onHit(hitInfo->getPoint(), hitInfo->getNormal(), hitObject); ${#HL}$ // применяем ущерб Health *health = ComponentSystem::get()->getComponent<Health>(hitObject); if (health) health->takeDamage(damage); ${HL#}$ } } void WeaponController::init() { // получаем камеру, которой назначен компонент ShootInput shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get()); } void WeaponController::update() { // обработка пользовательского ввода: проверяем нажата ли клавиша 'огонь' if (shootInput->isShooting()) shoot(); }
In Bullet.cpp, let's add several lines after detecting that the player has been hit and just before removing the bullet in order to apply damage to the character and update the health information:В Bullet.cpp после обнаружения попадания в игрока и непосредственно перед удалением пули добавим несколько строчек, для нанесения урона персонажу, в который наша пуля попала, а также обновление информации о здоровье игрока, если пуля попала в него :
Bullet.cpp
#include "Bullet.h" #include "PlayerLogic.h" ${#HL}$ #include "Health.h" #include "HUD.h" ${HL#}$ #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); ${#HL}$ // проверяем объект, в котороый попали (hitObject), игрок ли это и есть ли у него компонента Health Health *health = ComponentSystem::get()->getComponent<Health>(hitObject); if (health && ComponentSystem::get()->getComponentInParent<PlayerLogic>(hitObject)) { // применяем ущерб от пули health->takeDamage(damage); // обновляем информацию о здоровье игрока в HUD ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health); } ${HL#}$ // удаляем пулю node.deleteLater(); }
The robots that have zero health should be deleted from the scene. The Health component has the isDead() flag which is checked by the EnemyLogic component of the robot. If the flag is set to true, the node of that robot will be deleted. To do this, we will add the health property to the EnemyLogic component, initialize it in init() and then we'll check the health level of the enemy robot every frame in update() and remove it if necessary:Роботы, здоровье которых становится равным нулю, удаляются со сцены. У компонента Health есть флаг isDead(), который проверяется компонентом логики вражеского робота (EnemyLogic). Если флаг равен true, то нода соответствующего робота будет удалена. Для этого в компоненту EnemyLogic добавим свойство health, проинициализируем его в init() и каждый кадр в update() будем проверять уровень здоровья вражеского робота и удалять его при необходимости:
EnemyLogic.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
${#HL}$
#include "Health.h" ${HL#}$
#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;
${#HL}$
Health *health = nullptr; ${HL#}$
// создаем очередь для точек пути
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);
${#HL}$
// получаем компонент Health
health = ComponentSystem::get()->getComponentInChildren<Health>(node); ${HL#}$
shouldUpdateRoute = true;
lastCalculationTime = Game::getTime();
}
void EnemyLogic::update()
{
${#HL}$
// проверяем здоровье врага
if (health && health->isDead())
// удаляем врага, если его здоровье уменьшилось до нуля
node.deleteLater(); ${HL#}$
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);
}
We need to add the same check for the player, only instead of deleting it (in this case we'll just delete the main camera and see nothing else) we'll just make it immovable by disabling several components.Такую же проверку нам нужно добавить и для игрока, только вместо удаления (в этом случае мы просто удалим главную камеру и ничего больше не увидим) пока просто обездвижим его, отключив несколько компонент.
-
Create the PlayerLogic component and add the following code into it:Создайте компонент PlayerLogic и добавьте в него следующий код:
PlayerLogic.h
#pragma once #include <UnigineComponentSystem.h> #include "Health.h" class PlayerLogic : public Unigine::ComponentBase { public: COMPONENT_DEFINE(PlayerLogic, Unigine::ComponentBase); // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init,2); COMPONENT_UPDATE(update); private: // здоровье игрока Health *health = nullptr; // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); };
PlayerLogic.cpp
#include "PlayerLogic.h" #include "FirstPersonController.h" #include "HUD.h" #include "WeaponController.h" #include "ShootInput.h" REGISTER_COMPONENT(PlayerLogic); using namespace Unigine; using namespace Math; void PlayerLogic::init() { // берем у ноды компонент Health health = ComponentSystem::get()->getComponentInChildren<Health>(node); // обновляем информацию об исходном здоровье игрока ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health); } void PlayerLogic::update() { // проверяем выставлен ли флаг isDead if (health && health->isDead()) { // обездвиживаем игрока, отключая компоненты ComponentSystem::get()->getComponent<FirstPersonController>(node)->setEnabled(false); ComponentSystem::get()->getComponent<WeaponController>(node)->setEnabled(false); ComponentSystem::get()->getComponent<ShootInput>(node)->setEnabled(false); } }
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.
- Add the PlayerLogic component to the player node.Добавьте компонент PlayerLogic к ноде player.
Let's also add displaying of player's health information in the HUD. To do this, we will add a few lines to the init() method and add the updateHealthInfo() method to update the value in the GUI widget in the HUD component's code:Добавим также отображение информации о здоровье игрока в HUD. Для этого допишем несколько строчек в метод init() и добавим метод updateHealthInfo() для обновления значения в виджете GUI в коде HUD:
HUD.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
class HUD :
public Unigine::ComponentBase
{
public:
COMPONENT_DEFINE(HUD, Unigine::ComponentBase);
// параметры прицела
PROP_PARAM(File, crosshairImage, "");
PROP_PARAM(Int, crosshairSize, 16);
// регистрация методов, вызываемых на соответствующих этапах World Logic
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
// ссылка на экранный GUI
Unigine::GuiPtr screenGui = nullptr;
${#HL}$
void updateHealthInfo(int health); ${HL#}$
protected:
Unigine::WidgetSpritePtr sprite = nullptr;
${#HL}$
// виджет для отображения здоровья игрока
Unigine::WidgetLabelPtr label = nullptr; ${HL#}$
Unigine::Math::ivec2 prev_size;
// объявление методов, вызываемых на соответствующих этапах World Logic
void init();
void update();
};
HUD.cpp
#include "HUD.h"
#include <UnigineGame.h>
REGISTER_COMPONENT(HUD);
using namespace Unigine;
using namespace Math;
void HUD::init()
{
// получаем текущий экранный GUI
screenGui = Gui::getCurrent();
// создаем виджет WidgetSprite для прицела
if (crosshairImage != "")
sprite = WidgetSprite::create(screenGui, Unigine::FileSystem::guidToPath(FileSystem::getGUID(crosshairImage.getRaw())));
// задаем размер спрайта
sprite->setWidth(crosshairSize);
sprite->setHeight(crosshairSize);
// добавляем спрайт к GUI так, чтобы он всегда был посередине экрана и поверх всех остальных виджетов
screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
// привязываем время жизни виджета к миру
sprite->setLifetime(Widget::LIFETIME_WORLD);
${#HL}$
// добавляем виджет WidgetLabel для отображения здоровья игрока, устанавливаем его положение размер шрифта
label = WidgetLabel::create(screenGui, "");
label->setFontSize(50);
label->setPosition(10, 10);
label->setLifetime(Widget::LIFETIME_WORLD);
// добавляем виджет к GUI
screenGui->addChild(label, Gui::ALIGN_TOP | Gui::ALIGN_LEFT | Gui::ALIGN_OVERLAP); ${HL#}$
}
void HUD::update()
{
ivec2 new_size = screenGui->getSize();
if (prev_size != new_size)
{
screenGui->removeChild(sprite);
screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
}
prev_size = new_size;
}
${#HL}$
// обновление текущего уровня здоровья игрока
void HUD::updateHealthInfo(int health)
{
label->setText(Unigine::String::format("Health: %d", health));
} ${HL#}$
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.