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

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

public enum GameState
{
	Gameplay,
	Win,
	Lose,
}

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
public class GameController : Component
{
	public GameState state;
	public Player EndCamera = null;	// Camera for the game final
	private void Init()
	{
		// set the initial state of the game
		state = GameState.Gameplay;
	}

	private void Update()
	{
		// if the game is over
		if (state != GameState.Gameplay)
		{
			// switch to the camera for the game final
			Game.Player = EndCamera;
			// show the message about the game end in HUD
			ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state);
		}
		else
		{
			// if there are no more enemies left, go to the state 'Win'
			if (!ComponentSystem.FindComponentInWorld<EnemyLogic>())
				state = GameState.Win;
		}
	}
}

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

HUD.cs

Source code (C#)
// displaying the message about game's results
public void DisplayStateMessage(GameState state)
{
	// add WidgetLabel to display the final result message, set font size and color
	WidgetLabel end_message = new WidgetLabel(screenGui, (state == GameState.Win) ? "Победа!" : "Вы проиграли!");
	end_message.FontSize = 100;
	end_message.FontColor = vec4.RED;
	screenGui.AddChild(end_message, Gui.ALIGN_CENTER | Gui.ALIGN_OVERLAP);
	// bind the widget lifetime to the world
	end_message.Lifetime = Widget.LIFETIME.WORLD;
	
	// finalize the process
	ComponentSystem.FindComponentInWorld<GameController>().Enabled = false;
}

Next, modify code in the EnemyLogic.cs and PlayerLogic.cs components to use the logic of GameCotroller.cs:

EnemyLogic.cs

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;
${#HL}$	private GameController gameController = null; ${HL#}$
	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;
	private Health health = null;
	// 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>();
		// get the Health component
		health = node.GetComponentInChildren<Health>();
		shouldUpdateRoute = true;
		lastCalculationTime = Game.Time;
${#HL}$		// find the GameController component 
		gameController = ComponentSystem.FindComponentInWorld<GameController>(); ${HL#}$
	}

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

		// 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 = (float)(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();
		}
	}
}

PlayerLogic.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 PlayerLogic : Component
{
	private Health health = null;
${#HL}$	private GameController gameController = null; ${HL#}$
	[Method(Order=2)]
	private void Init()
	{
		// take the Health component from a node
		health = node.GetComponentInChildren<Health>();
		// обновляем информацию об исходном здоровье игрока
		ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
${#HL}$		// take the link to the gameplay manager (GameController)
		gameController = ComponentSystem.FindComponentInWorld<GameController>(); ${HL#}$
	}

	private void Update()
	{
${#HL}$		// check the player's health and, if he was killed, remove him and switch the game to the 'Defeat' state
		if (health && health.IsDead)
		{
			// remove the player
			node.DeleteLater();

			// switch the gameplay state to Lose
			gameController.state = GameState.Lose; ${HL#}$
		}
${#HL}$		// check the game state, if completed, remove the player
		else if (gameController.state != GameState.Gameplay)
			node.DeleteLater(); ${HL#}$
	}
}
  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.cs component:

    Source code (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    public enum GameState
    {
    	Gameplay,
    	Win,
    	Lose,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
    public class GameController : Component
    {
    	public GameState state;
    	public Player EndCamera = null;	// Camera for the game final
    ${#HL}$	public NodeDummy SpawnPoint = null; // enemy generation point 
    
    	public AssetLink enemyPrefab = null; // .node asset with enemy template
    	public int NumEnemies = 10;
    
    	private int spawned_enemy_counter = 0;
    	public float spawnInterval = 2.0f;
    	private float currentTime = 0.0f; ${HL#}$
    	private void Init()
    	{
    		// set the initial state of the game
    		state = GameState.Gameplay;
    	}
    
    	private void Update()
    	{
    		// if the game is over
    		if (state != GameState.Gameplay)
    		{
    			// switch to the camera for the game final
    			Game.Player = EndCamera;
    			// show the message about the game end in HUD
    			ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state);
    		}
    		else
    		{
    			// if there are no more enemies left, go to the state 'Win'
    ${#HL}$			if (!ComponentSystem.FindComponentInWorld<EnemyLogic>() && 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)
    			{
    				currentTime += Game.IFps;
    
    				if (currentTime > spawnInterval)
    				{
    					currentTime -= spawnInterval;
    					spawned_enemy_counter++;
    					World.LoadNode(enemyPrefab.AbsolutePath).Transform = SpawnPoint.WorldTransform;
    				}
    			} ${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-08-16
Build: ()