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
12. PROJECT3: Third-Person Cross-Country Arcade Racing Game
13. PROJECT4: VR Application With Simple Interaction

Controlling the Game Process

Implementing the GameController component that manages switching between the game states depending on the occurrence of certain events: all enemies are killed, player gets killed or time runs out.

The game should have different states depending on the occurrence of certain events. For example, you can add tracking the list of enemies, and if the list is empty the player has won. The game will end in defeat if the player is killed.

To switch between Gameplay and Win/Lose states, we have the GameController component.

Create the GameController component and copy the following code into it:

GameController.h

Source code (C++)
#pragma once
#include <UnigineComponentSystem.h>

#include <UnigineGame.h>

// game states
enum GameState
{
	Gameplay,
	Win,
	Lose,
};

class GameController :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(GameController, Unigine::ComponentBase);

	// current state of the game process
	GameState state;
	// camera for the game final (Player only)
	PROP_PARAM(Node, EndCamera, nullptr);

	// declare methods to be called at the corresponding stages of the execution sequence
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);

private:
	// indicator of displaying the end-game screen
	bool end_screen = false;

	// world main loop overrides
	void init();
	void update();
};

GameController.cpp

Source code (C++)
#include "GameController.h"
#include "HUD.h"
#include "EnemyLogic.h"

REGISTER_COMPONENT(GameController);

using namespace Unigine;
using namespace Math;
void GameController::init()
{
	// check whether the end-game camera is assigned and whether it is a Player
	if(EndCamera && !EndCamera->isPlayer())
	{
		Log::error("GameController error: %s is not a Player-node, it cannnot be used as an EndCamera.\n", EndCamera->getName());
		EndCamera = nullptr;
	}
	// set the initial state of the game
	state = GameState::Gameplay;
}

void GameController::update()
{
	// if the game is over
	if (state != GameState::Gameplay)
	{
		if (!end_screen && EndCamera)
		{
			// switch to the camera for the game final
			Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get()));
			// show the message about the game end in HUD
			ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state);
			end_screen = true;
		}
	}
	else
	{
		// if there are no more enemies left, go to the state 'Win'
		if (!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled())
			state = GameState::Win;
	}
}

So let's add the displayStateMessage() method to the HUD component to display the game result:

HUD.cs

