This page has been translated automatically.
Unigine Basics
1. Introduction
2. Managing Virtual Worlds
3. Preparing 3D Models
4. Materials
5. Cameras and Lighting
6. Implementing Application Logic
7. Making Cutscenes and Recording Videos
8. Preparing Your Project for Release
9. Physics
10. Optimization Basics
11. PROJECT2: First-Person Shooter
13. PROJECT4: VR Application With Simple Interaction

Implementing Gameplay

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).

MiniMap.h
#pragma once
#include "Car.h"
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
#include <UnigineWidgets.h>
#include <UnigineObjects.h>
class MiniMap : public Unigine::ComponentBase
{
public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(MiniMap, ComponentBase);
	// -------------------------------
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);
	COMPONENT_SHUTDOWN(shutdown);

	// component parameters
	// setting the paths to images for background, checkpoint, and car
	PROP_PARAM(File, heigthMapImage, "");
	PROP_PARAM(File, pointMarkerImage, "");
	PROP_PARAM(File, carMarkerImage, "");
	// get reference to the main terrain layer to find out its boundaries
	PROP_PARAM(Node, lMap_node, nullptr);


	class MapMarker { };
	// the Marker abstraction represents a marker on the map with the information about the layer it refers to
	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; }
	};
	
	// common point markers will be used to represent checkpoints
	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));
		}
	};

	// this marker will represent the player and its direction on the minimap
	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;
		}

		// rotating the marker sprite depending on the rotation of the node with the Car component assigned
		void update();
	};

	// methods for managing markers
	Marker* createPointMarker(Unigine::Math::Vec3 point);
	Marker* createCarMarker(Car* car);
	void clearMarker(Marker* marker);

private:
	Unigine::LandscapeLayerMapPtr layerMap = nullptr; 
	// use WidgetSprite for the minimap -- this widget allows having several image layers
	Unigine::WidgetSpritePtr mapSprite = nullptr;
	// images to be used
	Unigine::ImagePtr mapImage = nullptr;
	Unigine::ImagePtr pointImage = nullptr;
	Unigine::ImagePtr carImage = nullptr;

	// list of markers on the map
	Unigine::Vector<Marker*> markers;

	// method to convert world coordinates to the minimap coordinates
	Unigine::Math::ivec2 getImagePosition(Unigine::Math::Vec3 world_coord);

protected:
	// main loop overrides
	void init();
	void update();
	void shutdown();
};
MiniMap.cpp
#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;
}

// to delete a marker, you need to remove a layer from the list and shift layer indices of the remaining markers correctly
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.

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.

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.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePhysics.h>
#include <UniginePhysicals.h>
#include "Car.h"
#include "MiniMap.h"

class CheckPoint : public Unigine::ComponentBase
{
public:
	// component contructor and the list of methods
	COMPONENT_DEFINE(CheckPoint, ComponentBase);
	// -------------------------------
	COMPONENT_INIT(init, 0);
	COMPONENT_SHUTDOWN(shutdown);

	// component parameters
	PROP_PARAM(Node, trigger_node, nullptr);
	
	// setting the checkpoint status 
	void setEnabled(bool mode);

	Unigine::Event<Car*>& getEventEnter() { return enter_event; }

private:
	// marker representing this point on the minimap
	MiniMap::MapMarker *marker = nullptr;
	// reference to the MiniMap component
	MiniMap *minimap = nullptr;
	// reference to the physical trigger
	Unigine::PhysicalTriggerPtr trigger = nullptr;

	// event of passing the checkpoint
	Unigine::EventInvoker<Car*> enter_event;
	// event handler called when a physical body enters the trigger
	void onEnter(const Unigine::Ptr<Unigine::Body>& body);

	// auxiliary variable used to manage event subscriptions
	Unigine::EventConnections econn;

protected:
	// main loop overrides
	void init();
	void shutdown();
};
CheckPoint.cpp
#include "CheckPoint.h"

REGISTER_COMPONENT(CheckPoint);
using namespace Unigine;
using namespace Math;

// the event handler is called when the car passes a checkpoint
void CheckPoint::onEnter(const BodyPtr& body)
{
	// in case there are no subscriptions to the event, we do nothing
	if (enter_event.empty())
		return;
	// checking whether the Car component is assigned to an object whose body has entered the trigger
	Car *car = ComponentSystem::get()->getComponent<Car>(body->getObject());
	if (car == nullptr)
		return;
	// calling the handler for the checkpoint enter event
	enter_event.run(car);
}

