Реализация игрового процесса
The primary objective of the game is to drive through a race track comprising several checkpoints within a specified time. To implement such a game, we also need to consider several other essential components:Главная идея игры в том, чтобы проехать трассу, состоящую из нескольких контрольных точек за отведенное время. Разберем подробнее, что нам еще понадобится для реализации такой игры:
- Minimap to mark the player and checkpointsМиникарта для обозначения игрока и контрольных точек;
- Checkpoint entity and race track entity consisting of several checkpointsСущность контрольной точки и трассы, составленной из нескольких точек;
- Lap timerТаймер для отсчета времени прохождения трассы;
- Simple interface displaying statisticsПростой интерфейс, отображающий статистику;
- End game screenОкно завершения заезда;
- Game managing componentГлавный компонент управления игрой.
Implementing a Minimap for the LocationРеализация миникарты локации#
The minimap should be located in the corner of the screen and display the player's position and orientation and the next checkpoint location. Let's create the Map component for this purpose. We'll use the WidgetSprite widget that allows displaying multiple image layers (to draw the checkpoints and the player's position without changing the map itself). Миникарта должна находиться в углу экрана и отображать позицию и ориентацию игрока и следующую контрольную точку на карте локации. Создадим компонент Map для этого. Воспользуемся виджетом WidgetSprite, позволяющим отображать несколько слоев с изображениями (чтобы иметь возможность отрисовывать точки и положение игрока, не трогая саму карту).
#pragma once
#include "Car.h"
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
#include <UnigineWidgets.h>
#include <UnigineObjects.h>
class MiniMap : public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(MiniMap, ComponentBase);
// -------------------------------
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
COMPONENT_SHUTDOWN(shutdown);
// параметры компонента
// определим пути к изображениям для фона, контрольной точки и автомобиля
PROP_PARAM(File, heigthMapImage, "");
PROP_PARAM(File, pointMarkerImage, "");
PROP_PARAM(File, carMarkerImage, "");
// получим ссылку на главный слой террейна, чтобы выяснить его пространственные границы
PROP_PARAM(Node, lMap_node, nullptr);
class MapMarker { };
// абстракция Marker представляет собой маркер на карте с информацией о слое, которому он принадлежит
class Marker : public MapMarker
{
private:
int layer = -1;
protected:
MiniMap *owner = nullptr;
public:
Marker(MiniMap* owner, int layer);
virtual void update() { };
void clear();
void decLayer(int removed_layer);
int getLayer(){ return layer; }
};
// обычные точечные маркеры будут использоваться для отображения контрольных точек
class PointMarker : public Marker
{
public: PointMarker(MiniMap *owner, int layer, Unigine::Math::vec3 point) : Marker(owner, layer)
{
Unigine::Math::vec2 image_position = Unigine::Math::vec2(this->owner->getImagePosition(point));
this->owner->mapSprite->setLayerTransform(layer, Unigine::Math::translate(image_position));
}
};
// данный маркер будет представлять игрока и его направление на миникарте
class CarMarker : public PointMarker
{
private:
Car *car;
int imageSize = 0;
public:
CarMarker(MiniMap *owner, int layer, Car *car, int image_size) : PointMarker(owner, layer, car->getNode()->getWorldPosition())
{
imageSize = image_size;
this->car = car;
}
// поворот спрайта маркера в зависимости от вращения ноды с компонентом Car
void update();
};
// методы для работы с маркерами извне
Marker* createPointMarker(Unigine::Math::Vec3 point);
Marker* createCarMarker(Car* car);
void clearMarker(Marker* marker);
private:
Unigine::LandscapeLayerMapPtr layerMap = nullptr;
// Используем WidgetSprite для миникарты -- виджет, позволяющий иметь несколько слоев с изображениями
Unigine::WidgetSpritePtr mapSprite = nullptr;
// сами изображения
Unigine::ImagePtr mapImage = nullptr;
Unigine::ImagePtr pointImage = nullptr;
Unigine::ImagePtr carImage = nullptr;
// список маркеров на карте
Unigine::Vector<Marker*> markers;
// метод для преобразования мировых координат в координаты миникарты
Unigine::Math::ivec2 getImagePosition(Unigine::Math::Vec3 world_coord);
protected:
// методы основного цикла
void init();
void update();
void shutdown();
};
#include "MiniMap.h"
REGISTER_COMPONENT(MiniMap);
using namespace Unigine;
using namespace Math;
float angleSigned(const vec3 from, const vec3 to, const vec3 axis)
{
vec3 cross = Math::cross(from, to);
float angle = Math::acos(Math::clamp(Math::dot(from, to) / Math::fsqrt(Math::length2(from) * Math::length2(to)), -1.0f, 1.0f)) * Math::Consts::RAD2DEG;
return angle * Math::sign(axis.x * cross.x + axis.y * cross.y + axis.z * cross.z);
}
MiniMap::Marker::Marker(MiniMap* owner, int layer)
{
this->owner = owner;
this->layer = layer;
}
void MiniMap::Marker::clear()
{
owner->mapSprite->removeLayer(layer);
owner = nullptr;
layer = -1;
}
void MiniMap::Marker::decLayer(int removed_layer)
{
if (layer <= removed_layer)
return;
layer--;
}
// поворот спрайта маркера в зависимости от вращения ноды с компонентом Car
void MiniMap::CarMarker::update()
{
Unigine::Math::Vec3 position = car->getNode()->getWorldPosition();
Unigine::Math::vec2 image_position = vec2(owner->getImagePosition(position));
Unigine::Math::vec3 direction = car->getNode()->getWorldDirection(Math::AXIS_Y);
direction.z = 0.0f;
float a = angleSigned(direction, Math::vec3_forward, Math::vec3_up);
int hsize = imageSize / 2;
Unigine::Math::mat4 rot = Math::translate(hsize, hsize, 0.0f) * Unigine::Math::rotateZ(a) * Math::translate(-hsize, -hsize, 0.0f);
owner->mapSprite->setLayerTransform(this->getLayer(), Math::translate(image_position) * rot);
}
void MiniMap::init()
{
GuiPtr gui = Gui::getCurrent();
if (!heigthMapImage.nullCheck())
mapImage = Image::create(FileSystem::getGUID(heigthMapImage.getRaw()).getFileSystemString());
if (!pointMarkerImage.nullCheck())pointImage = Image::create(FileSystem::getGUID(pointMarkerImage.getRaw()).getFileSystemString());
if (!carMarkerImage.nullCheck())carImage = Image::create(FileSystem::getGUID(carMarkerImage.getRaw()).getFileSystemString());
layerMap = checked_ptr_cast<LandscapeLayerMap<(lMap_node.get());
mapSprite = WidgetSprite::create(gui);
mapSprite->setImage(mapImage);
gui->addChild(mapSprite, Gui::ALIGN_RIGHT);
int map_size = Math::ceilInt(gui->getWidth() * 0.15f);
mapSprite->setHeight(map_size);
mapSprite->setWidth(map_size);
}
void MiniMap::update()
{
for (Marker *m : markers)
{
m->update();
}
}
void MiniMap::shutdown()
{
for (int i = markers.size() - 1; i > 0; i--)
{
clearMarker(markers[i]);
}
markers.clear();
GuiPtr gui = Gui::getCurrent();
gui->removeChild(mapSprite);
mapSprite.deleteLater();
}
ivec2 MiniMap::getImagePosition(Vec3 world_coord)
{
int w = Math::ceilInt(world_coord.x * mapImage->getWidth() / layerMap->getSize().x);
int h = Math::ceilInt(mapImage->getHeight() - world_coord.y * mapImage->getHeight() / layerMap->getSize().y);
return Math::ivec2(w, h);
}
MiniMap::Marker *MiniMap::createPointMarker(Unigine::Math::Vec3 point)
{
int layer = mapSprite->addLayer();
mapSprite->setLayerImage(layer, pointImage);
PointMarker *m = new PointMarker(this, layer, point);
markers.append(m);
return m;
}
MiniMap::Marker *MiniMap::createCarMarker(Car *car)
{
int layer = mapSprite->addLayer();
mapSprite->setLayerImage(layer, carImage);
Marker *m = new CarMarker(this, layer, car, carImage->getWidth());
markers.append(m);
return m;
}
// для удаления маркера требуется удалить слой из списка и корректно сдвинуть индексы слоя в оставшихся маркерах
void MiniMap::clearMarker(Marker *marker)
{
if (marker == nullptr)
return;
for(Marker *mark : markers)
{
mark->decLayer(marker->getLayer());
}
marker->clear();
markers.remove(markers.find(marker));
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
Let's create one more NodeDummy for the MiniMap component and set the images for the background and markers and the LandscapeLayerMap node — the terrain layer.Создадим еще один NodeDummy для компонента MiniMap и укажем изображения для фона и маркеров и ноду LandscapeLayerMap — слой террейна.
Implementing the Race Track and CheckpointsРеализация трассы и контрольных точек#
The CheckPoint component will represent a checkpoint of the race track that the player needs to pass. To detect the moment of passing, we use Physical Trigger that triggers a callback when physical bodies enter its volume.Компонент CheckPoint будет представлять собой контрольную точку на трассе, которую игроку нужно пройти. Для определения момента прохождения используем физический триггер (Physical Trigger), который вызывает коллбэк при вхождении физических тел в его объем.
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePhysics.h>
#include <UniginePhysicals.h>
#include "Car.h"
#include "MiniMap.h"
class CheckPoint : public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(CheckPoint, ComponentBase);
// -------------------------------
COMPONENT_INIT(init, 0);
COMPONENT_SHUTDOWN(shutdown);
// параметры компонента
PROP_PARAM(Node, trigger_node, nullptr);
// установка статуса контрольной точки
void setEnabled(bool mode);
Unigine::Event<Car*>& getEventEnter() { return enter_event; }
private:
// маркер на миникарте, представляющий эту контрольную точку
MiniMap::MapMarker *marker = nullptr;
// ссылка на компонент миникарты
MiniMap *minimap = nullptr;
// ссылка на физический триггер
Unigine::PhysicalTriggerPtr trigger = nullptr;
// событие прохождения контрольной точки
Unigine::EventInvoker<Car*> enter_event;
// обработчик события попадания физического тела в триггер
void onEnter(const Unigine::Ptr<Unigine::Body>& body);
// вспомогательная переменная для управления подписками на события
Unigine::EventConnections econn;
protected:
// методы основного цикла
void init();
void shutdown();
};
#include "CheckPoint.h"
REGISTER_COMPONENT(CheckPoint);
using namespace Unigine;
using namespace Math;
// обработчик вызывается при посещении контрольной точки автомобилем
void CheckPoint::onEnter(const BodyPtr& body)
{
// если на событие нет подписок, ничего не делаем
if (enter_event.empty())
return;
// проверяем, назначен ли компонент Car на объект, тело которого попало в триггер
Car *car = ComponentSystem::get()->getComponent<Car>(body->getObject());
if (car == nullptr)
return;
// вызываем обработчик события прохода контрольной точки
enter_event.run(car);
}
// по умолчанию все контрольные точки должны быть выключены, их переключением будет управлять отдельный компонент
void CheckPoint::init()
{
// получаем ссылку на компонент MiniMap
minimap = ComponentSystem::get()->getComponentInWorld<MiniMap>();
setEnabled(false);
trigger = checked_ptr_cast<Unigine::PhysicalTrigger>(trigger_node.get());
if (!trigger)
return;
trigger->getEventEnter().connect(econn, this, &CheckPoint::onEnter);
}
void CheckPoint::shutdown()
{
marker = nullptr;
setEnabled(false);
}
// при включении контрольной точки на миникарту будет добавлен её маркер и удален при отключении
void CheckPoint::setEnabled(bool mode)
{
node->setEnabled(mode);
if (mode)
{
if (marker == nullptr && minimap != nullptr)
marker = minimap->createPointMarker(node->getWorldPosition());
}
else
{
if (marker != nullptr && minimap != nullptr)
{
minimap->clearMarker((MiniMap::Marker*) marker);
marker = nullptr;
}
}
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
Find the race/props/checkpoint/CheckPoint.node asset and add it to the scene by dragging with the left mouse button in the viewport.Найдите ассет race/props/checkpoint/CheckPoint.node и добавьте на сцену, просто перетащив его левой кнопкой мыши во вьюпорт.
This is a Node Reference, click Edit to modify this instance. It has a cylinder and a decal inside to visualize the checkpoint. Our task is to create a new node via the menu Create → Logic → Physical Trigger, make it a child of the point node, and place within the checkpoint bounds by selecting the Cylinder type and setting the required size.Это Node Reference, нажмите Edit, чтобы отредактировать данный экземпляр. Внутри расположены цилиндр и декаль для визуального представления контрольной точки. Наша задача — создать новую ноду в меню Create → Logic → Physical Trigger в качестве дочерней к ноде point и расположить в границах контрольной точки, выбрав тип Cylinder и установив нужные размеры.
Then select the top point node and assign the CheckPoint component to it. Make sure to set the trigger in the component parameters by dragging the child PhysicalTrigger to the corresponding field.Далее, выделите верхнюю ноду point и назначьте на неё компонент CheckPoint. Не забудьте указать триггер в параметрах компонента, перетянув дочерний физический триггер в поле.
After that, save the Node Reference by clicking Apply, then create several instances of the CheckPoint node, and place them on the landscape, mapping out a small track consisting of points.После этого сохраните Node Reference, нажав Apply, затем создайте несколько экземпляров ноды CheckPoint, и расставьте их по ландшафту, организовав небольшой импровизированный трек из точек.
The next step is to create a track from the placed points. First, group them by selecting all checkpoints and pressing Ctrl + G, and rename the added parent NodeDummy from group to Track. Now let's create a new Track component that will receive the list of child nodes with the CheckPoint component, shuffle their order and provide an interface for various actions with the track:Следующим шагом необходимо создать трассу из расставленных точек. Для начала сгруппируем их, выделив все контрольные точки и нажав Ctrl + G, и переименуем добавленный родительский NodeDummy из group в Track. Теперь создадим новый компонент Track, который будет получать список дочерних нод с компонентом CheckPoint, перемешивать их порядок и предоставлять интерфейс для различных действий с трассой:
#pragma once
#include <UnigineComponentSystem.h>
#include "Car.h"
#include "CheckPoint.h"
class Track :
public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(Track, ComponentBase);
// -------------------------------
COMPONENT_INIT(init, 1);
COMPONENT_SHUTDOWN(shutdown);
// перемешивание контрольных точек трассы
void shuffle();
// рестарт трассы
void restart();
Unigine::Event<>& getEventTrackEnd() { return track_end; }
Unigine::Event<>& getEventCheckPoint() { return check_point; }
Unigine::Math::Vec3 getPoint(int num);
// длина трассы, полученная сложением расстояний между контрольными точками и начальной позицией
double getLength(Unigine::Math::Vec3 start_position);
private:
// список контрольных точек этой трассы
Unigine::Vector<CheckPoint*> points;
Unigine::Vector<int> current_track;
// индекс текущей точки
int current_point = -1;
// вспомогательная переменная для управления подписками на события
Unigine::EventConnections econn;
// события завершения проходжения трассы и прохода контрольной точки
Unigine::EventInvoker<> track_end;
Unigine::EventInvoker<> check_point;
void onCarEnter(Car* car);
protected:
// методы основного цикла
void init();
void shutdown();
};
#include "Track.h"
REGISTER_COMPONENT(Track);
using namespace Unigine;
using namespace Math;
// метод, вызываемый из компонента контрольной точки при посещении игроком
void Track::onCarEnter(Car *)
{
points[current_track[current_point]]->setEnabled(false);
current_point++;
if (current_point < points.size())
{
points[current_track[current_point]]->setEnabled(true);
check_point.run();
}
else
{
current_point = -1;
track_end.run();
}
}
// список контрольных точек строится на основе дочерних нод с нужным компонентом
void Track::init()
{
ComponentSystem::get()->getComponentsInChildren<CheckPoint>(this->node, this->points);
for (int i = 0; i < points.size(); i++)
{
// подписываемся на событие прохождения контрольной точки
points[i]->getEventEnter().connect(econn, this, &Track::onCarEnter);
current_track.append(i);
}
}
void Track::shutdown()
{
points.clear();
current_track.clear();
current_point = -1;
}
// каждый новый маршрут строится путем перемешивания списка контрольных точек
void Track::shuffle()
{
if (current_point >= 0 && current_point < points.size())
points[current_track[current_point]]->setEnabled(false);
for (int i = current_track.size() - 1; i >= 1; i--)
{
int j = Math::randInt(0, i + 1);
int tmp = current_track[j];
current_track[j] = current_track[i];
current_track[i] = tmp;
}
current_point = 0;
points[current_track[current_point]]->setEnabled(true);
}
void Track::restart()
{
if (current_point >= 0 && current_point < points.size())
points[current_track[current_point]]->setEnabled(false);
current_point = 0;
points[current_track[current_point]]->setEnabled(true);
}
Vec3 Track::getPoint(int num)
{
return points[num]->getNode()->getWorldPosition();
}
// длина трассы, полученная сложением расстояний между контрольными точками и начальной позицией
double Track::getLength(Vec3 start_position)
{
double result = Math::length(getPoint(0) - start_position);
for (int i = 1; i < points.size(); i++)
{
result += Math::length(getPoint(current_track[i - 1]) - getPoint(current_track[i]));
}
return result;
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
Assign the Track component to the checkpoints group (the Track node).Назначим компонент Track на группу контрольных точек (нода Track).
Implementing the TimerРеализация таймера#
Let's also add the GameTimer component that implements a simple timer each frame adding the time spent on rendering the previous frame to the current time value. Thus, we don't need a system timer.Добавим также компонент GameTimer, реализующий простой таймер, каждый кадр прибавляя к значению текущего времени время, затраченное на отрисовку предыдущего кадра. Таким образом, системные таймер нам не понадобится.
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGame.h>
#include <UnigineString.h>
class GameTimer:
public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(GameTimer, ComponentBase)
// -------------------------------
COMPONENT_UPDATE(update);
// функции управления таймером
void start(double time);
void stop();
void resume();
// метод для доступа к событию
Unigine::Event<>& getEventTimeZero() { return time_zero; }
// получение оставшегося/прошедшего времени
double getTimeLeft() { return time - timer; }
double getTimePassed() { return timer; }
static Unigine::String secondsToString(double seconds);
private:
double time = 0.0;
double timer = 0.0;
bool is_running = false;
// событие истечение времени таймера
Unigine::EventInvoker<> time_zero;
protected:
// методы основного цикла
void update();
};
#include "GameTimer.h"
REGISTER_COMPONENT(GameTimer);
using namespace Unigine;
using namespace Math;
void GameTimer::update()
{
if (!is_running)
return;
timer += Game::getIFps();
if (timer > time)
{
is_running = false;
time_zero.run();
}
}
void GameTimer::start(double time)
{
is_running = true;
this->time = time;
timer = 0.0f;
}
void GameTimer::stop()
{
is_running = false;
}
void GameTimer::resume()
{
is_running = true;
}
Unigine::String GameTimer::secondsToString(double seconds)
{
Unigine::String builder;
double t = seconds;
int h = Math::floorInt((double)(t / 3600.0));
t -= h * 3600.0;
int m = Math::floorInt((double)(t / 60.0));
t -= m * 60.0;
int s = Math::floorInt((double)t);
if (h > 0)
{
builder.append(String::format("%d",h));
builder.append("h:");
if (m < 10) builder.append('0');
}
if (m > 0)
{
builder += String::format("%d", m);
builder.append("m:");
if (s < 10) builder.append("0");
}
builder.append(String::format("%d", s));
builder.append("s");
return builder;
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
We'll also need a dedicated NodeDummy for the GameTimer component — let's create it and assign this component to it.Нам также понадобится отдельный NodeDummy для компонента таймера — создадим его и назначим компонент.
Implementing the InterfaceРеализация интерфейса#
A simple interface displaying the current speed, timer, and the best race time would be a nice thing to have. Let's create a separate HUD component for that and add the methods to update the values from outside. Assign the HUD component to a separate NodeDummy node.Простой интерфейс с отображением текущей скорости движения, таймера и лучшего времени прохождения данной трассы не будет лишним. Оформим это в отдельном компоненте HUD, добавив методы для обновления значений извне. Назначим компонент HUD на отдельную ноду NodeDummy.
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
#include "GameTimer.h"
class HUD :
public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(HUD, ComponentBase)
// -------------------------------
COMPONENT_INIT(init);
COMPONENT_SHUTDOWN(shutdown);
// параметры
// размер шрифта для отображения информации
PROP_PARAM(Int, font_size, 50);
// показать/скрыть информационное табло
void show();
void hide();
// установка значений отображаемых параметров
void setSpeed(float speed);
void setTime(double time);
void setBestTime(double time);
private:
// виджеты интерфейса
Unigine::WidgetLabelPtr speed_label = nullptr;
Unigine::WidgetLabelPtr time_label = nullptr;
Unigine::WidgetLabelPtr best_time_label = nullptr;
Unigine::WidgetVBoxPtr vbox = nullptr;
protected:
// методы основного цикла
void init();
void shutdown();
};
#include "HUD.h"
REGISTER_COMPONENT(HUD);
using namespace Unigine;
using namespace Math;
void HUD::init()
{
GuiPtr gui = Gui::getCurrent();
vbox = WidgetVBox::create(gui);
WidgetLabelPtr label = nullptr;
WidgetGridBoxPtr grid = WidgetGridBox::create(gui, 2);
label = WidgetLabel::create(gui, "Best Time:");
label->setFontSize(font_size);
best_time_label = WidgetLabel::create(gui, "");
best_time_label->setFontSize(font_size);
grid->addChild(label);
grid->addChild(best_time_label);
label = WidgetLabel::create(gui, "Time:");
label->setFontSize(font_size);
time_label = WidgetLabel::create(gui, "");
time_label->setFontSize(font_size);
grid->addChild(label);
grid->addChild(time_label);
label = WidgetLabel::create(gui, "Speed:");
label->setFontSize(font_size);
speed_label = WidgetLabel::create(gui, "");
speed_label->setFontSize(font_size);
grid->addChild(label);
grid->addChild(speed_label);
vbox->addChild(grid);
gui->addChild(vbox, Gui::ALIGN_BOTTOM | Gui::ALIGN_RIGHT | Gui::ALIGN_OVERLAP);
}
void HUD::shutdown()
{
GuiPtr gui = Gui::getCurrent();
gui->removeChild(vbox);
vbox.deleteLater();
speed_label = nullptr;
time_label = nullptr;
best_time_label = nullptr;
vbox = nullptr;
}
void HUD::setSpeed(float speed)
{
speed_label->setText(String::format("%d km/h", Math::toInt(speed)));
}
void HUD::setTime(double time)
{
time_label->setText(GameTimer::secondsToString(time));
}
void HUD::setBestTime(double time)
{
best_time_label->setText(GameTimer::secondsToString(time));
}
void HUD::show()
{
vbox->setHidden(false);
}
void HUD::hide()
{
vbox->setHidden(true);
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
Implementing the End Game ScreenРеализация финального окна#
One more component (UI) will be responsible for the window with the choice of actions after the player finished driving the track or ran out of time. Its only function is to display a widget with the race result text and two buttons: replay the current track and generate a new one. За финальное окно с выбором действий после прохождения трассы или истечения времени будет отвечать еще один компонент UI. Все, что он выполняет, это отображение виджета в окне с надписью о результате заезда и двумя кнопками: для повторного прохождения текущей трассы и генерации новой трассы.
#pragma once
#include <UnigineComponentSystem.h>
class UI:
public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(UI, ComponentBase);
// -------------------------------
COMPONENT_INIT(init);
COMPONENT_SHUTDOWN(shutdown);
enum STATUS
{
STATUS_WIN,
STATUS_LOSE,
};
Unigine::WidgetVBoxPtr vbox = nullptr;
Unigine::WidgetLabelPtr status_label = nullptr;
void show(UI::STATUS status);
void hide();
// событие щелчка по кнопке Restart
Unigine::Event<>& getEventRestartClicked() { return restart_clicked; }
// событие щелчка по кнопке New
Unigine::Event<>& getEventNewClicked() { return new_clicked; }
private:
// вспомогательная переменная для управления подписками на события
Unigine::EventConnections econn;
Unigine::EventInvoker<> restart_clicked;
Unigine::EventInvoker<> new_clicked;
// прокси-обработчики, вызывающие обработчики по подписке
void onRestartClicked() { restart_clicked.run(); }
void onNewClicked() { new_clicked.run(); }
protected:
// методы основного цикла
void init();
void shutdown();
};
#include "UI.h"
using namespace Unigine;
using namespace Math;
REGISTER_COMPONENT(UI);
void UI::init()
{
GuiPtr gui = Gui::getCurrent();
vbox = WidgetVBox::create(gui);
int vbox_size = 100;
vbox->setWidth(vbox_size);
vbox->setHeight(vbox_size);
vbox->setBackground(1);
status_label = WidgetLabel::create(gui, "Status");
status_label->setTextAlign(Gui::ALIGN_CENTER);
WidgetSpacerPtr spacer = WidgetSpacer::create(gui);
WidgetButtonPtr button_restart = WidgetButton::create(gui, "Restart");
button_restart->setToggleable(false);
WidgetButtonPtr button_new = WidgetButton::create(gui, "New Track");
button_new->setToggleable(false);
button_restart->getEventClicked().connect(econn, this, &UI::onRestartClicked);
button_new->getEventClicked().connect(econn, this, &UI::onNewClicked);
vbox->addChild(status_label);
vbox->addChild(spacer);
vbox->addChild(button_restart);
vbox->addChild(button_new);
vbox->arrange();
gui->addChild(vbox, Gui::ALIGN_OVERLAP);
vbox->setPosition(gui->getWidth() / 2 - vbox_size / 2, gui->getHeight() / 5);
hide();
}
void UI::shutdown()
{
GuiPtr gui = Gui::getCurrent();
gui->removeChild(vbox);
vbox.deleteLater();
vbox = nullptr;
status_label = nullptr;
}
void UI::show(UI::STATUS status)
{
status_label->setText(status == UI::STATUS::STATUS_WIN ? "You Win" : "You Lose");
vbox->setHidden(false);
Input::setMouseHandle(Input::MOUSE_HANDLE::MOUSE_HANDLE_USER);
}
void UI::hide()
{
vbox->setHidden(true);
Input::setMouseHandle(Input::MOUSE_HANDLE::MOUSE_HANDLE_GRAB);
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
This component should also be assigned to a dedicated NodeDummy.Нам также понадобится назначить данный компонент на отдельную ноду NodeDummy.
Implementing the GameplayРеализация игрового процесса#
We finally got to the main GameManager component, which ties together all the game logic and sets the game rules. It controls the timer, the track, the interface, and the end game screen.Наконец, создадим главный компонент GameManager, связывающий воедино всю игровую логику и устанавливающий правила игры. Он управляет таймером, трассой, интерфейсом и финальным окном.
Let's add one more standard speed parameter to calculate the standard time for the generated tracks, this time will be considered the best until the result is updated by the player.Добавим еще один параметр стандартной скорости для расчета стандартного времени прохождения сгенерированных трасс, полученное время будет считаться лучшим, пока результат не будет обновлен игроком.
#pragma once
#include <UnigineComponentSystem.h>
#include "CarPlayer.h"
#include "GameTimer.h"
#include "UI.h"
#include "HUD.h"
#include "Track.h"
class GameManager :
public Unigine::ComponentBase
{
public:
// конструктор компонента и список методов
COMPONENT_DEFINE(GameManager, ComponentBase);
// -------------------------------
COMPONENT_INIT(init, 2);
COMPONENT_UPDATE(update);
// параметры компонента
// скорость в км/ч для расчета стандартного времени прохождения трассы
PROP_PARAM(Float, default_speed, 1.0f); // скорость перемещения (м/с)
PROP_PARAM(Float, turn_speed, 90.0f); // скорость поворота (град/с)
PROP_PARAM(Node, player_node, nullptr); // владелец компонента CarPlayer
PROP_PARAM(Node, timer_node, nullptr); // владелец компонента GameTimer
PROP_PARAM(Node, track_node, nullptr); // владелец компонента Track
PROP_PARAM(Node, hud_node, nullptr); // владелец компонента HUD
PROP_PARAM(Node, ui_node, nullptr); // владелец компонента UI
private:
// ссылки на компоненты игровой логики
CarPlayer* player = nullptr;
GameTimer* timer = nullptr;
Track* track = nullptr;
HUD* hud = nullptr;
UI* ui = nullptr;
// ссылка на компонент миникарты
MiniMap* minimap = nullptr;
// переменная для хранения лучшего результата
double best_time = 0.0;
// позиция для сброса положения автомобиля и позиция начала трассы
Unigine::Math::Mat4 reset_transform = Unigine::Math::Mat4_identity;
Unigine::Math::Mat4 start_track_transform = Unigine::Math::Mat4_identity;
// управление игровым процессом
void start();
void startTrack();
void restartTrack();
// окно проигрыша при истечении таймера
void onTimeLeft();
// окно выигрыша при завершении трассы
void onTrackEnd();
// обновляем позицию сброса автомобиля на каждой контрольной точке
void onCheckPoint();
// вспомогательная переменная для управления подписками на события
Unigine::EventConnections econn;
protected:
// методы основного цикла
void init();
void update();
};
#include "GameManager.h"
#include "MiniMap.h"
using namespace Unigine;
using namespace Math;
REGISTER_COMPONENT(GameManager);
// окно проигрыша при истечении таймера
void GameManager::onTimeLeft()
{
timer->stop();
ui->show(UI::STATUS_LOSE);
InputController::getInstance()->setEnabled(false);
}
// окно выигрыша при завершении трассы
void GameManager::onTrackEnd()
{
timer->stop();
best_time = timer->getTimePassed();
ui->show(UI::STATUS_WIN);
InputController::getInstance()->setEnabled(false);
}
// обновляем позицию сброса автомобиля на каждой контрольной точке
void GameManager::onCheckPoint()
{
reset_transform = player->getNode()->getWorldTransform();
}
void GameManager::init()
{
// получаем компоненты логики из указанных нод
player = ComponentSystem::get()->getComponent<CarPlayer>(player_node);
timer = ComponentSystem::get()->getComponent<GameTimer>(timer_node);
track = ComponentSystem::get()->getComponent<Track>(track_node);
hud = ComponentSystem::get()->getComponent<HUD>(hud_node);
ui = ComponentSystem::get()->getComponent<UI>(ui_node);
reset_transform = player->getNode()->getWorldTransform();
// пытаемся получить компонент миникарты и добавить маркер игрока
minimap = ComponentSystem::get()->getComponentInWorld<MiniMap>();
if (minimap != nullptr)
minimap->createCarMarker(player);
// подписываемся на события интерфейса и истечения времени таймера
ui->getEventNewClicked().connect(econn, this, &GameManager::startTrack);
ui->getEventRestartClicked().connect(econn, this, &GameManager::restartTrack);
timer->getEventTimeZero().connect(econn, this, &GameManager::onTimeLeft);
// подписываемся на события трассы (прохождение контрольной точки,завершение)
track->getEventCheckPoint().connect(econn, this, &GameManager::onCheckPoint);
track->getEventTrackEnd().connect(econn, this, &GameManager::onTrackEnd);
startTrack();
}
void GameManager::update()
{
if (InputController::getInstance()->getAction(InputController::INPUT_ACTION_TYPE::RESET) > 0.0f)
player->reset(reset_transform);
// обновление отображения скорости и времени
hud->setSpeed(player->getSpeed());
hud->setTime(timer->getTimePassed());
}
void GameManager::start()
{
reset_transform = start_track_transform;
timer->start(best_time);
hud->setBestTime(best_time);
ui->hide();
InputController::getInstance()->setEnabled(true);
}
// при старте трассы производим расчет стандартного времени прохождения трассы
void GameManager::startTrack(void)
{
start_track_transform = player->getNode()->getWorldTransform();
track->shuffle();
double length = track->getLength(player->getNode()->getWorldPosition());
double default_time = length / default_speed * 3.6f;
best_time = default_time;
start();
}
// повторное прохождение трассы сначала
void GameManager::restartTrack(void)
{
player->reset(start_track_transform);
track->restart();
start();
}
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система сгенерировала свойство (property), которое будет использоваться для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
Now assign the component to a dedicated node and set the links to the corresponding components by dragging the nodes to which they are assigned into the corresponding fields in the Parameters window.Назначим компонент отдельной ноде и укажем ссылки на соответствующие компоненты, перетаскивая ноды, на которые они назначены в соответствующие поля в окне Parameters.
Set moderate standard speed for the timer, taking into account that one needs to get used to driving on mountainous terrain.Установим не слишком высокую стандартную скорость для таймера, учитывая, что к езде по гористой местности еще необходимо привыкнуть.
To simplify customization of rendering settings for beginners, the template_simplified.node preset with optimal settings has been added to the race folder. To apply it, double-click on the asset, or go to the Settings window, select any subsection of the Render section and select the desired preset in the Render Preset parameter.Чтобы упростить настройку параметров рендера для помощи новичкам, в папку race был добавлен пресет template_simplified.node с оптимальными настройками. Чтобы его применить, щелкните по нему дважды левой кнопкой мыши или перейдите в окно Settings, выберите любой подраздел в Render и выберите нужный пресет в параметре Render Preset.
Done, now you can build the project and enjoy the game!Готово, теперь можно переходить к сборке проекта и наслаждаться игрой!