Source code (C++)
// to display the final result message
void HUD::displayStateMessage(GameState state)
{
	// add WidgetLabel to display the final result message, set font size and color
	WidgetLabelPtr end_message = WidgetLabel::create(screenGui, (state == GameState::Win) ? "Победа!" : "Вы проиграли!");
	end_message->setFontSize(100);
	end_message->setFontColor(vec4_red);
	screenGui->addChild(end_message, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	// bind the widget lifetime to the world
	end_message->setLifetime(Widget::LIFETIME_WORLD);

	// finalize the process
	ComponentSystem::get()->getComponentInWorld<GameController>()->setEnabled(false);
}

Next, modify code in the EnemyLogic and PlayerLogic components to use the logic of GameController:

EnemyLogic.h

Source code (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
#include "Health.h"
${#HL}$
#include "GameController.h" ${HL#}$
#include "EnemyFireController.h"

class EnemyLogic :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(EnemyLogic, Unigine::ComponentBase);

	// declare the enemy states
	enum EnemyLogicState
	{
		Idle,
		Chase,
		Attack,
	};

	PROP_PARAM(Node, player, nullptr);
	PROP_PARAM(Node, intersectionSocket, nullptr);
	PROP_PARAM(Mask, playerIntersectionMask, ~0);

	PROP_PARAM(File, hitPrefab, "");
	PROP_PARAM(Float, reachRadius, 0.5);
	PROP_PARAM(Float, attackInnerRadius, 5.0f);
	PROP_PARAM(Float, attackOuterRadius, 7.0f);
	PROP_PARAM(Float, speed, 1.0f);
	PROP_PARAM(Float, rotationStiffness, 8);
	PROP_PARAM(Float, routeRecalculationInterval, 3.0f);

	PROP_PARAM(Int, damage, 1);

	PROP_PARAM(Mask, intersectionMask, ~0);

	// declare methods to be called at the corresponding stages of the execution sequence
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);

protected:
	// world main loop overrides
	void init();
	void update();

	bool isTargetVisible();
	void updateRoute();
	void updateTargetState();
	void updateOrientation();
	void processIdleState();
	void processChaseState();
	void processAttackState();
private:
	// initializing the enemy state
	EnemyLogicState currentState = EnemyLogicState::Idle;

	bool targetIsVisible;
	Unigine::Math::Vec3 lastSeenPosition;
	Unigine::Math::vec3 lastSeenDirection;
	float lastSeenDistanceSqr;

	Unigine::BodyRigidPtr bodyRigid = nullptr;
	Unigine::WorldIntersectionPtr hitInfo = Unigine::WorldIntersection::create();
	Unigine::Vector<Unigine::NodePtr> hitExcludes;

	EnemyFireController *fireController = nullptr;
	Health *health = nullptr;
${#HL}$
	GameController *gameController = nullptr; ${HL#}$
	// creating a queue for route points
	Unigine::Vector<Unigine::Math::Vec3> calculatedRoute;
	Unigine::PathRoutePtr route = Unigine::PathRoute::create();
	bool shouldUpdateRoute = true;
	float lastCalculationTime = 0.0f;
};

EnemyLogic.cpp

Source code (C++)
#include "EnemyLogic.h"
REGISTER_COMPONENT(EnemyLogic);
using namespace Unigine;
using namespace Math;


bool EnemyLogic::isTargetVisible()
{
	Vec3 direction = (player->getWorldPosition() - intersectionSocket->getWorldPosition());
	Vec3 p0 = intersectionSocket->getWorldPosition();
	Vec3 p1 = p0 + direction;
	
	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, playerIntersectionMask.get(), hitExcludes, hitInfo);
	if (!hitObject)
		return false;

	return player->getID() == hitObject->getID();
}

void EnemyLogic::updateRoute()
{
	if (Game::getTime() - lastCalculationTime < routeRecalculationInterval)
		return;

	if (shouldUpdateRoute)
	{
		// calculate the route to the player
		route->create2D(node->getWorldPosition(), lastSeenPosition, 1);
		
		shouldUpdateRoute = false;
	}

	//  if route calculation is over
	if (route->isReady())
	{
		// check if the target point was reached
		if (route->isReached())
		{
			// clear the queue of path points
			calculatedRoute.clear();

			// add all root points to the queue
			for (int i = 1; i < route->getNumPoints(); ++i)
				calculatedRoute.append(route->getPoint(i));

			shouldUpdateRoute = true;
			lastCalculationTime = Game::getTime();
		}
		else
			// recalculate the route if the target point wasn't reached
			shouldUpdateRoute = true;
	}
}

void EnemyLogic::updateTargetState()
{
	// refresh the current visibility state
	targetIsVisible = isTargetVisible();

	// if the player is visible, remember his latest registered position
	if (targetIsVisible)
		lastSeenPosition = player->getWorldPosition();

	lastSeenDirection = (vec3)(lastSeenPosition - node->getWorldPosition());
		lastSeenDistanceSqr = lastSeenDirection.length2();
	lastSeenDirection.normalize();
}

void EnemyLogic::updateOrientation()
{
	vec3 direction = lastSeenDirection;
	direction.z = 0.0f;

	quat targetRotation = quat(Math::setTo(vec3_zero, direction.normalize(), vec3_up, Math::AXIS_Y));
	quat currentRotation = node->getWorldRotation();

	currentRotation = Math::slerp(currentRotation, targetRotation, Game::getIFps() * rotationStiffness);
	node->setWorldRotation(currentRotation);
}

void EnemyLogic::processIdleState()
{
	// if the target is visible (player) - shift Idle -> Chase
	if (targetIsVisible)
		currentState = EnemyLogicState::Chase;
}

void EnemyLogic::processChaseState()
{
	vec3 currentVelocity = bodyRigid->getLinearVelocity();
	currentVelocity.x = 0.0f;
	currentVelocity.y = 0.0f;
	if (calculatedRoute.size() > 0)
	{
		float distanceToTargetSqr = (calculatedRoute.first() - node->getWorldPosition()).length2();

		bool targetReached = (distanceToTargetSqr < reachRadius* reachRadius);
		if (targetReached)
			calculatedRoute.removeFirst();

		if (calculatedRoute.size() > 0)
		{
			vec3 direction = calculatedRoute.first() - node->getWorldPosition();
			direction.z = 0.0f;
			direction.normalize();
			currentVelocity.x = direction.x * speed;
			currentVelocity.y = direction.y * speed;
		}
	}

	// check distance and shift Chase -> Attack
	if (!targetIsVisible)
		currentState = EnemyLogicState::Idle;

	// check distance and shift Chase -> Attack
	else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
	{
		currentState = EnemyLogicState::Attack;
		currentVelocity.x = 0.0f;
		currentVelocity.y = 0.0f;
		// start shooting
		if (fireController)
			fireController->startFiring();
	}

	bodyRigid->setLinearVelocity(currentVelocity);
}

void EnemyLogic::processAttackState()
{
	// check distance and shift Attack -> Chase
	if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
	{
		currentState = EnemyLogicState::Chase;
		// stop shooting
		if (fireController)
			fireController->stopFiring();
	}
}

void EnemyLogic::init()
{
	// initialize the parameters of the point moving along the path within navigational mesh 
	route->setRadius(0.0f);
	route->setHeight(1.0f);
	route->setMaxAngle(0.5f);

	bodyRigid = node->getObjectBodyRigid();
	hitExcludes.append(node);
	hitExcludes.append(node->getChild(0));

	targetIsVisible = false;
	// getting the EnemyFireController component
	fireController = ComponentSystem::get()->getComponent<EnemyFireController>(node);

	// getting the Health component
	health = ComponentSystem::get()->getComponentInChildren<Health>(node);

${#HL}$
	// getting a link to the Game Manager component (GameController)
	gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$
	shouldUpdateRoute = true;
	lastCalculationTime = Game::getTime();
}

void EnemyLogic::update()
{
${#HL}$
	// check the current state, if the gameplay is stopped, the enemy doesn't perform any actions
	if (gameController->state != GameState::Gameplay)
		return; ${HL#}$

	// check the enemy's health
	if (health && health->isDead())
		// remove the enemy, if its health is reduced to zero
		node.deleteLater();
	updateTargetState();
	updateOrientation();
	updateRoute();

	// switching between the enemy states
	switch (currentState)
	{
		case EnemyLogicState::Idle: processIdleState(); break;
		case EnemyLogicState::Chase: processChaseState(); break;
		case EnemyLogicState::Attack: processAttackState(); break;
	}

	// changing color depending on the current state
	vec4 color = vec4_black;
	switch (currentState)
	{
		case EnemyLogicState::Idle: color = vec4_blue; break;
		case EnemyLogicState::Chase: color = vec4(1.0f, 1.0f, 0.0f, 1.0f); break;
		case EnemyLogicState::Attack: color = vec4_red; break;
	}

	// visualize the enemy states
	Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 2.0f, 0.25f, color);
	Visualizer::renderPoint3D(node->getWorldPosition() + vec3_up * 3.0f, 0.25f, isTargetVisible() ? vec4_green : vec4_red);
	Visualizer::renderPoint3D(lastSeenPosition, 0.1f, vec4(1.0f, 0.0f, 1.0f, 1.0f));


	// visualize the attack radius
	Visualizer::renderSphere(attackInnerRadius, node->getWorldTransform(), vec4_red);
	Visualizer::renderSphere(attackOuterRadius, node->getWorldTransform(), vec4_red);

	// visualize the route points
	for(Vec3 route_point: calculatedRoute)
		Visualizer::renderPoint3D(route_point + vec3_up, 0.25f, vec4_black);
}

PlayerLogic.h

Source code (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include "Health.h"
${#HL}$
#include "GameController.h" ${HL#}$
class PlayerLogic :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(PlayerLogic, Unigine::ComponentBase);

	// declare methods to be called at the corresponding stages of the execution sequence
	COMPONENT_INIT(init,2);
	COMPONENT_UPDATE(update);

private:
	// player's health
	Health *health = nullptr;
${#HL}$
	// link to the Game Manager
	GameController* gameController = nullptr; ${HL#}$
	// world main loop overrides
	void init();
	void update();
};

PlayerLogic.cpp

Source code (C++)
#include "PlayerLogic.h"
#include "FirstPersonController.h"
#include "HUD.h"
#include "WeaponController.h"
#include "ShootInput.h"
REGISTER_COMPONENT(PlayerLogic);
using namespace Unigine;
using namespace Math;

void PlayerLogic::init()
{
	// take the Health component from a node
	health = ComponentSystem::get()->getComponentInChildren<Health>(node);
	// updating info on initial player's health
	ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health);

${#HL}$
	// getting a link to the Game Manager component (GameController)
	gameController = ComponentSystem::get()->getComponentInWorld<GameController>(); ${HL#}$
}

void PlayerLogic::update()
{
${#HL}$
	// check the player's health and, if he was killed, remove him and switch the game to the 'Lose' state
	if (health && health->isDead())
	{
		// remove the player
		node.deleteLater();

		// switch the gameplay state to Lose
		gameController->state = GameState::Lose;
	}
	// check the game state, if completed, remove the player
	else if (gameController->state != GameState::Gameplay)
		node.deleteLater(); ${HL#}$
}

Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.

  1. Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.

  2. For the game ending, let's create a separate camera that will look at the scene from above. Choose Create -> Camera -> Player Dummy in the menu. Rename the camera to end_camera. Switch to this camera in the Editor and control the camera to select the desired scene view.
  3. Drag the end_camera node to the End Camera field of the GameController component assigned to the gameplay_systems node.

Now your can add more enemies and test the game.

  1. To generate an arbitrary number of enemies, add a few lines to the GameController component:

    GameController.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    
    #include <UnigineGame.h>
    
    // game states
    enum GameState
    {
    	Gameplay,
    	Win,
    	Lose,
    };
    
    class GameController :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(GameController, Unigine::ComponentBase);
    
    	// current state of the game process
    	GameState state;
    	// camera for the game final (Player only)
    	PROP_PARAM(Node, EndCamera, nullptr);
    
    ${#HL}$
    	// enemy spawn point
    	PROP_PARAM(Node, spawnPoint, nullptr);
    	// .node asset with enemy template
    	PROP_PARAM(File, enemyPrefab, nullptr);
    	// number of enemies
    	PROP_PARAM(Int, numEnemies, 1);
    	// enemy spawn interval
    	PROP_PARAM(Float, spawnInterval, 2.0f);
    
    	int spawned_enemy_counter = 0;
    	float currentTime = 0.0f; ${HL#}$
    
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    private:
    	// indicator of displaying the end-game screen
    	bool end_screen = false;
    
    	// world main loop overrides
    	void init();
    	void update();
    };

    GameController.cpp

    Source code (C++)
    #include "GameController.h"
    #include "HUD.h"
    #include "EnemyLogic.h"
    
    REGISTER_COMPONENT(GameController);
    
    using namespace Unigine;
    using namespace Math;
    void GameController::init()
    {
    	// check whether the end-game camera is assigned and whether it is a Player
    	if(EndCamera && !EndCamera->isPlayer())
    	{
    		Log::error("GameController error: %s is not a Player-node, it cannnot be used as an EndCamera.\n", EndCamera->getName());
    		EndCamera = nullptr;
    	}
    	// set the initial state of the game
    	state = GameState::Gameplay;
    }
    
    void GameController::update()
    {
    	// if the game is over
    	if (state != GameState::Gameplay)
    	{
    		if (!end_screen && EndCamera)
    		{
    			// switch to the camera for the game final
    			Game::setPlayer(checked_ptr_cast<Player>(EndCamera.get()));
    			// show the message about the game end in HUD
    			ComponentSystem::get()->getComponentInWorld<HUD>()->displayStateMessage(state);
    			end_screen = true;
    		}
    	}
    	else
    	{
    		// if there are no more enemies left, go to the state 'Win'
    ${#HL}$		if ((!ComponentSystem::get()->getComponentInWorld<EnemyLogic>() || !ComponentSystem::get()->getComponentInWorld<EnemyLogic>()->isEnabled()) && spawned_enemy_counter == numEnemies) ${HL#}$
    			state = GameState::Win;
    ${#HL}$
    		// generate new enemies (enemyPrefab) at a given point (SpawnPoint) with a specified time interval spawnInterval)
    		if (spawned_enemy_counter < numEnemies && !enemyPrefab.nullCheck())
    		{
    			currentTime += Game::getIFps();
    
    			if (currentTime > spawnInterval)
    			{
    				currentTime -= spawnInterval;
    				spawned_enemy_counter++;
    				World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(enemyPrefab.getRaw())))->setTransform(spawnPoint->getWorldTransform());
    			}
    		} ${HL#}$
    	}
    }
  2. Create the Node Dummy node and place it to the point where new enemies will appear and name it spawn_point.
  3. Drag the spawn_point node to the Spawn Point field, and the robot_enemy.node asset – to the Enemy Prefab field, and set the number of enemies and their spawn interval in seconds.

Now, let's get down to business!

Last update: 2024-11-06
Build: ()