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:
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
public enum GameState
{
Gameplay,
Win,
Lose,
}
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component
public class GameController : Component
{
public GameState state;
public Player EndCamera = null; // Camera for the game ending
private void Init()
{
// Set the initial state of the gameplay
state = GameState.Gameplay;
}
private void Update()
{
// if the game is over
if (state != GameState.Gameplay)
{
// switch to the camera for the game ending
Game.Player = EndCamera;
// display the message with the game results in HUD
ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state);
}
else
{
// if there are no more enemies, switch to the Win state
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
// updating the player's health state
public void DisplayStateMessage(GameState state)
{
// add WidgetLabel to display the game result message, set the size and font size
WidgetLabel end_message = new WidgetLabel(screenGui, (state == GameState.Win)?"VICTORY!":"YOU LOSE!");
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;
// end 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
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")] // <-- identifier 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 of 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 if the point that mives along the path inside the Navigation 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 paused, the enemy doesn't do anything
if (gameController.state != GameState.Gameplay)
return; ${HL#}$
// check the enemy's health
if (health && health.IsDead)
// remove the enemy if its health has been reduced to 0
node.DeleteLater();
UpdateTargetState();
UpdateOrientation();
UpdateRoute();
// switch between the enemy's states
switch (currentState)
{
case EnemyLogicState.Idle: ProcessIdleState(); break;
case EnemyLogicState.Chase: ProcessChaseState(); break;
case EnemyLogicState.Attack: ProcessAttackState(); break;
}
// switch the 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's 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 the route is calculated
if (route.IsReady)
{
// check if the target point of the route is reached
if (route.IsReached)
{
// clear the points queue
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 isn't reached
shouldUpdateRoute = true;
}
}
private void UpdateTargetState()
{
targetIsVisible = IsTargetVisible();
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 (player) is visible, transition Idle -> Chase
if (targetIsVisible)
{
// change the current state to Chase
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 (player) is not visible, transition Chase -> Idle
if (!targetIsVisible)
currentState = EnemyLogicState.Idle;
// check the distance, transition 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 the distance, transition Attack -> Chase
if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius)
{
currentState = EnemyLogicState.Chase;
// stop shooting
if (fireController)
fireController.StopFiring();
}
}
}
PlayerLogic.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component
public class PlayerLogic : Component
{
private Health health = null;
${#HL}$ private GameController gameController = null; ${HL#}$
private void Init()
{
// grab the Health component managing the player's health
health = node.GetComponentInChildren<Health>();
// update the player's health info
ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
${#HL}$ // find the GameController component
gameController = ComponentSystem.FindComponentInWorld<GameController>(); ${HL#}$
}
private void Update()
{
// check the player's health, and if it's killed, remove it and switch the game to the Lose state
if (health && health.IsDead)
{
${#HL}$ // remove the player
node.DeleteLater();
// change the gameplay state to Lose
gameController.state = GameState.Lose; ${HL#}$
}
${#HL}$ // check the game state, if it's over, remove the player
else if (gameController.state != GameState.Gameplay)
node.DeleteLater();
} ${HL#}$
}
-
Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.
- 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.
-
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.
-
To generate an arbitrary number of enemies, add a few lines to the GameController.cs component:
using System; using System.Collections; using System.Collections.Generic; using Unigine; public enum GameState { Gameplay, Win, Lose, } [Component(PropertyGuid = "99c186143ea2ba6374a0dc66c660c88d75065088")] public class GameController : Component { public GameState state; public Player EndCamera = null; ${#HL}$ public NodeDummy SpawnPoint = null; ${HL#}$ [ParameterFile] public AssetLink enemyPrefab = null; public int NumEnemies = 10; private int spawned_enemy_counter = 0; public float spawnInterval = 2.0f; private float currentTime = 0.0f; private void Init() { // set the initial gameplay state state = GameState.Gameplay; } private void Update() { // if the game is over if (state != GameState.Gameplay) { // switch to the camera for the game ending Game.Player = EndCamera; // displaying a resulting game message in the HUD ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state); } else { // if no enemies are left, switch to the Win state if (!ComponentSystem.FindComponentInWorld<EnemyLogic>() ${#HL}$ && spawned_enemy_counter == NumEnemies ${HL#}$) state = GameState.Win; ${#HL}$ // generate new enemies (enemyPrefab) in the specified SpawnPoint // with the specified spawnInterval if (spawned_enemy_counter < NumEnemies) { currentTime += Game.IFps; if (currentTime > spawnInterval) { currentTime -= spawnInterval; spawned_enemy_counter++; Node enemy = World.LoadNode(enemyPrefab.AbsolutePath); enemy.WorldTransform = SpawnPoint.WorldTransform; } } ${HL#}$ } } }
- Create the Node Dummy node and place it to the point where new enemies will appear and name it spawn_point.
-
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!