Управление игровым процессом
Implementing the GameController component that manages switching between the game states depending on the occurrence of certain events: all enemies are killed, player gets killed or time runs out.Реализация компонента-менеджера GameController, осуществляющего переключение между состояниями игры в зависимости от событий: уничтожение всех противников, гибель игрока, истечение времени игры.
The game should have different states depending on the occurrence of certain events. For example, you can add tracking the list of enemies, and if the list is empty the player has won. The game will end in defeat if the player is killed.Игра должна иметь разные состояния в зависимости от наступления тех или иных событий. Например, можно добавить отслеживание списка врагов, и если этот список окажется пустым – игрок победил. Игра закончится поражением, если игрок будет убит.
To switch between Gameplay and Win/Lose states, we have the GameController component.Для переключения между состояниями Gameplay и Win/Lose у нас есть компонент GameController.
Create the GameController component and copy the following code into it:Создайте компонент GameController и скопируйте следующий код:
GameController.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGame.h>
// состояния игры
enum GameState
{
Gameplay,
Win,
Lose,
};
class GameController :
public Unigine::ComponentBase
{
public:
// Объявляем конструктор и деструктор для класса GameController, а также имя связанного с компонентом свойства (property).
COMPONENT_DEFINE(GameController, Unigine::ComponentBase);
// текущее состояние игрового процесса
GameState state;
// камера для финала игры (только Player)
PROP_PARAM(Node, EndCamera, nullptr);
// регистрация методов, вызываемых на соответствующих этапах World Logic
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
private:
// индикатор включения финального экрана
bool end_screen = false;
// объявление методов, вызываемых на соответствующих этапах World Logic
void init();
void update();
};
GameController.cpp
#include "GameController.h"
#include "HUD.h"
#include "EnemyLogic.h"
REGISTER_COMPONENT(GameController);
using namespace Unigine;
using namespace Math;
void GameController::init()
{
// проверяем назначена ли финальная камера и Player ли это
if(EndCamera && !EndCamera->isPlayer())
{
Log::error("GameController error: %s is not a Player-node, it cannnot be used as an EndCamera.\n", EndCamera->getName());
EndCamera = nullptr;
}
// Задаем начальное состояние игрового процесса
state = GameState::Gameplay;
}
void GameController::update()
{
// если игра окончена
if (state != GameState::Gameplay)
{
if (!end_screen && EndCamera)
{
// переключаемся на камеру для финала игры
Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get()));
// показываем сообщение об итоге игры в HUD
ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state);
end_screen = true;
}
}
else
{
// если врагов больше не осталось, переходим в состояние 'Победа' (Win)
if (!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled())
state = GameState::Win;
}
}
So let's add the displayStateMessage() method to the HUD component to display the game result:Соответственно добавим в компонент HUD новый метод displayStateMessage(), чтобы отобразить результат игры.
HUD
// отображение сообщения о результате игры
void HUD::displayStateMessage(GameState state)
{
// добавляем виджет WidgetLabel для отображения финального сообщение о результате игры, устанавливаем размер и цвет шрифта
WidgetLabelPtr end_message = WidgetLabel::create(screenGui, (state == GameState::Win) ? "Победа!" : "Вы проиграли!");
end_message->setFontSize(100);
end_message->setFontColor(vec4_red);
screenGui->addChild(end_message, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
// привязываем время жизни виджета к миру
end_message->setLifetime(Widget::LIFETIME_WORLD);
// завершаем процесс
ComponentSystem::get()->getComponentInWorld<GameController>()->setEnabled(false);
}
Next, modify code in the EnemyLogic and PlayerLogic components to use the logic of GameController:Далее доработаем код компонент EnemyLogic и PlayerLogic, чтобы использовалась логика GameController.
EnemyLogic.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
#include "Health.h"
${#HL}$
#include "GameController.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;
Health *health = nullptr;
${#HL}$
GameController *gameController = 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);
// получаем компонент Health
health = ComponentSystem::get()->getComponentInChildren<Health>(node);
${#HL}$
// берем ссылку на менеджер игрового процесса (GameController)
gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$
shouldUpdateRoute = true;
lastCalculationTime = Game::getTime();
}
void EnemyLogic::update()
{
${#HL}$
// Проверяем текущее состояние, если игровой процесс остановлен, то враг не выполняет никаких действий
if (gameController->state != GameState::Gameplay)
return; ${HL#}$
// проверяем здоровье врага
if (health && health->isDead())
// удаляем врага, если его здоровье уменьшилось до нуля
node.deleteLater();
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);
}
PlayerLogic.h
#pragma once
#include <UnigineComponentSystem.h>
#include "Health.h"
${#HL}$
#include "GameController.h" ${HL#}$
class PlayerLogic :
public Unigine::ComponentBase
{
public:
COMPONENT_DEFINE(PlayerLogic, Unigine::ComponentBase);
// регистрация методов, вызываемых на соответствующих этапах World Logic
COMPONENT_INIT(init,2);
COMPONENT_UPDATE(update);
private:
// здоровье игрока
Health *health = nullptr;
${#HL}$
// ссылка на менеджер игрового процесса
GameController* gameController = nullptr; ${HL#}$
// объявление методов, вызываемых на соответствующих этапах 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);
${#HL}$
// берем ссылку на менеджер игрового процесса (GameController)
gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$
}
void PlayerLogic::update()
{
${#HL}$
// проверяем здоровье игрока и, если он убит, удаляем его с переключением игры в состояние 'Поражение'
if (health && health->isDead())
{
// удаляем игрока
node.deleteLater();
// меняем состояние игрового процесса на (Lose - поражение)
gameController->state = GameState::Lose;
}
// проверяем состояние игры, если она окончена, удаляем игрока
else if (gameController->state != GameState::Gameplay)
node.deleteLater(); ${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.
-
Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.Создадим NodeDummy, назовем его gameplay_systems и назначим ему компонент GameController.
- For the game ending, let's create a separate camera that will look at the scene from above. Choose Create -> Camera -> Player Dummy in the menu. Rename the camera to end_camera. Switch to this camera in the Editor and control the camera to select the desired scene view.Теперь для финала игры создадим отдельную камеру, которая будет смотреть на сцену сверху. Выберите в меню Create -> Camera -> Player Dummy. Переименуйте камеру в end_camera. Переключитесь на нее в Редакторе и, управляя камерой, выберите желаемый вид сцены.
-
Drag the end_camera node to the End Camera field of the GameController component assigned to the gameplay_systems node.Перетащите ноду end_camera в поле End Camera компонента GameController ноды gameplay_systems.
Now your can add more enemies and test the game.Далее можно добавить больше врагов и проверить игру в действии.
-
To generate an arbitrary number of enemies, add a few lines to the GameController component:Для генерации произвольного количества врагов можно добавить немного кода в компонент GameController таким образом:
GameController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineGame.h> // состояния игры enum GameState { Gameplay, Win, Lose, }; class GameController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(GameController, Unigine::ComponentBase); // текущее состояние игрового процесса GameState state; // камера для финала игры (только Player) PROP_PARAM(Node, EndCamera, nullptr); ${#HL}$ // точка генерации врагов PROP_PARAM(Node, spawnPoint, nullptr); // .node-ассет с шаблоном врага PROP_PARAM(File, enemyPrefab, nullptr); // количество врагов PROP_PARAM(Int, numEnemies, 1); // интервал появления врагов PROP_PARAM(Float, spawnInterval, 2.0f); int spawned_enemy_counter = 0; float currentTime = 0.0f; ${HL#}$ // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); private: // индикатор включения финального экрана bool end_screen = false; // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); };
GameController.cpp
#include "GameController.h" #include "HUD.h" #include "EnemyLogic.h" REGISTER_COMPONENT(GameController); using namespace Unigine; using namespace Math; void GameController::init() { // проверяем назначена ли финальная камера и Player ли это if(EndCamera && !EndCamera->isPlayer()) { Log::error("GameController error: %s is not a Player-node, it cannnot be used as an EndCamera.\n", EndCamera->getName()); EndCamera = nullptr; } // Задаем начальное состояние игрового процесса state = GameState::Gameplay; } void GameController::update() { // если игра окончена if (state != GameState::Gameplay) { if (!end_screen && EndCamera) { // переключаемся на камеру для финала игры Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get())); // показываем сообщение об итоге игры в HUD ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state); end_screen = true; } } else { // если врагов больше не осталось, переходим в состояние 'Победа' (Win) ${#HL}$ if ((!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled()) && spawned_enemy_counter == numEnemies) ${HL#}$ state = GameState::Win; ${#HL}$ // генерируем новых врагов (enemyPrefab) в заданной точке (spawnPoint) с заданным интервалом времени (spawnInterval) if (spawned_enemy_counter < numEnemies && !enemyPrefab.nullCheck()) { currentTime += Game::getIFps(); if (currentTime > spawnInterval) { currentTime -= spawnInterval; spawned_enemy_counter++; World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(enemyPrefab.getRaw())))->setTransform(spawnPoint->getWorldTransform()); } } ${HL#}$ } }
- Create the Node Dummy node and place it to the point where new enemies will appear and name it spawn_point.Создайте ноду Node Dummy и поместите ее в точку, где будут появляться новые враги, и назовите ее spawn_point.
-
Drag the spawn_point node to the Spawn Point field, and the robot_enemy.node asset – to the Enemy Prefab field, and set the number of enemies and their spawn interval in seconds.Затем перетащите ноду spawn_point в поле Spawn Point, а ассет robot_enemy.node – в поле Enemy Prefab, а также задайте количество врагов и интервал их появления в секундах.
Now, let's get down to business!А, теперь – за дело!