实现游戏玩法
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;
}
// 根据车对象的旋转,旋转小地图上的标记精灵
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--;
}
// 根据车对象的旋转,旋转小地图上的标记精灵
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::guidToPath(FileSystem::getGUID(heigthMapImage.getRaw())));
if (!pointMarkerImage.nullCheck())pointImage = Image::create(FileSystem::guidToPath(FileSystem::getGUID(pointMarkerImage.getRaw())));
if (!carMarkerImage.nullCheck())carImage = Image::create(FileSystem::guidToPath(FileSystem::getGUID(carMarkerImage.getRaw())));
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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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 *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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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 按钮可修改该实例。它内部包含一个圆柱体和一个贴花(decal)用于可视化检查点。我们的任务是通过菜单 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 组件赋予它。确保将该组件参数中的触发器设置好——将子节点 PhysicalTrigger 拖动到对应字段中。
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.之后,点击 Apply(应用) 保存 节点引用(Node Reference),然后创建多个 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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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; }
// 获取剩余时间和已用时间的 getter 方法
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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 UnigineEditor。
We'll also need a dedicated NodeDummy for the GameTimer component — let's create it and assign this component to it.我们还需要一个专用的 NodeDummy 用于挂载 GameTimer 组件 —— 创建该节点并将该组件指派给它。
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);
// 组件参数
// HUD 的字体大小
PROP_PARAM(Int, font_size, 50);
// 显示/隐藏 HUD
void show();
void hide();
// 设置要显示的参数值
void setSpeed(float speed);
void setTime(double time);
void setBestTime(double time);
private:
// UI 小部件
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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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();
// 重新开始按钮点击事件
Unigine::Event<>& getEventRestartClicked() { return restart_clicked; }
// 新游戏按钮点击事件
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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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);
// 组件参数
// 用于计算标准赛道完成时间的速度(km/h)
PROP_PARAM(Float, default_speed, 1.0f); // 移动速度(m/s)
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* 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 组件并添加玩家标记
minimap = ComponentSystem::get()->getComponentInWorld<MiniMap>();
if (minimap != nullptr)
minimap->createCarMarker(player);
// 订阅 UI 和计时器的事件
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 来构建并运行应用程序,从而使组件系统生成用于将组件分配到节点的属性。运行结束后关闭应用程序,并切换回 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!完成,现在你可以构建项目并享受游戏了!
本页面上的信息适用于 UNIGINE 2.20 SDK.