This page has been translated automatically.
Основы UNIGINE
1. Введение
2. Виртуальные миры и работа с ними
3. Подготовка 3D моделей
4. Материалы
5. Камеры и освещение
6. Реализация логики приложения
7. Создание кат-сцен и запись видео
8. Подготовка проекта к релизу
9. Физика
10. Основы оптимизации
12. ПРОЕКТ3: Аркадная гонка по пересеченной местности от 3-го лица
13. ПРОЕКТ4: VR приложение с простым взаимодействием

Управление игровым процессом

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.Реализация компонента-менеджера GameController, осуществляющего переключение между состояниями игры в зависимости от событий: уничтожение всех противников, гибель игрока, истечение времени игры.

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.Для переключения между состояниями Gameplay и Win/Lose у нас есть компонент GameController.

Create the GameController.cs component and copy the following code into it:Создайте компонент GameController.cs и скопируйте следующий код:

Исходный код (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

public enum GameState
{
	Gameplay,
	Win,
	Lose,
}

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class GameController : Component
{
	public GameState state;
	public Player EndCamera = null;	// Камера для финала игры
	private void Init()
	{
		// Задаем начальное состояние игрового процесса
		state = GameState.Gameplay;
	}

	private void Update()
	{
		// если игра окончена
		if (state != GameState.Gameplay)
		{
			// переключаемся на камеру для финала игры
			Game.Player = EndCamera;
			// показываем сообщение об итоге игры в HUD
			ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state);
		}
		else
		{
			// если врагов больше не осталось, переходим в состояние 'Победа' (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 новый метод DisplayStateMessage(), чтобы отобразить результат игры.

HUD.cs

Исходный код (C#)
// отображение сообщения о результате игры
public void DisplayStateMessage(GameState state)
{
	// добавляем виджет WidgetLabel для отображения финального сообщение о результате игры, устанавливаем размер и цвет шрифта
	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);
	// привязываем время жизни виджета к миру
	end_message.Lifetime = Widget.LIFETIME.WORLD;
	
	// завершаем процесс
	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 и PlayerLogic.cs, чтобы использовалась логика GameCotroller.cs.

EnemyLogic.cs

Исходный код (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

// определяем состояния врага
public enum EnemyLogicState
{
	Idle,
	Chase,
	Attack,
}

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
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;

	// инициализируем состояние врага
	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;
	// создаем очередь для точек пути
	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()
	{
		// инициализируем параметры точки, движущейся по пути в пределах навигационного меша 
		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;
		// получаем компонент EnemyFireController
		fireController = node.GetComponent<EnemyFireController>();
		// получаем компонент Health
		health = node.GetComponentInChildren<Health>();
		shouldUpdateRoute = true;
		lastCalculationTime = Game.Time;
${#HL}$		// находим компонент GameController
		gameController = ComponentSystem.FindComponentInWorld<GameController>(); ${HL#}$
	}

	private void Update()
	{
${#HL}$		// Проверяем текущее состояние, если игровой процесс остановлен, то враг не выполняет никаких действий
		if (gameController.state != GameState.Gameplay)
			return; ${HL#}$
		// проверяем здоровье врага
		if (health && health.IsDead)
			// удаляем врага, если его здоровье уменьшилось до нуля
			node.DeleteLater();

		UpdateTargetState();
		UpdateOrientation();
		UpdateRoute();

		// переключение состояний врага
		switch (currentState)
		{
			case EnemyLogicState.Idle: ProcessIdleState(); break;
			case EnemyLogicState.Chase: ProcessChaseState(); break;
			case EnemyLogicState.Attack: ProcessAttackState(); break;
		}

		// переключение цвета в зависимости от текущего состояния
		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;
		}

		// визуализируем состояния врага
		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);

		// визуализируем радиус атаки
		Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED);
		Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED);

		// визуализируем точки маршрута
		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)
		{
			// рассчитываем путь до игрока
			route.Create2D(node.WorldPosition, lastSeenPosition, 1);
			shouldUpdateRoute = false;
		}

		// если расчет пути окончен
		if (route.IsReady)
		{
			// проверяем, не достигнута ли целевая точка
			if (route.IsReached)
			{
				// очищаем очередь точек пути
				calculatedRoute.Clear();

				// добавляем все корневые точки в очередь
				for(int i = 1; i < route.NumPoints; ++i)
					calculatedRoute.Enqueue(route.GetPoint(i));

				shouldUpdateRoute = true;
				lastCalculationTime = Game.Time;
			}
			else
				// пересчитываем путь, если целевая точка не была достигнута
				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()
	{
		// если видна цель (игрок) - переход Бездействие -> Преследование (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 (!targetIsVisible)
			currentState = EnemyLogicState.Idle;

		// проверка дистанции и переход Преследование -> Атака
		else if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius)
		{
			currentState = EnemyLogicState.Attack;
			currentVelocity.x = 0.0f;
			currentVelocity.y = 0.0f;
			// начинаем стрельбу
			if (fireController)
				fireController.StartFiring();
		}

		bodyRigid.LinearVelocity = currentVelocity;
	}

	private void ProcessAttackState()
	{
		// проверка дистанции и переход Атака -> Преследование
		if (!targetIsVisible || lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius )
		{
			currentState = EnemyLogicState.Chase;
			// прекращаем стрельбу
			if (fireController)
				fireController.StopFiring();
		}
	}
}

PlayerLogic.cs

Исходный код (C#)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class PlayerLogic : Component
{
	private Health health = null;
${#HL}$	private GameController gameController = null; ${HL#}$
	[Method(Order=2)]
	private void Init()
	{
		// берем у ноды компонент Health
		health = node.GetComponentInChildren<Health>();
		// обновляем информацию об исходном здоровье игрока
		ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
${#HL}$		// берем ссылку на менеджер игрового процесса (GameController)
		gameController = ComponentSystem.FindComponentInWorld<GameController>(); ${HL#}$
	}

	private void Update()
	{
${#HL}$		// проверяем здоровье игрока и, если он убит, удаляем его с переключением игры в состояние 'Поражение'
		if (health && health.IsDead)
		{
			// удаляем игрока
			node.DeleteLater();

			// меняем состояние игрового процесса на (Lose - поражение)
			gameController.state = GameState.Lose; ${HL#}$
		}
${#HL}$		// проверяем состояние игры, если она окончена, удаляем игрока
		else if (gameController.state != GameState.Gameplay)
			node.DeleteLater(); ${HL#}$
	}
}
  1. Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.Создадим NodeDummy, назовем его gameplay_systems и назначим ему компонент GameController.

  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.Теперь для финала игры создадим отдельную камеру, которая будет смотреть на сцену сверху. Выберите в меню Create -> Camera -> Player Dummy. Переименуйте камеру в end_camera. Переключитесь на нее в Редакторе и, управляя камерой, выберите желаемый вид сцены.
  3. Drag the end_camera node to the End Camera field of the GameController component assigned to the gameplay_systems node.Перетащите ноду end_camera в поле End Camera компонента GameController ноды gameplay_systems.

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:Для генерации произвольного количества врагов можно добавить немного кода в компонент GameController.cs таким образом:

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    public enum GameState
    {
    	Gameplay,
    	Win,
    	Lose,
    }
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
    public class GameController : Component
    {
    	public GameState state;
    	public Player EndCamera = null;	// Камера для финала игры
    ${#HL}$	public NodeDummy SpawnPoint = null; // точка генерации врагов 
    
    	public AssetLink enemyPrefab = null; // .node-ассет с шаблоном врага
    	public int NumEnemies = 10;
    
    	private int spawned_enemy_counter = 0;
    	public float spawnInterval = 2.0f;
    	private float currentTime = 0.0f; ${HL#}$
    	private void Init()
    	{
    		// Задаем начальное состояние игрового процесса
    		state = GameState.Gameplay;
    	}
    
    	private void Update()
    	{
    		// если игра окончена
    		if (state != GameState.Gameplay)
    		{
    			// переключаемся на камеру для финала игры
    			Game.Player = EndCamera;
    			// показываем сообщение об итоге игры в HUD
    			ComponentSystem.FindComponentInWorld<HUD>().DisplayStateMessage(state);
    		}
    		else
    		{
    			// если врагов больше не осталось, переходим в состояние 'Победа' (Win)
    ${#HL}$			if (!ComponentSystem.FindComponentInWorld<EnemyLogic>() && spawned_enemy_counter == NumEnemies) ${HL#}$
    				state = GameState.Win;
    ${#HL}$			// генерируем новых врагов (enemyPrefab) в заданной точке (SpawnPoint) с заданным интервалом времени (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.Создайте ноду Node Dummy и поместите ее в точку, где будут появляться новые враги, и назовите ее 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.Затем перетащите ноду spawn_point в поле Spawn Point, а ассет robot_enemy.node – в поле Enemy Prefab, а также задайте количество врагов и интервал их появления в секундах.

Now, let's get down to business!А, теперь – за дело!

Последнее обновление: 27.03.2024
Build: ()