// by default all checkpoints are disabled, we'll create a dedicated component to switch their state
void CheckPoint::init()
{
	// getting a reference to the MiniMap component
	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);
}

// when a checkpoint is enabled, its marker is added to the minimap, when disabled - the marker is deleted
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.

Find the race/props/checkpoint/CheckPoint.node asset and add it to the scene by dragging with the left mouse button in the viewport.

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.

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.

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.

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:

Track.h
#pragma once
#include <UnigineComponentSystem.h>
#include "Car.h"
#include "CheckPoint.h"
class Track :
    public Unigine::ComponentBase
{
public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(Track, ComponentBase);
	// -------------------------------
	COMPONENT_INIT(init, 1);
	COMPONENT_SHUTDOWN(shutdown);

	// shuffling the checkpoints of the track
	void shuffle();
	// track restart
	void restart();

	Unigine::Event<>& getEventTrackEnd() { return track_end; }
	Unigine::Event<>& getEventCheckPoint() { return check_point; }

	Unigine::Math::Vec3 getPoint(int num);

	// the track length obtained by adding the distances between the checkpoints and the starting position
	double getLength(Unigine::Math::Vec3 start_position);

private:
	// list of the track checkpoints
	Unigine::Vector<CheckPoint*> points;
	Unigine::Vector<int> current_track;
	// index of the current point
	int current_point = -1;

	// auxiliary variable used to manage event subscriptions
	Unigine::EventConnections econn;

	// track completion and checkpoint contact events
	Unigine::EventInvoker<> track_end;
	Unigine::EventInvoker<> check_point;
	void onCarEnter(Car* car);

protected:
	// main loop overrides
	void init();
	void shutdown();
};
Track.cpp
#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();
	}
}

// list of checkpoints is built from children with the corresponding component assigned
void Track::init()
{
	ComponentSystem::get()->getComponentsInChildren<CheckPoint>(this->node, this->points);

	for (int i = 0; i < points.size(); i++)
	{
		// subscribong to the checkpoint contact event
		points[i]->getEventEnter().connect(econn, this, &Track::onCarEnter);
		current_track.append(i);
	}
}

void Track::shutdown()
{
	points.clear();
	current_track.clear();
	current_point = -1;
}

// each new route is created by shuffling the checkpoint list
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();
}

// the track length obtained by adding the distances between the checkpoints and the starting position
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.

Assign the Track component to the checkpoints group (the Track node).

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.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGame.h>
#include <UnigineString.h>
class GameTimer:
    public Unigine::ComponentBase
{
public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(GameTimer, ComponentBase)
	// -------------------------------
	COMPONENT_UPDATE(update);

	// timer control functions
	void start(double time);
	void stop();
	void resume();

	// method providing access to the event
	Unigine::Event<>& getEventTimeZero() { return time_zero; }

	// getters for the time left and the time passed
	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;
	
	// timer countdown end event
	Unigine::EventInvoker<> time_zero;

protected:
	// main loop overrides
	void update();
};
GameTimer.cpp
#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.

We'll also need a dedicated NodeDummy for the GameTimer component — let's create it and assign this component to it.

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.h
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
#include "GameTimer.h"
class HUD :
	public Unigine::ComponentBase
{
public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(HUD, ComponentBase)
	// -------------------------------
	COMPONENT_INIT(init);
	COMPONENT_SHUTDOWN(shutdown);

	// component parameters
	// font size for the HUD
	PROP_PARAM(Int, font_size, 50);

	// show/hide the HUD
	void show();
	void hide();

	// set the values of the parameters to be displayed
	void setSpeed(float speed);
	void setTime(double time);
	void setBestTime(double time);

private:
	// UI widgets
	Unigine::WidgetLabelPtr speed_label = nullptr;
	Unigine::WidgetLabelPtr time_label = nullptr;
	Unigine::WidgetLabelPtr best_time_label = nullptr;
	Unigine::WidgetVBoxPtr vbox = nullptr;

protected:
	// main loop overrides
	void init();
	void shutdown();
};
HUD.cpp
#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.

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.h
#pragma once
#include <UnigineComponentSystem.h>
class UI:
	public Unigine::ComponentBase
{
public:
	// component constructor and the list of methods
	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 click event
	Unigine::Event<>& getEventRestartClicked() { return restart_clicked; }
	// New click event
	Unigine::Event<>& getEventNewClicked() { return new_clicked; }
private:
	// auxiliary variable used to manage event subscriptions
	Unigine::EventConnections econn;
	Unigine::EventInvoker<> restart_clicked;
	Unigine::EventInvoker<> new_clicked;

	// proxy-handlers calling handlers according to subscriptions
	void onRestartClicked() { restart_clicked.run(); }
	void onNewClicked() { new_clicked.run(); }

protected:
	// main loop overrides
	void init();
	void shutdown();
};
UI.cpp
#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.

