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.cs component and copy the following code into it:

    Health.cs

    Source code (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Health : Component
    {
    	public int health = 5;	// initial health level
    
    	public bool IsDead => health <= 0;	//  flag indicating that the health value is less or equal to 0
    
    	public void TakeDamage(int damage)
    	{
    		// apply damage
    		health = MathLib.Max(health - damage, 0);
    	}
    }
  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.cs component logic, we need to modify number of components.

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

    Source code (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class WeaponController : Component
    {
    	public PlayerDummy shootingCamera = null;
    	public ShootInput shootInput = null;
    	public NodeDummy weaponMuzzle = null;
    	public VFXController vfx = null;
    	public int damage = 1;
    	// Intersection mask to define which objects bullets can hit
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		if (weaponMuzzle)
    			vfx.OnShoot(weaponMuzzle.WorldTransform);
    		// 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.WorldPosition;
    		Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection()  * 100;
    
    		// create an intersection-normal storage object
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// get the first object intersected by the (p0,p1) line
    		Unigine.Object 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.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    			// генерируем в точке попадания NodeReference визуального эффекта
    			vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject);
    ${#HL}$			// применяем ущерб
    			Health health = hitObject.GetComponent<Health>();
    			if (health)
    				health.TakeDamage(damage); ${HL#}$
    		}
    	}
    
    	private void Update()
    	{
    		// handle user input: check if the 'fire' button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }

    In Bullet.cs, 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 characher and update the health information:

    Source code (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    #region Math Variables
    #if UNIGINE_DOUBLE
    using Vec3 = Unigine.dvec3;
    #else
    using Vec3 = Unigine.vec3;
    #endif
    #endregion
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class Bullet : Component
    {
    	public float speed = 10.0f;
    	public int damage = 1;
    
    	public AssetLink hitPrefab = null;
    
    	[ParameterMask]
    	public int intersectionMask = ~0;
    
    	private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    
    	private void Update()
    	{
    		// set the current bullet position
    		Vec3 currentPosition = node.WorldPosition;
    		// set the direction of the bullet movement along the Y axis
    		vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y);
    
    		// update bullet position along the trajectory according to the set speed
    		node.WorldPosition += currentDirection * speed * Game.IFps;
    
    		 // find the intersection of the bullet's trajectory with some of objects
    		Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo);
    
    		// if intersections weren't found, do nothing 
    		if (!hitObject)
    			return;
    
    		// otherwise load NodeReference with hit effect
    		Node hitEffect = World.LoadNode(hitPrefab.AbsolutePath);
    		// set NodeReference to the hit point and set its direction according to the hit normal
    		hitEffect.Parent = hitObject;
    		hitEffect.WorldPosition = hitInfo.Point;
    		hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y);
    ${#HL}$		// check the hitObject, whether it is a player and whether it has a component Health
    		Health health = hitObject.GetComponent<Health>();
    		if (health && hitObject.GetComponentInParent<PlayerLogic>())
    		{
    			// apply damage from the bullet
    			health.TakeDamage(damage);
    
    			// update player health info in HUD
    			ComponentSystem.FindComponentInWorld<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:

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

#region Math Variables
#if UNIGINE_DOUBLE
using Vec3 = Unigine.dvec3;
#else
using Vec3 = Unigine.vec3;
#endif
#endregion

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

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
public class EnemyLogic : Component
{
	public Node player = null;
	public Node intersectionSocket = null;
	public float reachRadius = 0.5f;
	public float attackInnerRadius = 5.0f;
	public float attackOuterRadius = 7.0f;
	public float speed = 1.0f;
	public float rotationStiffness = 8.0f;
	public float routeRecalculationInterval = 3.0f;

	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
	public int playerIntersectionMask = ~0;

	// initialize the enemy state
	private EnemyLogicState currentState = EnemyLogicState.Idle;
	private bool targetIsVisible;
	private Vec3 lastSeenPosition;
	private vec3 lastSeenDirection;
	private float lastSeenDistanceSqr;

	private BodyRigid bodyRigid = null;
	private WorldIntersection hitInfo = new WorldIntersection();
	private Node[] hitExcludes = new Node[2];

	private EnemyFireController fireController = null;
${#HL}$	private Health health = null; ${HL#}$
	// create a queue for route points
	private Queue<Vec3> calculatedRoute = new Queue<Vec3>();

	private PathRoute route = new PathRoute();
	private bool shouldUpdateRoute = true;
	private float lastCalculationTime = 0.0f;
	private bool IsTargetVisible()
	{
		Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition);
		Vec3 p0 = intersectionSocket.WorldPosition;
		Vec3 p1 = p0 + direction;

		Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo);
		if (!hitObject)
			return false;

		return player.ID == hitObject.ID;
	}

	private void Init()
	{
		// initialize the parameters of the point moving along the path within navigational mesh 
		route.Radius = 0.0f;
		route.Height = 1.0f;
		route.MaxAngle = 0.5f;

		bodyRigid = node.ObjectBodyRigid;
		hitExcludes[0] = node;
		hitExcludes[1] = node.GetChild(0);

		targetIsVisible = false;
		// get the EnemyFireController component
		fireController = node.GetComponent<EnemyFireController>();
${#HL}$		// get the Health component
		health = node.GetComponentInChildren<Health>(); ${HL#}$
		shouldUpdateRoute = true;
		lastCalculationTime = Game.Time;
	}

	private void 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();

		// enemy state switching
		switch (currentState)
		{
			case EnemyLogicState.Idle: ProcessIdleState(); break;
			case EnemyLogicState.Chase: ProcessChaseState(); break;
			case EnemyLogicState.Attack: ProcessAttackState(); break;
		}

		// change color depending on the current state
		vec4 color = vec4.BLACK;
		switch (currentState)
		{
			case EnemyLogicState.Idle: color = vec4.BLUE; break;
			case EnemyLogicState.Chase: color = vec4.YELLOW; break;
			case EnemyLogicState.Attack: color = vec4.RED; break;
		}

		// visualize the enemy states
		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 2.0f, 0.25f, color);
		Visualizer.RenderPoint3D(node.WorldPosition + vec3.UP * 3.0f, 0.25f, IsTargetVisible() ? vec4.GREEN : vec4.RED);
		Visualizer.RenderPoint3D(lastSeenPosition, 0.1f, vec4.MAGENTA);

		// visualize the attack radius
		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);

		// visualize the route points
		foreach (vec3 route_point in calculatedRoute)
			Visualizer.RenderPoint3D(route_point + vec3.UP, 0.25f, vec4.BLACK);

	}
	private void UpdateRoute()
	{
		if (Game.Time - lastCalculationTime < routeRecalculationInterval)
			return;

		if (shouldUpdateRoute)
		{
			// calculate the route to the player
			route.Create2D(node.WorldPosition, 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.NumPoints; ++i)
					calculatedRoute.Enqueue(route.GetPoint(i));

				shouldUpdateRoute = true;
				lastCalculationTime = Game.Time;
			}
			else
				// recalculate the route if the target point wasn't reached
				shouldUpdateRoute = true;
		}
	}

	private void UpdateTargetState()
	{
		targetIsVisible = IsTargetVisible();
		// if the player is visible, remember his latest registered position
		if (targetIsVisible)
			lastSeenPosition = player.WorldPosition;

		lastSeenDirection = (vec3)(lastSeenPosition - node.WorldPosition)
		lastSeenDistanceSqr = lastSeenDirection.Length2;
		lastSeenDirection.Normalize();
	}

	private void UpdateOrientation()
	{
		vec3 direction = lastSeenDirection;
		direction.z = 0.0f;

		quat targetRotation = new quat(MathLib.SetTo(vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y));
		quat currentRotation = node.GetWorldRotation();

		currentRotation = MathLib.Slerp(currentRotation, targetRotation, Game.IFps * rotationStiffness);
		node.SetWorldRotation(currentRotation);
	}

	private void ProcessIdleState()
	{
		// if the target is visible (player) - shift Idle -> Chase
		if (targetIsVisible)
			currentState = EnemyLogicState.Chase;
	}

	private void ProcessChaseState()
	{
		vec3 currentVelocity = bodyRigid.LinearVelocity;
		currentVelocity.x = 0.0f;
		currentVelocity.y = 0.0f;
		if (calculatedRoute.Count > 0)
		{
			float distanceToTargetSqr = (calculatedRoute.Peek() - node.WorldPosition).Length2;

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

			if (calculatedRoute.Count > 0)
			{
				vec3 direction = calculatedRoute.Peek() - node.WorldPosition;
				direction.z = 0.0f;
				direction.Normalize();
				currentVelocity.x = direction.x * speed;
				currentVelocity.y = direction.y * speed;
			}
		}

		// if the target isn't visible - shift Chase -> Idle
		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.LinearVelocity = currentVelocity;
	}

	private void ProcessAttackState()
	{
		// check distance and shift Attack -> Chase
		if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius )
		{
			currentState = EnemyLogicState.Chase;
			// stop shooting
			if (fireController)
				fireController.StopFiring();
		}
	}
}

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.cs component and add the following code into it:

    Source code (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class PlayerLogic : Component
    {
    	private Health health = null;
    	[Method(Order=2)]
    	private void Init()
    	{
    		// take the Health component from a node
    		health = node.GetComponentInChildren<Health>();
    		// обновляем информацию об исходном здоровье игрока
    		ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
    	}
    
    	private void Update()
    	{
    		// проверяем выставлен ли флаг IsDead
    		if (health && health.IsDead)
    		{
    			// make the player immovable by disabling the components
    			node.GetComponent<FirstPersonController>().Enabled = false;
    			node.GetComponent<WeaponController>().Enabled = false;
    			node.GetComponent<ShootInput>().Enabled = false;
    		}
    	}
    }
  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.cs file:

Source code (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
public class HUD : Component
{
	// crosshair parameters
	public AssetLink crosshairImage = null;
	public int crosshairSize = 16;

${#HL}$	private WidgetLabel label = null; ${HL#}$
	private WidgetSprite sprite = null;
	private Gui screenGui = null;
	ivec2 prev_size;

	[Method(Order=1)]
	private void Init()
	{
		// get the current screen GUI
		screenGui = Gui.GetCurrent();

		// add WidgetSprite for crosshair
		sprite = new WidgetSprite(screenGui, crosshairImage.AbsolutePath);
		// set the sprite size
		sprite.Width = crosshairSize;
		sprite.Height = 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.Lifetime = Widget.LIFETIME.WORLD;
${#HL}$		// добавляем виджет WidgetLabel для отображения здоровья игрока, устанавливаем его положение размер шрифта
		label = new WidgetLabel(screenGui, "");
		label.FontSize = 50;
		label.SetPosition(10,10);
		label.Lifetime = Widget.LIFETIME.WORLD;

		// add widget to GUI
		screenGui.AddChild(label, Gui.ALIGN_TOP| Gui.ALIGN_LEFT | Gui.ALIGN_OVERLAP); ${HL#}$
	}
${#HL}$	// updating the player's current health level
	public void UpdateHealthInfo(int health)
	{
		label.Text = "Health: " + health.ToString();
	} ${HL#}$
	private void Update()
	{
		ivec2 new_size = screenGui.Size;
		if (prev_size != new_size)
		{
			screenGui.RemoveChild(sprite);
			screenGui.AddChild(sprite, Gui.ALIGN_CENTER | Gui.ALIGN_OVERLAP);
		}
		prev_size = new_size;
	}
}
Last update: 2024-03-27
Build: ()