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
注意! 这个版本的文档是过时的,因为它描述了一个较老的SDK版本!请切换到最新SDK版本的文档。
注意! 这个版本的文档描述了一个不再受支持的旧SDK版本!请升级到最新的SDK版本。

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")] // <-- 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

Source code (C#)
// 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

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")] // <-- 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

Source code (C#)
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#}$
    
}
  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 = "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#}$
           	 }
        }
    }
  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-04-04
Build: ()