This component should also be assigned to a dedicated 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.

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.

GameManager.h
#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 constructor and the list of methods
	COMPONENT_DEFINE(GameManager, ComponentBase);
	// -------------------------------
	COMPONENT_INIT(init, 2);
	COMPONENT_UPDATE(update);

	// component parameters
	// speed in km/h to calculate the standard track completion time
	PROP_PARAM(Float, default_speed, 1.0f);	// moving speed (m/s)
	PROP_PARAM(Float, turn_speed, 90.0f);	// turning speed (deg/s)

	PROP_PARAM(Node, player_node, nullptr);	// CarPlayer component owner
	PROP_PARAM(Node, timer_node, nullptr);	// GameTimer component owner
	PROP_PARAM(Node, track_node, nullptr);	// Track component owner
	PROP_PARAM(Node, hud_node, nullptr);	// HUD component owner
	PROP_PARAM(Node, ui_node, nullptr);		// UI component owner

private:
	// links to the game logic components
	CarPlayer* player = nullptr;
	GameTimer* timer = nullptr;
	Track* track = nullptr;
	HUD* hud = nullptr;
	UI* ui = nullptr;
	// reference to the MiniMap component
	MiniMap* minimap = nullptr;
	// variable to store the best result
	double best_time = 0.0;
	// position to reset the car location and track starting point
	Unigine::Math::Mat4 reset_transform = Unigine::Math::Mat4_identity;
	Unigine::Math::Mat4 start_track_transform = Unigine::Math::Mat4_identity;

	// game process management
	void start();
	void startTrack();
	void restartTrack();

	// end game screen when time is over
	void onTimeLeft();
	// end game screen on completing the track
	void onTrackEnd();
	// update the car reset position in every checkpoint
	void onCheckPoint();
	// auxiliary variable used to manage event subscriptions
	Unigine::EventConnections econn;

protected:
	// main loop overrides
	void init();
	void update();
};
GameManager.cpp
#include "GameManager.h"
#include "MiniMap.h"
using namespace Unigine;
using namespace Math;

REGISTER_COMPONENT(GameManager);

	// end game screen when time is over
	void GameManager::onTimeLeft()
	{
		timer->stop();
		ui->show(UI::STATUS_LOSE);
		InputController::getInstance()->setEnabled(false);
	}

	// end game screen on completing the track
	void GameManager::onTrackEnd()
	{
		timer->stop();
		best_time = timer->getTimePassed();
		ui->show(UI::STATUS_WIN);
		InputController::getInstance()->setEnabled(false);
	}

	// update the car reset position in every checkpoint
	void GameManager::onCheckPoint()
	{
		reset_transform = player->getNode()->getWorldTransform();
	}

	void GameManager::init()
	{
		// getting login component from the specified nodes
		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();
		// trying to get the MiniMap component and add a player marker
		minimap = ComponentSystem::get()->getComponentInWorld<MiniMap>();
		if (minimap != nullptr)
			minimap->createCarMarker(player);

		//subscribing to UI and timer events
		ui->getEventNewClicked().connect(econn, this, &GameManager::startTrack);
		ui->getEventRestartClicked().connect(econn, this, &GameManager::restartTrack);
		timer->getEventTimeZero().connect(econn, this, &GameManager::onTimeLeft);

		// subscribing for track events (checkpoint contact, completion)
		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);

		// updated the speed and time displayed
		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);
	}

	// when starting the track, calculate the standard track finish time
	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();
	}

	// restarting the track from the beginning
	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.

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.

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.

Done, now you can build the project and enjoy the game!

Last update: 2024-12-13
Build: ()