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

Здоровье и урон при попадании

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

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

    Health.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
    public class Health : Component
    {
    	public int health = 5;	// начальный уровень здоровья
    
    	public bool IsDead => health <= 0;	// флаг, проверяющий, не достиг ли текущий уровень здоровья 0
    
    	public void TakeDamage(int damage)
    	{
    		// применяем ущерб
    		health = MathLib.Max(health - damage, 0);
    	}
    }
  2. Add it to the visuals node of the robot_enemy node. Добавьте его в ноду visuals ноды robot_enemy.

  3. Add it to the player_hit_box node of the player node.Добавьте его в ноду player_hit_box ноды player.
  4. In order to use the Health.cs component logic, we need to modify number of components.Чтобы использовать логику компонента Health.cs. надо внести изменения в код ряда компонентов.

    In WeaponController.cs, add the following several lines to the part detecting that the player has hit an object:В WeaponController.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
    
    [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 чтобы определить, в какие объекты могут попадать пули
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void Shoot()
    	{
    		if (weaponMuzzle)
    			vfx.OnShoot(weaponMuzzle.WorldTransform);
    		// задаем начало отрезка (p0) в позиции камеры и конец (p1) - в точке удаленной на 100 единиц в направлении взгляда камеры
    		Vec3 p0 = shootingCamera.WorldPosition;
    		Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection()  * 100;
    
    		// создаем объект для хранения intersection-нормали
    		WorldIntersectionNormal hitInfo = new WorldIntersectionNormal();
    		// ищем первый объект, который пересекает отрезок (p0, p1)
    		Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo);
    		// если пересечение найдено
    		if (hitObject)
    		{
    			// отрисовываем нормаль к поверхности в точке попадания при помощи 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()
    	{
    		// обработка пользовательского ввода: проверяем нажата ли клавиша 'огонь'
    		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:В Bullet.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
    
    [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
    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()
    	{
    		// устанавливаем текущую позицию пули
    		Vec3 currentPosition = node.WorldPosition;
    		// устанавливаем направление движения пули вдоль оси Y
    		vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y);
    
    		// обновляем положение пули вдоль траектории в соответствии с заданной скоростью
    		node.WorldPosition += currentDirection * speed * Game.IFps;
    
    		 // ищем пересечение траектории пули с каким-либо объектом
    		Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo);
    
    		// если пересечений не найдено, ничего не делаем
    		if (!hitObject)
    			return;
    
    		// иначе загружаем NodeReference с эффектом попадания
    		Node hitEffect = World.LoadNode(hitPrefab.AbsolutePath);
    		// устанавливаем NodeReference в точку попадания и ориентируем его по нормали к поверхности
    		hitEffect.Parent = hitObject;
    		hitEffect.WorldPosition = hitInfo.Point;
    		hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y);
    ${#HL}$		// проверяем объект, в котороый попали (hitObject), игрок ли это и есть ли у него компонента Health
    		Health health = hitObject.GetComponent<Health>();
    		if (health && hitObject.GetComponentInParent<PlayerLogic>())
    		{
    			// применяем ущерб от пули
    			health.TakeDamage(damage);
    
    			// обновляем информацию о здоровье игрока в HUD
    			ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
    		} ${HL#}$
    
    		// удаляем пулю
    		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:Роботы, здоровье которых становится равным нулю, удаляются со сцены. У компонента Health есть флаг IsDead, который проверяется компонентом логики вражеского робота (EnemyLogic). Если флаг равен true, то нода соответствующего робота будет удалена. Для этого в компоненту EnemyLogic добавим свойство health, проинициализируем его в Init() и каждый кадр в Update() будем проверять уровень здоровья вражеского робота и удалять его при необходимости:

Исходный код (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;
	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#}$
	// создаем очередь для точек пути
	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>();
${#HL}$		// получаем компонент Health
		health = node.GetComponentInChildren<Health>(); ${HL#}$
		shouldUpdateRoute = true;
		lastCalculationTime = Game.Time;
	}

	private void Update()
	{
${#HL}$		// проверяем здоровье врага
		if (health && health.IsDead)
			// удаляем врага, если его здоровье уменьшилось до нуля
			node.DeleteLater(); ${HL#}$

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

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:Создайте компонент 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;
    	[Method(Order=2)]
    	private void Init()
    	{
    		// берем у ноды компонент Health
    		health = node.GetComponentInChildren<Health>();
    		// обновляем информацию об исходном здоровье игрока
    		ComponentSystem.FindComponentInWorld<HUD>().UpdateHealthInfo(health.health);
    	}
    
    	private void Update()
    	{
    		// проверяем выставлен ли флаг IsDead
    		if (health && health.IsDead)
    		{
    			// обездвиживаем игрока, отключая компоненты
    			node.GetComponent<FirstPersonController>().Enabled = false;
    			node.GetComponent<WeaponController>().Enabled = false;
    			node.GetComponent<ShootInput>().Enabled = false;
    		}
    	}
    }
  2. Add the PlayerLogic component to the player node.Добавьте компонент PlayerLogic к ноде player.

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:Добавим также отображение информации о здоровье игрока в HUD. Для этого допишем несколько строчек в метод Init() и добавим метод UpdateHealthInfo() для обновления значения в виджете GUI в файле HUD.cs:

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

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class HUD : Component
{
	// параметры прицела
	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()
	{
		// получаем текущий экранный GUI
		screenGui = Gui.GetCurrent();

		// создаем виджет WidgetSprite для прицела
		sprite = new WidgetSprite(screenGui, crosshairImage.AbsolutePath);
		// задаем размер спрайта
		sprite.Width = crosshairSize;
		sprite.Height = crosshairSize;
		// добавляем спрайт к GUI так, чтобы он всегда был посередине экрана и поверх всех остальных виджетов
		screenGui.AddChild(sprite, Gui.ALIGN_CENTER | Gui.ALIGN_OVERLAP);
		// привязываем время жизни виджета к миру
		sprite.Lifetime = Widget.LIFETIME.WORLD;
${#HL}$		// добавляем виджет WidgetLabel для отображения здоровья игрока, устанавливаем его положение размер шрифта
		label = new WidgetLabel(screenGui, "");
		label.FontSize = 50;
		label.SetPosition(10,10);
		label.Lifetime = Widget.LIFETIME.WORLD;

		// добавляем виджет к GUI
		screenGui.AddChild(label, Gui.ALIGN_TOP| Gui.ALIGN_LEFT | Gui.ALIGN_OVERLAP); ${HL#}$
	}
${#HL}$	// обновление текущего уровня здоровья игрока
	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;
	}
}
Последнее обновление: 27.03.2024
Build: ()