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

Health and Damage

The player and enemies should have a health level that will decrease each time they are hit by a bullet. We'll store the health information in the Health component.

  1. Create the Health component and copy the following code into it:

    Health.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    class Health :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(Health, Unigine::ComponentBase);
    
    	// initial health level
    	PROP_PARAM(Int, health, 5);
    
    	// method indicating that the health value is less or equal to 0
    	bool isDead() { return health <= 0; }
    
    	// method implementing damage application
    	void takeDamage(int damage);
    };

    Health.cpp

    Source code (C++)
    #include "Health.h"
    REGISTER_COMPONENT(Health);
    using namespace Unigine;
    using namespace Math;
    
    void Health::takeDamage(int damage)
    {
    	// apply damage
    	health = max(health - damage, 0);
    }

    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.

  2. Add it to the visuals node of the robot_enemy node.

  3. Add it to the player_hit_box node of the player node.
  4. In order to use the Health component logic, we need to modify number of components.

    In WeaponController code, add the following several lines to the part detecting that the player has hit an object:

    WeaponController.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include <UnigineVisualizer.h>
    #include "ShootInput.h"
    
    ${#HL}$ #include "VFXController.h" ${HL#}$
    
    class WeaponController :
    	public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(WeaponController, Unigine::ComponentBase);
    
    	PROP_PARAM(Node, shooting_camera, nullptr);
    	PROP_PARAM(Node, shoot_input_node, nullptr);
    
    	PROP_PARAM(Node, weapon_muzzle, nullptr);
    	PROP_PARAM(Node, vfx_node, nullptr);
    
    	Unigine::NodeDummyPtr weaponMuzzle;
    	VFXController* vfx;
    
    	Unigine::PlayerDummyPtr shootingCamera = nullptr;
    	ShootInput *shootInput = nullptr;
    ${#HL}$	int damage = 1; ${HL#}$
    
    	// Intersection mask to define objects to be affected by bullets
    	int mask = ~0;
    
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	void shoot();
    
    protected:
    	// world main loop overrides
    	void init();
    	void update();
    };

    WeaponController.cpp

    Source code (C++)
    #include "WeaponController.h"
    ${#HL}$
    #include "Health.h" ${HL#}$
    REGISTER_COMPONENT(WeaponController);
    using namespace Unigine;
    using namespace Math;
    
    void WeaponController::shoot()
    {
    	if (weaponMuzzle)
    		vfx->onShoot(weaponMuzzle->getWorldTransform());
    
    	// set the line starting point (p0) in the camera position and end point (p1) in the point 100 units away in the camera view direction
    	Vec3 p0 = shootingCamera->getWorldPosition();
    	Vec3 p1 = shootingCamera->getWorldPosition() + shootingCamera->getWorldDirection() * 100;
    
    	// create an intersection-normal storage object
    	WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create();
    	 get the first object intersected by the (p0,p1) line
    	Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo);
    	// if the intersection is found
    	if (hitObject)
    	{
    		// render the intersection normal to the surface in the hit point using Visualizer
    		Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f);
    
    		// render the hit effect in the intersection point
    		vfx->onHit(hitInfo->getPoint(), hitInfo->getNormal(), hitObject);
    ${#HL}$
    		// apply damage
    		Health *health = ComponentSystem::get()->getComponent<Health>(hitObject);
    		if (health)
    			health->takeDamage(damage); ${HL#}$
    	}
    }
    
    void WeaponController::init()
    {
    	// getting the camera to which the ShootInput component is assigned
    	shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get());
    }
    
    void WeaponController::update()
    {
    	// handle user input: check if the 'fire' button is pressed
    	if (shootInput->isShooting())
    		shoot();
    }

    In Bullet.cpp, let's add several lines after detecting that the player has been hit and just before removing the bullet in order to apply damage to the character and update the health information:

    Bullet.cpp

    Source code (C++)
    #include "Bullet.h"
    #include "PlayerLogic.h"
    ${#HL}$
    #include "Health.h"
    #include "HUD.h" ${HL#}$
    #include <UnigineGame.h>
    REGISTER_COMPONENT(Bullet);
    using namespace Unigine;
    using namespace Math;
    
    void Bullet::update()
    {
    	// set the current bullet position
    	Vec3 currentPosition = node->getWorldPosition();
    	// set the direction of the bullet movement along the Y axis
    	vec3 currentDirection = node->getWorldDirection(Math::AXIS_Y);
    
    	// update bullet position along the trajectory according to the set speed
    	node->setWorldPosition(node->getWorldPosition() + currentDirection * speed * Game::getIFps());
    
    	// find the intersection of the bullet's trajectory with some of objects
    	Unigine::ObjectPtr hitObject = World::getIntersection(currentPosition, node->getWorldPosition(), intersectionMask, hitInfo);
    
    	// if intersections weren't found, do nothing
    	if (!hitObject)
    		return;
    
    	// otherwise load NodeReference with hit effect
    	NodePtr hitEffect = World::loadNode(Unigine::FileSystem::guidToPath(FileSystem::getGUID(hitPrefab.getRaw())));
    	// set NodeReference to the hit point and set its direction according to the hit normal
    	hitEffect->setParent(hitObject);
    	hitEffect->setWorldPosition(hitInfo->getPoint());
    	hitEffect->setWorldDirection(hitInfo->getNormal(), vec3_up, Math::AXIS_Y);
    
    ${#HL}$
    	// check the hitObject, whether it is a player and whether it has a Health component 
    	Health *health = ComponentSystem::get()->getComponent<Health>(hitObject);
    	if (health && ComponentSystem::get()->getComponentInParent<PlayerLogic>(hitObject))
    	{
    		// apply damage from the bullet
    		health->takeDamage(damage);
    
    		// update player health info in HUD
    		ComponentSystem::get()->getComponentInWorld<HUD>()->updateHealthInfo(health->health);
    	} ${HL#}$
    
    	// delete the bullet
    	node.deleteLater();
    }

The robots that have zero health should be deleted from the scene. The Health component has the isDead() flag which is checked by the EnemyLogic component of the robot. If the flag is set to true, the node of that robot will be deleted. To do this, we will add the health property to the EnemyLogic component, initialize it in init() and then we'll check the health level of the enemy robot every frame in update() and remove it if necessary:

EnemyLogic.h

Source code (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UniginePathFinding.h>
#include <UnigineVisualizer.h>
${#HL}$
#include "Health.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;
${#HL}$
	Health *health = 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);

${#HL}$
	// getting the Health component
	health = ComponentSystem::get()->getComponentInChildren<Health>(node); ${HL#}$

	shouldUpdateRoute = true;
	lastCalculationTime = Game::getTime();
}

void EnemyLogic::update()
{
${#HL}$
	// check the enemy's health
	if (health && health->isDead())
		// remove the enemy, if its health is reduced to zero
		node.deleteLater(); ${HL#}$
	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);
}

We need to add the same check for the player, only instead of deleting it (in this case we'll just delete the main camera and see nothing else) we'll just make it immovable by disabling several components.

  1. Create the PlayerLogic component and add the following code into it:

    PlayerLogic.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    #include "Health.h"
    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;
    	// 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);
    }
    
    void PlayerLogic::update()
    {
    	// checking if the isDead flag is set
    	if (health && health->isDead())
    	{
    		// make the player immovable by disabling the components
    		ComponentSystem::get()->getComponent<FirstPersonController>(node)->setEnabled(false);
    		ComponentSystem::get()->getComponent<WeaponController>(node)->setEnabled(false);
    		ComponentSystem::get()->getComponent<ShootInput>(node)->setEnabled(false);
    	}
    }

    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.

  2. Add the PlayerLogic component to the player node.

Let's also add displaying of player's health information in the HUD. To do this, we will add a few lines to the init() method and add the updateHealthInfo() method to update the value in the GUI widget in the HUD component's code:

HUD.h

Source code (C++)
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGui.h>
class HUD :
	public Unigine::ComponentBase
{
public:
	COMPONENT_DEFINE(HUD, Unigine::ComponentBase);
	// crosshair parameters
	PROP_PARAM(File, crosshairImage, "");
	PROP_PARAM(Int, crosshairSize, 16);

	// declare methods to be called at the corresponding stages of the execution sequence
	COMPONENT_INIT(init);
	COMPONENT_UPDATE(update);
	
	// link to the screen GUI
	Unigine::GuiPtr screenGui = nullptr;
${#HL}$
	void updateHealthInfo(int health); ${HL#}$
protected:
	Unigine::WidgetSpritePtr sprite = nullptr;
${#HL}$
	// label to display player's health
	Unigine::WidgetLabelPtr label = nullptr; ${HL#}$
	Unigine::Math::ivec2 prev_size;
	// world main loop overrides
	void init();
	void update();
};

HUD.cpp

Source code (C++)
#include "HUD.h"
#include <UnigineGame.h>
REGISTER_COMPONENT(HUD);
using namespace Unigine;
using namespace Math;

void HUD::init()
{
	// get the current screen GUI
	screenGui = Gui::getCurrent();

	// add WidgetSprite for crosshair
	if (crosshairImage != "")
		sprite = WidgetSprite::create(screenGui, Unigine::FileSystem::guidToPath(FileSystem::getGUID(crosshairImage.getRaw())));
	// set the sprite size
	sprite->setWidth(crosshairSize);
	sprite->setHeight(crosshairSize);
	// add the sprite to GUI so that it would always be in the center of the screen and overlap other widgets
	screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	// bind the widget lifetime to the world
	sprite->setLifetime(Widget::LIFETIME_WORLD);

${#HL}$
	// adding a WidgetLabel to display player's health and setting font size
	label = WidgetLabel::create(screenGui, "");
	label->setFontSize(50);
	label->setPosition(10, 10);
	label->setLifetime(Widget::LIFETIME_WORLD);

	// adding the widget to GUI
	screenGui->addChild(label, Gui::ALIGN_TOP | Gui::ALIGN_LEFT | Gui::ALIGN_OVERLAP); ${HL#}$
}

void HUD::update()
{
	ivec2 new_size = screenGui->getSize();
	if (prev_size != new_size)
	{
		screenGui->removeChild(sprite);
		screenGui->addChild(sprite, Gui::ALIGN_CENTER | Gui::ALIGN_OVERLAP);
	}
	prev_size = new_size;
}

${#HL}$
// updating the player's current health level
void HUD::updateHealthInfo(int health)
{
	label->setText(Unigine::String::format("Health: %d", health));
} ${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.

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