Создание шутера от первого лица (C#)
This tutorial shows how to create a First-Person Shooter.В этом руководстве показано, как создать шутер от первого лица.
Using the C# component system, we will create a controllable player who can fire a gun with a crosshair in the center of the screen, and enemies pursuing the player and trying to shoot him.Используя компонентную систему C#, мы создадим управляемого игрока, который может стрелять из пистолета с прицелом в центре экрана, и врагов, преследующих игрока и пытающихся его застрелить.
Topics covered in this article include:Темы, затронутые в этой статье, включают:
- Setting up a first-person view of the weapon Настройка вида оружия от первого лица
- Implementing shooting and simple hit effects Реализация эффектов стрельбы и простого попадания
- Making a HUD for the crosshair Создание HUD для перекрестия
- Creating basic logic of enemies with different states Создание базовой логики врагов с разными состояниями
- Using a navigation mesh for pathfinding Использование навигационной сетки для поиска пути
- Adding win / lose logic Добавление логики победы/проигрыша
Creating a New Project and Downloading AssetsСоздание нового проекта и загрузка ассетов#
We are going to use the previously made assets which are available for downloading on Add-On Store.Мы собираемся использовать ранее созданные ассеты, которые доступны для загрузки через браузер SDK.
- Create a new empty C# project. Open the SDK Browser, go to the My Projects tab and click the Create New button. Создайте новый пустой проект C#. Откройте браузер SDK, перейдите на вкладку My Projects и нажмите кнопку Create New.
-
In the window that opens, make sure to select C# (.NET) in the API + IDE list and click Create New Project.В открывшемся окне обязательно выберите C# (.NET) в списке API + IDE и нажмите Create New Project.
-
After the new project is created, it will appear in the My Projects tab. Click Open Editor under the created project to open it in the UnigineEditor.После создания нового проекта он появится на вкладке My Projects. Нажмите Open Editor под созданным проектом, чтобы открыть его в UnigineEditor.
-
Download the Docs Sample Content add-on from Add-On Store at https://store.unigine.com/ and add it to the project by dragging into the project data/ folder in the Asset Browser. In the Import Package window that opens, click the Import Package button and wait until the add-on contents are imported.Скачайте дополнение Docs Sample Content с сайта Add-On Store (https://store.unigine.com/) и добавьте его в проект: перетащите в папку data/ в браузере ассетов, в открывшемся окне Import Package нажмите кнопку Import Package и дождитесь окончания импорта содержимого.
Creating a PlayerСоздание игрока#
To make a main character, we will need a controller node implementing basic player functionality (handling keyboard and mouse input, movement settings, etc.). Attached to this node, we will have a first-person view camera, hands with a gun, and a body imitation to check for collisions with other characters, bullets, and environment. Later we will assign logic components to the nodes to implement shooting, visual effects, etc.Чтобы создать главного персонажа, нам понадобится нода контроллера, реализующая базовые функции игрока (обработка ввода с клавиатуры и мыши, настройки движения и т.д.). К этой ноде у нас будет привязана камера с видом от первого лица, руки с пистолетом и имитация тела для проверки столкновений с другими персонажами, пулями и окружающей средой. Позже мы назначим нодам логические компоненты для реализации съемки, визуальных эффектов и т.д.
Character ControllerКонтроллер персонажа#
For the character controller, we will use the template first-person controller. It is included in the default scene of a new C# project as the first_person_controller node reference that stores a dummy object with the FirstPersonController component assigned.Для контроллера персонажа мы будем использовать шаблон контроллера от первого лица. Он по умолчанию добавлен в сцену нового проекта C# как Node Reference на first_person_controller, в которой хранится Dummy Object с назначенным компонентом FirstPersonController.
Let's rename it player: enable editing of the node reference and rename both nodes. It will represent our character.Давайте переименуем его в player: включите редактирование Node Reference и переименуйте обе ноды. Он будет представлять наш персонаж.
Arranging a First-Person Setup with Hands and PistolНастройка вида от первого лица с помощью рук и пистолета#
For a first-person setup you will need hands and weapon models and animations previously created in a 3D modeling software. Our ready-to-use assets are available in the data/fps folder.Для настройки вида от первого лица вам понадобятся модели рук и оружия, а также анимации, ранее созданные в программном обеспечении для 3D-моделирования. Наши готовые к использованию ассеты доступны в папке data/fps.
Adding HandsДобавление рук#
We start with adding hands, and then we will attach a pistol to them.Мы начнем с добавления рук, а затем прикрепим к ним пистолет.
In the Asset Browser, find the data/fps/hands/hands.fbx asset and add it to the scene.В браузере ассетов найдите ассет data/fps/hands/hands.fbx и добавьте его в сцену.
Player BodyТело игрока#
To simulate a player's body that takes damage when hit by enemy bullets, you can use an additional node:Чтобы имитировать тело игрока, которое получает урон при попадании вражеских пуль, вы можете использовать дополнительную ноду:
-
In the Menu bar, choose Create -> Primitive -> Box to create a box primitive of the (1,1,2) size, add it to the scene and rename player_hit_box.В строке меню выберите Create -> Primitive -> Box, чтобы создать примитив box размером (1,1,2), добавить его в сцену и переименовать player_hit_box.
- Add it as a child to the hands dummy node and reset its position to the parent one. Добавьте его в качестве дочернего элемента в Dummy Node hands и сбросьте его положение на родительское.
- Adjust the position of the player_hit_box so that it is placed below the hands.Отрегулируйте положение player_hit_box так, чтобы он располагался под руками.
-
And switch it to Dynamic mode.И переключите его в режим Dynamic.
- Make it invisible by clearing its Viewport mask in the Node tab of the Parameters window. Also clear the Shadow mask to disable shadows rendering.Сделайте его невидимым, очистив его маску Viewport на вкладке Node окна Parameters. Также снимите маску Shadow, чтобы отключить отрисовку теней.
Later, we will assign a Health component to it.Позже мы присвоим ему компонент Health.
Adding a CameraДобавление камеры#
To be able to see through the eyes of the character in the UnigineEditor, you can create a new camera (PlayerDummy). It will make it easier to test the first-person setup.Чтобы иметь возможность видеть глазами персонажа в UnigineEditor, вы можете создать новую камеру (PlayerDummy). Это облегчит тестирование настройки от первого лица.
Right-click the player dummy object and choose Create -> Camera -> Dummy. Place the created camera somewhere in the world. If necessary, enable editing of the player node reference first.Щелкните правой кнопкой мыши на Dummy Object player и выберите Create -> Camera -> Dummy. Поместите созданную камеру где-нибудь в мире. При необходимости сначала включите редактирование Node Reference player.
- In the Node tab of the Parameters window, reset the position of the camera to the player position. Then adjust the rotation so that the camera is directed forward: set the rotation around the X axis to 90. Во вкладке Node в окне Parameters сбросьте положение камеры в позицию player. Затем отрегулируйте поворот так, чтобы камера была направлена вперед: установите вращение вокруг оси X равным 90.
Add the hands dummy node as a child to the PlayerDummy node.Добавьте Dummy Node hands в качестве дочерней ноды к ноде PlayerDummy.
- Adjust the position of the hands so that you can see them through the PlayerDummy camera. Transformation of the player's body should also be changed.Отрегулируйте положение рук так, чтобы вы могли видеть их через камеру PlayerDummy. Трансформация тела игрока также должна быть изменена.
- In the Parameters window, change Near Clipping and FOV Degrees values of the PlayerDummy node: it will help you to get the required camera view.В окне Parameters измените значения Near Clipping и FOV Degrees ноды PlayerDummy: это поможет вам получить требуемый обзор камеры.
- Check the Main Player box in the parameters of the PlayerDummy to make it the main camera. Включите опцию Main Player в параметрах PlayerDummy, чтобы сделать его основной камерой.
- To avoid hands falling under gravity, adjust the position of the player dummy object's ShapeCapsule in the Physics tab of the Parameters window. It should almost coincide with the position of the player_hit_box node.Чтобы избежать падения рук под действием силы тяжести, отрегулируйте положение ShapeCapsule Dummy Object player на вкладке Physics окна Parameters. Оно должно почти совпадать с положением ноды player_hit_box.
- Select the player dummy object and go to the Node tab of the Parameters window.Выберите Dummy Object player и перейдите на вкладку Node окна Parameters.
- In the Node Components and Properties section, choose Camera -> Camera mode -> USE EXTERNAL.В разделе Node Components and Properties выберите Camera -> Camera mode -> USE EXTERNAL.
Drag and drop the PlayerDummy node to the Camera field.Перетащите ноду PlayerDummy в поле Camera.
Now you can switch to the PlayerDummy camera in the Editor Viewport.Теперь вы можете переключиться на камеру PlayerDummy в Editor Viewport.
Attaching a Weapon to the HandsПрикрепление оружия к рукам#
Our FBX model of hands contains several bones. We can attach the pistol to a particular bone of the hands to make it follow the transformations of this bone. For this purpose, you should create a WorldTransformBone node.Наша FBX модель рук содержит несколько костей. Мы можем прикрепить пистолет к определенной кости руки, чтобы заставить его следовать преобразованиям этой кости. Для этой цели вы должны создать ноду WorldTransformBone.
- In the Asset Browser, find the data/fps/pistol/pistol.fbx asset and add it to the scene.В Asset Browser найдите ассет data/fps/pistol/pistol.fbx и добавьте его в сцену.
In the Menu bar, choose Create -> Mesh -> SkinnedBone: a WorldTransformBone node will be created. Add it as a child to the hands skinned mesh (the one that is inherited from the hands dummy node).В строке меню выберите Create -> Mesh -> SkinnedBone: будет создана нода WorldTransformBone. Добавьте ее в качестве дочернего элемента в Skinned Mesh hands (тот, который унаследован от Dummy Node hands).
In the Bone drop-down list, select joint_hold. This will be the bone to which the pistol will be attached.В раскрывающемся списке Bone выберите joint_hold. Это будет кость, к которой будет прикреплен пистолет.
Make the pistol a child of the WorldTransformBone node. Reset its relative position and rotation to zero if needed.Сделайте пистолет дочерним элементом ноды WorldTransformBone. При необходимости сбросьте его относительное положение и поворот на ноль.
Testing AnimationsПроверка анимации#
There are also a number of animations to be used for the hands (idle, walking, shooting). You can check how a certain animation looks like, for example:Существует также ряд анимаций, которые можно использовать для рук (холостой ход, ходьба, стрельба). Вы можете проверить, как выглядит определенная анимация, например:
- In the Asset Browser, find the fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim file and drag it to the Preview Animation section of the hands skinned mesh parameters.В Asset Browser найдите файл fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim и перетащите его в раздел Preview Animation параметров Skinned Mesh hands.
Check the Loop option and click Play.Установите флажок Loop и нажмите Play.
Playing Animations via CodeВоспроизведение анимации с помощью кода#
When the character changes its states (shooting, walking forward/backward/left/right), the corresponding animations should change smoothly. We will implement a component for mixing our animations.Когда персонаж меняет свои состояния (стрельба, ходьба вперед / назад / влево / вправо), соответствующие анимации должны плавно меняться. Мы внедрим компонент для смешивания наших анимаций.
Blending AnimationsСмешивание анимаций#
To ensure a seamless transition, we need to play two animations simultaneously and blend them. To do so, we will use multiple layers; then we will be able to assign different weights to these layers and achieve smooth blending.Чтобы обеспечить плавный переход, нам нужно воспроизвести две анимации одновременно и смешать их. Для этого мы будем использовать несколько слоев; тогда мы сможем назначить этим слоям разные веса и добиться плавного смешивания.
The following scheme shows the blend tree we are going to use:На следующей схеме показано дерево наложения, которое мы собираемся использовать:
Create a new C# component HandAnimationController.cs: in the Asset Browser, right-click and choose Create Code -> C# Component in the drop-down list. Copy and paste the following code to the created component: Создайте новый компонент C# HandAnimationController.cs: в Asset Browser щелкните правой кнопкой мыши и выберите Create Code -> C# Component в выпадающем списке. Скопируйте и вставьте следующий код в созданный компонент:
HandAnimationController.cs
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 HandAnimationController : Component { // first person controller public FirstPersonController fpsController = null; public float moveAnimationSpeed = 30.0f; public float shootAnimationSpeed = 30.0f; public float idleWalkMixDamping = 5.0f; public float walkDamping = 5.0f; public float shootDamping = 1.0f; // animation parameters [ParameterFile(Filter = ".anim")] public string idleAnimation = null; [ParameterFile(Filter = ".anim")] public string moveForwardAnimation = null; [ParameterFile(Filter = ".anim")] public string moveBackwardAnimation = null; [ParameterFile(Filter = ".anim")] public string moveRightAnimation = null; [ParameterFile(Filter = ".anim")] public string moveLeftAnimation = null; [ParameterFile(Filter = ".anim")] public string shootAnimation = null; public vec2 LocalMovementVector { get { return new vec2( MathLib.Dot(fpsController.SlopeAxisY, fpsController.HorizontalVelocity), MathLib.Dot(fpsController.SlopeAxisX, fpsController.HorizontalVelocity) ); } set {} } private ObjectMeshSkinned meshSkinned = null; private float currentIdleWalkMix = 0.0f; // 0 means idle animation, 1 means walk animation private float currentShootMix = 0.0f; // 0 means idle/walk mix, 1 means shoot animation private float currentWalkForward = 0.0f; private float currentWalkBackward = 0.0f; private float currentWalkRight = 0.0f; private float currentWalkLeft = 0.0f; private float currentWalkIdleMixFrame = 0.0f; private float currentShootFrame = 0.0f; private int numShootAnimationFrames = 0; // animation layers number private const int numLayers = 6; private void Init() { // grab the node with the current component assigned // and cast it to the ObjectMeshSkinned type meshSkinned = node as ObjectMeshSkinned; // set the number of animation layers for the node meshSkinned.NumLayers = numLayers; // set animation for each animation layer meshSkinned.SetLayerAnimationFilePath(0, idleAnimation); meshSkinned.SetLayerAnimationFilePath(1, moveForwardAnimation); meshSkinned.SetLayerAnimationFilePath(2, moveBackwardAnimation); meshSkinned.SetLayerAnimationFilePath(3, moveRightAnimation); meshSkinned.SetLayerAnimationFilePath(4, moveLeftAnimation); meshSkinned.SetLayerAnimationFilePath(5, shootAnimation); numShootAnimationFrames = meshSkinned.GetLayerNumFrames(5); // enable all animation layers for (int i = 0; i < numLayers; ++i) meshSkinned.SetLayerEnabled(i, true); } ${#HL}$ public void Shoot() { // enable the shooting animation currentShootMix = 1.0f; // set the animation layer frame to 0 currentShootFrame = 0.0f; } ${HL#}$ private void Update() { vec2 movementVector = LocalMovementVector; // check if the character is moving bool isMoving = movementVector.Length2 > MathLib.EPSILON; // handle input: check if the fire button is pressed bool isShooting = Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT); if (isShooting) Shoot(); // calculate the target values for the layer weights float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f; float targetWalkForward = (float) MathLib.Max(0.0f, movementVector.x); float targetWalkBackward = (float) MathLib.Max(0.0f, -movementVector.x); float targetWalkRight = (float) MathLib.Max(0.0f, movementVector.y); float targetWalkLeft = (float) MathLib.Max(0.0f, -movementVector.y); // apply the current layer weights float idleWeight = 1.0f - currentIdleWalkMix; float walkMixWeight = currentIdleWalkMix; float shootWalkIdleMix = 1.0f - currentShootMix; meshSkinned.SetLayerWeight(0, shootWalkIdleMix * idleWeight); meshSkinned.SetLayerWeight(1, shootWalkIdleMix * walkMixWeight * currentWalkForward); meshSkinned.SetLayerWeight(2, shootWalkIdleMix * walkMixWeight * currentWalkBackward); meshSkinned.SetLayerWeight(3, shootWalkIdleMix * walkMixWeight * currentWalkRight); meshSkinned.SetLayerWeight(4, shootWalkIdleMix * walkMixWeight * currentWalkLeft); meshSkinned.SetLayerWeight(5, currentShootMix); // update the animation frames: set the same frame for the animation layers to keep them in sync meshSkinned.SetLayerFrame(0, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(1, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(2, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(3, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(4, currentWalkIdleMixFrame); // set the shooting animation layer frame to 0 to start animation from the beginning meshSkinned.SetLayerFrame(5, currentShootFrame); currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps; currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames); // smoothly update the current weight values currentIdleWalkMix = MathLib.Lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game.IFps); currentWalkForward = MathLib.Lerp(currentWalkForward, targetWalkForward, walkDamping * Game.IFps); currentWalkBackward = MathLib.Lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game.IFps); currentWalkRight = MathLib.Lerp(currentWalkRight, targetWalkRight, walkDamping * Game.IFps); currentWalkLeft = MathLib.Lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game.IFps); currentShootMix = MathLib.Lerp(currentShootMix, 0.0f, shootDamping * Game.IFps); } }
- In the UnigineEditor, assign this component to the hands skinned mesh.В UnigineEditor назначьте этот компонент на Skinned Mesh hands.
- Remove the fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim from the Preview Animation field of the Mesh Skinned section.Удалите fps/hands/hands_animations/handspistolidle.fbx/handspistolidle.anim из поля Preview Animation в разделе Mesh Skinned.
Add animations stored in the fps/hands/hands_animations folder to the corresponding parameters.Добавьте анимации, хранящиеся в папке fps/hands/hands_animations, к соответствующим параметрам.
- Assign (drag and drop) the player dummy node to the Fps Controller field of the HandAnimationController component so that it could get required data from the player's first person controller to perform blending. Назначьте (перетащите) Dummy Node player в поле Fps Controllerкомпонента HandAnimationController, чтобы он мог получать необходимые данные из first person controller для выполнения смешивания.
- Save all changes and run the application logic via the UnigineEditor to check the result.Сохраните все изменения и запустите логику приложения через UnigineEditor, чтобы проверить результат.
Implementing Player ShootingРеализация стрельбы игрока#
Shooting ControlsЭлементы управления стрельбой#
Let's implement a new component for checking if the fire button is pressed. This is the preferred way as we are going to use this logic in the other components:Давайте внедрим новый компонент для проверки того, нажата ли кнопка стрельбы. Это предпочтительный способ, поскольку мы собираемся использовать эту логику в других компонентах:
- In the HandAnimationController component to start the shooting animation.В компоненте HandAnimationController для запуска анимации стрельбы.
- In the WeaponController component to start the shooting logic.В компоненте WeaponController, чтобы запустить логику стрельбы.
In this component, you can also define a button that acts as a fire button.В этом компоненте вы также можете определить кнопку, которая действует как кнопка запуска.
To handle user input, use one of the Input class functions to check if the given button is pressed.Для обработки пользовательского ввода используйте одну из функций класса Input, чтобы проверить, нажата ли данная кнопка.
Create a ShootInput.cs component and copy the following code to it.Создайте компонент ShootInput.cs и скопируйте в него следующий код.
ShootInput.cs
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 ShootInput : Component { public bool IsShooting() { return Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT); } }
Add the ShootInput.cs component to the player dummy node.Добавьте компонент ShootInput.cs к Dummy Node player.
-
Modify the HandAnimationController.cs component in order to use logic of the ShootInput.cs. Replace your current code with the following one:Измените компонент HandAnimationController.cs, чтобы использовать логику ShootInput.cs. Замените ваш текущий код следующим:
HandAnimationController.cs
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 HandAnimationController : Component { // first person controller public FirstPersonController fpsController = null; ${#HL}$ public ShootInput shootInput = null; ${HL#}$ public float moveAnimationSpeed = 30.0f; public float shootAnimationSpeed = 30.0f; public float idleWalkMixDamping = 5.0f; public float walkDamping = 5.0f; public float shootDamping = 1.0f; // animation parameters [ParameterFile(Filter = ".anim")] public string idleAnimation = null; [ParameterFile(Filter = ".anim")] public string moveForwardAnimation = null; [ParameterFile(Filter = ".anim")] public string moveBackwardAnimation = null; [ParameterFile(Filter = ".anim")] public string moveRightAnimation = null; [ParameterFile(Filter = ".anim")] public string moveLeftAnimation = null; [ParameterFile(Filter = ".anim")] public string shootAnimation = null; public vec2 LocalMovementVector { get { return new vec2( MathLib.Dot(fpsController.SlopeAxisY, fpsController.HorizontalVelocity), MathLib.Dot(fpsController.SlopeAxisX, fpsController.HorizontalVelocity) ); } set {} } private ObjectMeshSkinned meshSkinned = null; private float currentIdleWalkMix = 0.0f; // 0 means idle animation, 1 means walk animation private float currentShootMix = 0.0f; // 0 means idle/walk mix, 1 means shoot animation private float currentWalkForward = 0.0f; private float currentWalkBackward = 0.0f; private float currentWalkRight = 0.0f; private float currentWalkLeft = 0.0f; private float currentWalkIdleMixFrame = 0.0f; private float currentShootFrame = 0.0f; private int numShootAnimationFrames = 0; // animation layers number private const int numLayers = 6; private void Init() { // grab the node with the current component assigned // and cast it to the ObjectMeshSkinned type meshSkinned = node as ObjectMeshSkinned; // set the number of animation layers for the node meshSkinned.NumLayers = numLayers; // set animation for each animation layer meshSkinned.SetLayerAnimationFilePath(0, idleAnimation); meshSkinned.SetLayerAnimationFilePath(1, moveForwardAnimation); meshSkinned.SetLayerAnimationFilePath(2, moveBackwardAnimation); meshSkinned.SetLayerAnimationFilePath(3, moveRightAnimation); meshSkinned.SetLayerAnimationFilePath(4, moveLeftAnimation); meshSkinned.SetLayerAnimationFilePath(5, shootAnimation); numShootAnimationFrames = meshSkinned.GetLayerNumFrames(5); // enable all animation layers for (int i = 0; i < numLayers; ++i) meshSkinned.SetLayerEnabled(i, true); } ${#HL}$ public void Shoot() { // enable the shooting animation currentShootMix = 1.0f; // set the animation layer frame to 0 currentShootFrame = 0.0f; } ${HL#}$ private void Update() { vec2 movementVector = LocalMovementVector; // check if the character is moving bool isMoving = movementVector.Length2 > MathLib.EPSILON; // handle input: check if the fire button is pressed ${#HL}$ if (shootInput.IsShooting()) Shoot(); ${HL#}$ // calculate the target values for the layer weights float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f; float targetWalkForward = (float) MathLib.Max(0.0f, movementVector.x); float targetWalkBackward = (float) MathLib.Max(0.0f, -movementVector.x); float targetWalkRight = (float) MathLib.Max(0.0f, movementVector.y); float targetWalkLeft = (float) MathLib.Max(0.0f, -movementVector.y); // apply the current layer weights float idleWeight = 1.0f - currentIdleWalkMix; float walkMixWeight = currentIdleWalkMix; float shootWalkIdleMix = 1.0f - currentShootMix; meshSkinned.SetLayerWeight(0, shootWalkIdleMix * idleWeight); meshSkinned.SetLayerWeight(1, shootWalkIdleMix * walkMixWeight * currentWalkForward); meshSkinned.SetLayerWeight(2, shootWalkIdleMix * walkMixWeight * currentWalkBackward); meshSkinned.SetLayerWeight(3, shootWalkIdleMix * walkMixWeight * currentWalkRight); meshSkinned.SetLayerWeight(4, shootWalkIdleMix * walkMixWeight * currentWalkLeft); meshSkinned.SetLayerWeight(5, currentShootMix); // update the animation frames: set the same frame for the animation layers to keep them in sync meshSkinned.SetLayerFrame(0, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(1, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(2, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(3, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(4, currentWalkIdleMixFrame); // set the shooting animation layer frame to 0 to start animation from the beginning meshSkinned.SetLayerFrame(5, currentShootFrame); currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps; currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames); // smoothly update the current weight values currentIdleWalkMix = MathLib.Lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game.IFps); currentWalkForward = MathLib.Lerp(currentWalkForward, targetWalkForward, walkDamping * Game.IFps); currentWalkBackward = MathLib.Lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game.IFps); currentWalkRight = MathLib.Lerp(currentWalkRight, targetWalkRight, walkDamping * Game.IFps); currentWalkLeft = MathLib.Lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game.IFps); currentShootMix = MathLib.Lerp(currentShootMix, 0.0f, shootDamping * Game.IFps); } }
-
Select the hands node, drag and drop the player dummy node to the Shoot Input field in the HandAnimationController section.Выберите ноду hands, перетащите Dummy Node player в поле Shoot Input в разделе HandAnimationController.
Using IntersectionsИспользование пересечений#
To implement shooting, you can use the properties of the PlayerDummy camera. This camera has its -Z axis pointing at the middle of the screen. So, you can do a ray cast from the camera to the middle of the screen, get the intersection, and check if you hit something.Для реализации стрельбы вы можете использовать свойства камеры PlayerDummy. Ось этой камеры -Z направлена в середину экрана. Итак, вы можете направить луч с камеры в середину экрана, получить пересечение и проверить, не попали ли вы во что-нибудь.
In the component code below, we will store two points (p0, p1): the camera point and the point of the mouse pointer. GetIntersection() method will cast a ray from p0 to p1 and check the intersection with an object's surface that has the matching intersection mask. If we get the intersection, the method returns hitObject and hitInfo values (the intersection point and normal).В приведенном ниже коде компонента мы сохраним две точки (p0, p1): точку камеры и точку указателя мыши. Метод GetIntersection() преобразует луч из p0 в p1 и проверяет пересечение с поверхностью объекта, которая имеет соответствующую маску пересечения. Если мы получаем пересечение, метод возвращает значения hitObject и hitInfo (точка пересечения и нормаль).
Create a WeaponController.cs component and copy the following code:Создайте компонент WeaponController.cs и скопируйте следующий код:
WeaponController.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 [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 int damage = 1; // intersection mask [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { // initialize the camera point (p0) and the point of the mouse pointer (p1) Vec3 p0, p1; shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y); // create an intersection normal WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // get the first object intersected by the (p0,p1) line Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); } } private void Update() { // handle input: check if the fire button is pressed if (shootInput.IsShooting()) Shoot(); } }
- Add the component to the player dummy node.Добавьте компонент в Dummy Node player.
- Assign PlayerDummy to the Shooting Camera field so that the component could get information from the camera.Назначьте PlayerDummy полю Shooting Camera, чтобы компонент мог получать информацию с камеры.
Assign the player dummy node to the Shoot Input field.Назначьте Dummy Node player в поле Shoot Input.
To view the shooting intersection points and normals, you can enable Visualizer when the application is running: Для просмотра точек пересечения и нормалей стрельбы вы можете включить Visualizer во время работы приложения:
- Open the console by pressing ~Откройте консоль, нажав ~
- Type show_visualizer 1.Введите show_visualizer 1.
Hit Effect and Muzzle FlashЭффект попадания и вспышка выстрела#
Visual effects for shooting can be implemented in a separate component. You can get information about the hit point and spawn a hit prefab at this point oriented along the hit normal. For the muzzle flash, you can attach a NodeDummy to the muzzle of the pistol, and spawn a muzzle flash prefab at this position.Визуальные эффекты для стрельбы могут быть реализованы в отдельном компоненте. Вы можете получить информацию о точке попадания и создать префаб, имитирующий попадание, в этой точке, ориентированный вдоль нормали попадания. Для вспышки выстрела вы можете прикрепить NodeDummy к дулу пистолета и создать префаб вспышки выстрела в этом положении.
In the component code below, the OnHit() and OnShoot() methods implement this logic.В приведенном ниже коде компонента методы OnHit() и OnShoot() реализуют эту логику.
-
Create a VFXController.cs component and copy the code below. You can also use the existing data/fps/components/VFXController.cs component.Создайте компонент VFXController.cs и скопируйте приведенный ниже код. Вы также можете использовать существующий компонент data/fps/components/VFXController.cs.
VFXController.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; #region Math Variables #if UNIGINE_DOUBLE using Vec3 = Unigine.dvec3; using Mat4 = Unigine.dmat4; #else using Vec3 = Unigine.vec3; using Mat4 = Unigine.mat4; #endif #endregion [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component public class VFXController : Component { // prefabs for hit and muzzle flash visualization [ParameterFile(Filter = ".node")] public string hitPrefab = null; [ParameterFile(Filter = ".node")] public string muzzleFlashPrefab = null; public void OnShoot(Mat4 transform) { // if no hit prefab is specified, do nothing if (string.IsNullOrEmpty(hitPrefab)) return; // load the prefab for muzzle flash visualization Node muzzleFlashVFX = World.LoadNode(muzzleFlashPrefab); // set the muzzle flash node transformation muzzleFlashVFX.WorldTransform = transform; } public void OnHit(Vec3 hitPoint, vec3 hitNormal, Unigine.Object hitObject) { // if no hit prefab is specified, do nothing if (string.IsNullOrEmpty(hitPrefab)) return; // load the prefab for hit visualization Node hitVFX = World.LoadNode(hitPrefab); // place the prefab in the hit point and set its direction according to the hit normal hitVFX.WorldPosition = hitPoint; hitVFX.SetWorldDirection(hitNormal, vec3.UP, MathLib.AXIS.Y); } }
-
Modify the WeaponController.cs component in order to use logic of the VFXController.cs.Измените компонент WeaponController.cs, чтобы использовать логику VFXController.cs.
WeaponController.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 [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 mask [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { // spawn a muzzle flash if (weaponMuzzle) vfx.OnShoot(weaponMuzzle.WorldTransform); // initialize the camera point (p0) and the point of the mouse pointer (p1) Vec3 p0, p1; shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y); // create an intersection normal WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // get the first object intersected by the (p0,p1) line Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); // spawn a hit prefab at the intersection point vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject); } } private void Update() { // handle input: check if the fire button is pressed if (shootInput.IsShooting()) Shoot(); } }
- Add the VFXController.cs component to the player dummy node.Добавьте компонент VFXController.cs к Dummy Node player.
Create a NodeDummy, call it muzzle, make it a child of the pistol skinned mesh, and place it near the end of the weapon muzzle.Создайте NodeDummy, назовите его muzzle, сделайте его дочерним элементом Skinned Mesh pistol и поместите его рядом с дулом оружия.
- Select the player dummy node, assign the muzzle node to the Weapon Muzzle field in the WeaponController section.Выберите Dummy Node player, назначьте ноду muzzle полю Weapon Muzzle в разделе WeaponController.
Assign the player dummy node to the Vfx field in the WeaponController section.Назначьте Dummy Node player полю Vfx в разделе WeaponController.
- Add the data/fps/bullet/bullet_hit.node to the Hit Prefab field of the VFXController section.Добавьте data/fps/bullet/bullet_hit.node в поле Hit Prefab раздела VFXController.
- Add the data/fps/bullet/bullet_spawn.node to the Muzzle Flash Prefab field.Добавьте data/fps/bullet/bullet_spawn.node в поле Muzzle Flash Prefab.
Now you can press Start and test the shooting visual effects.Теперь вы можете нажать Start и протестировать визуальные эффекты съемки.
VFX LifetimeВремя отображения VFX#
To control the duration of visual effects, you can add a component that will allow you to define a time interval for the node during which it will live and after which it will be deleted. The ready-to-use data/fps/components/LifeTime.cs component implements this logic.Чтобы управлять длительностью визуальных эффектов, вы можете добавить компонент, который позволит вам определить интервал времени для ноды, в течение которого он будет отображаться и после которого он удалится. Готовый к использованию компонент data/fps/components/LifeTime.cs реализует эту логику.
LifeTime.cs
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 Lifetime : Component
{
[ShowInEditor][Parameter(Tooltip = "Object's lifetime")]
private float lifeTime = 1.0f;
private float startTime = 0.0f;
void Init()
{
// remember initialization time of an object
startTime = Game.Time;
}
void Update()
{
// wait until the lifetime ends and delete the object
if (Game.Time - startTime > lifeTime)
node.DeleteLater();
}
}
This component is already added to the bullet_hit.node and bullet_spawn.node prefabs:Этот компонент уже добавлен в префабы bullet_hit.node и bullet_spawn.node:
For the bullet_hit.node, the Life Time parameter is set to 1 second.Для параметра bullet_hit.node параметр Life Time устанавливается равным 1 секунде.
For the bullet_spawn.node, the Life Time parameter is set to 5 seconds.Для параметра bullet_spawn.node параметр Life Time устанавливается равным 5 секундам.
Adding a HUD for the Crosshair and Player StatsДобавление HUD для прицела и статистики игрока#
To make a HUD displaying some game info or other graphic elements, you can get an instance of the screen GUI and then add widgets as its children. We will use a WidgetSprite to make a crosshair at the center of the screen.Чтобы создать HUD, отображающий некоторую информацию об игре или другие графические элементы, вы можете получить экземпляр экранного графического интерфейса, а затем добавить виджеты в качестве его дочерних элементов. Мы будем использовать WidgetSprite, чтобы создать прицел в центре экрана.
Create a HUD.cs component (or use the existing one in data/fps/components).Создайте компонент HUD.cs (или используйте существующий из data/fps/components).
HUD.cs
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 HUD : Component { // crosshair parameters [ParameterFile] public string crosshairImage = null; public int crosshairSize = 16; private WidgetSprite sprite = null; private void Init() { // get an instance of the screen Gui Gui screenGui = Gui.GetCurrent(); // add a sprite widget sprite = new WidgetSprite(screenGui, crosshairImage); // set the sprite size sprite.Width = crosshairSize; sprite.Height = crosshairSize; // make the sprite stay in the screen center and overlap the other widgets screenGui.AddChild(sprite, Gui.ALIGN_CENTER | Gui.ALIGN_OVERLAP); } private void Update() { // write here code to be called before updating each render frame } }
Create a NodeDummy, place it somewhere in the scene, name it HUD and add the HUD.cs component to it.Создайте NodeDummy, поместите его где-нибудь в сцене, назовите его HUD и добавьте к нему компонент HUD.cs.
Add the data/fps/hud/textures/crosshair.png file to the Crosshair Image field.Добавьте файл data/fps/hud/textures/crosshair.png в поле Crosshair Image.
Creating an EnemyСоздание врага#
The important part of any shooter is an enemy. We are going to create an enemy which moves around the scene chasing the player, starts firing at a certain distance from the player, and gets killed (deleted) if hit by the player's bullets.Враги — важная часть любого шутера. Мы собираемся создать врага, который перемещается по сцене, преследуя игрока, начинает стрелять на определенном расстоянии от игрока и убит (удаляется), если попадает под пули игрока.
Adding Enemy ModelДобавление модели врага#
Before adding an enemy model, you should create it in a 3D modeling software.Прежде чем добавлять модель врага, вы должны создать ее в программном обеспечении для 3D-моделирования.
Find our ready-to-use robot_enemy.node enemy prefab in the data/fps/robot folder and place it in the scene.Найдите наш префаб врага robot_enemy.node в папке data/fps/robot и добавьте его в сцену.
Applying a Finite-State Machine for AIПрименение конечного автомата для искусственного интеллекта#
To be a strong opponent, your enemy must have a certain level of intelligence. A simple AI can be implemented using a Finite-State Machine — a concept allowing you to describe the logic in terms of states and transitions between them.Чтобы быть сильным противником, ваш враг должен обладать определенным уровнем интеллекта. Простой искусственный интеллект может быть реализован с использованием Конечного автомата — концепции, позволяющей описывать логику в терминах состояний и переходов между ними.
For simplicity, consider three states: Idle, Chase, and Attack/Fire.Для простоты рассмотрим три состояния: Idle, Chase и Attack/Fire.
The following diagram describes what the enemy should do in each state, and how it will switch different states. The typical transitions would be from Idle to Chase, from Chase to Attack, and vice versa.Следующая диаграмма описывает, что враг должен делать в каждом состоянии и как он будет переключать разные состояния. Типичными переходами будут переходы от Idle к Chase, от Chase к Attack и наоборот.
Create an EnemyLogic.cs component and copy the code below:Создайте компонент EnemyLogic.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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public NavigationMesh navigationMesh = null; 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; 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 bool IsTargetVisible() { Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition); Vec3 p0 = intersectionSocket.WorldPosition; Vec3 p1 = p0 + direction * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { bodyRigid = node.ObjectBodyRigid; hitExcludes[0] = node; hitExcludes[1] = node.GetChild(0); targetIsVisible = false; } private void Update() { UpdateTargetState(); UpdateOrientation(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED); Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED); } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } private void ProcessChaseState() { vec3 currentVelocity = bodyRigid.LinearVelocity; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; bool targetReached = (lastSeenDistanceSqr < reachRadius * reachRadius); if (!targetReached) { currentVelocity.x = lastSeenDirection.x * speed; currentVelocity.y = lastSeenDirection.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; } } }
- Enable editing of the robot_enemy node and assign the component to the robot_root dummy node in the Parameters window.Включите редактирование ноды robot_enemy и назначьте компонент Dummy Node robot_root в окне Parameters.
Right-click the player node reference in the World Nodes window and choose Unpack to Node Content. The node reference will be removed and its content will be displayed in the World Nodes hierarchy.Щелкните правой кнопкой мыши на Node Reference player в окне World Nodes и выберите Unpack to Node Content. Node Reference будет удалена, и ее содержимое будет отображаться в иерархии World Nodes.
- Drag and drop the player_hit_box node to the Player field. The player_hit_box imitates the body of the player and is used in calculations.Перетащите ноду player_hit_box в поле Player. player_hit_box имитирует тело игрока и используется в вычислениях.
Drag and drop the robot_intersection_socket node of the robot_enemy to Intersection Socket field. It is the node from which the robot will do intersection checks.Перетащите ноду robot_intersection_socket ноды robot_enemy в поле Intersection Socket. Это нода, от которой робот будет выполнять проверку пересечений.
For debugging, you can enable Visualizer which will display the inner and outer attack radius, as well as colored squares above the robot indicating:Для отладки вы можете включить Visualizer, который будет отображать внутренний и внешний радиус атаки, а также цветные квадраты над роботом, указывающие:
- The state of the robot: Idle — BLUE, Chase — YELLOW, Attack — RED.Состояние робота: Idle — СИНИЙ, Chase — ЖЕЛТЫЙ, Attack — КРАСНЫЙ.
- If the target is visible: Yes — GREEN, No — RED.Видна ли цель: Да — ЗЕЛЕНЫЙ, Нет — КРАСНЫЙ.
Implementing Enemy ShootingРеализация стрельбы по врагу#
Creating GunsСоздание оружия#
To implement robot shooting, you need a bullet prefab that will be spawned at certain points when the robot is in the Attack state.Чтобы реализовать возможность робота стрелять, вам нужен префаб пули, который будет создаваться в определенные моменты, когда робот находится в состоянии Attack.
In the EnemyFireController component we will add some shooting logic to make the robot shoot alternately from the left and right muzzle. Positions of these muzzles, where bullet nodes will be spawned, are defined by the positions of two nodes that we will assign to the Left Muzzle and Right Muzzle fields of the component.В компоненте EnemyFireController мы добавим некоторую логику стрельбы, чтобы робот стрелял попеременно с левого и правого дула. Позиции этих дул, в которых будут создаваться ноды пуль, определяются позициями двух нод, которые мы назначим в поля Left Muzzle и Right Muzzle компонента.
Create an EnemyFireController.cs component (or use the existing one in data/fps/components).Создайте компонент EnemyFireController.cs (или используйте существующий в data/fps/components).
EnemyFireController.cs
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 EnemyFireController : Component { public Node leftMuzzle = null; public Node rightMuzzle = null; [ParameterFile] public string bulletPrefab = null; public float shootInterval = 1.0f; private float currentTime = 0.0f; private bool isLeft = false; private bool isFiring = false; public void StartFiring() { isFiring = true; } public void StopFiring() { isFiring = false; } private void Init() { // reset the current time currentTime = 0.0f; // start shooting from the right muzzle isLeft = false; } private void Update() { // if the enemy is in the Chase state, do nothing if (!isFiring) return; // update the current time currentTime += Game.IFps; // check if the next shot should be fired if (currentTime > shootInterval) { // reset the current time currentTime = 0.0f; // spawn a bullet Node bullet = World.LoadNode(bulletPrefab); // set the bullet transformation bullet.WorldTransform = (isLeft) ? leftMuzzle.WorldTransform : rightMuzzle.WorldTransform; // switch the muzzle for the next shot isLeft = !isLeft; } } }
Modify the EnemyLogic.cs component in order to use the implemented logic.Измените компонент EnemyLogic.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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public NavigationMesh navigationMesh = null; 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; 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 bool IsTargetVisible() { Vec3 direction = (player.WorldPosition - intersectionSocket.WorldPosition); Vec3 p0 = intersectionSocket.WorldPosition; Vec3 p1 = p0 + direction * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { bodyRigid = node.ObjectBodyRigid; hitExcludes[0] = node; hitExcludes[1] = node.GetChild(0); targetIsVisible = false; // grab the EnemyFireController component fireController = node.GetComponent<EnemyFireController>(); } private void Update() { UpdateTargetState(); UpdateOrientation(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus Visualizer.RenderSphere(attackInnerRadius, node.WorldTransform, vec4.RED); Visualizer.RenderSphere(attackOuterRadius, node.WorldTransform, vec4.RED); } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } private void ProcessChaseState() { vec3 currentVelocity = bodyRigid.LinearVelocity; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; bool targetReached = (lastSeenDistanceSqr < reachRadius * reachRadius); if (!targetReached) { currentVelocity.x = lastSeenDirection.x * speed; currentVelocity.y = lastSeenDirection.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; // start firing if (fireController) fireController.StartFiring(); } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; // stop firing if (fireController) fireController.StopFiring(); } } }
- Enable editing of the robot_enemy node (if required) and assign the EnemyFireController.cs component to the robot_root dummy object. Включите редактирование ноды robot_enemy (если требуется) и назначьте компонент EnemyFireController.cs на Dummy Object robot_root.
Drag and drop the LeftGunMuzzle and RightGunMuzzle dummy nodes to the corresponding fields.Перетащите Dummy Node LeftGunMuzzle и RightGunMuzzle в соответствующие поля.
Drag and drop the data/fps/bullet/bullet.node to the Bullet Prefab field.Перетащите data/fps/bullet/bullet.node в поле Bullet Prefab.
Making a Moving and Destroyable BulletСоздание движущейся и разрушаемой пули#
After spawning, a bullet should move in the right direction changing its position in the world. If the bullet intersects with an object, a hit effect should be spawned at the point of impact. And if this object can take damage (i.e., it has a Health component, see below), its health should be decreased by a certain value. Also, you can make the bullet apply an impulse to physical objects.После появления пуля должна двигаться в соответствующем направлении, меняя свое положение в мире. Если пуля пересекается с объектом, в точке попадания должен возникнуть эффект попадания. И если этот объект может наносить урон (т.е. у него есть компонент Health, см. ниже), то здоровье цели должно быть уменьшено на определенное значение. Кроме того, вы можете заставить пулю прикладывать импульс к физическим объектам.
- Add the data/fps/bullet/bullet.node to the scene.Добавьте data/fps/bullet/bullet.node в сцену.
Create a Bullet.cs component and copy the following code:Создайте компонент Bullet.cs и скопируйте следующий код:
Bullet.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 [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component public class Bullet : Component { public float speed = 10.0f; public int damage = 1; [ParameterFile] public string hitPrefab = null; [ParameterMask] public int intersectionMask = ~0; private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); private void Init() { } private void Update() { // set the current position of the bullet Vec3 currentPosition = node.WorldPosition; // set the current world direction vector of the bullet pointing along the Y axis vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y); // change the bullet position node.WorldPosition += currentDirection * speed * Game.IFps; // get the first intersected object Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo); // if no intersections are found, do nothing if (hitObject == null) return; // load a prefab for hit visualization Node hitEffect = World.LoadNode(hitPrefab); // place the prefab in the hit point and set its direction according to the hit normal hitEffect.WorldPosition = hitInfo.Point; hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y); // delete the bullet node.DeleteLater(); } }
- Enable editing of the bullet node and assign the component to the bullet static mesh.Включите редактирование ноды bullet и назначьте компонент bullet на Static Mesh.
- Drag and drop data/fps/bullet/bullet_hit.node to the Hit Prefab field.Перетащите data/fps/bullet/bullet_hit.node в поле Hit Prefab.
- Assign the LifeTime.cs component to the bullet static mesh and set its Life Time to 5 seconds.Назначьте компонент LifeTime.cs на Static Mesh bullet и установите для него значение Life Time равным 5 секундам.
Using PathfindingИспользование поиска пути#
The enemy should be able to chase the player correctly and not get stuck. To give the enemy additional knowledge about how it can navigate through the level, you can use pathfinding. This requires creating a navigation mesh, which specifies areas available for navigation and can be generated based on the FBX model of the scene using special tools, for example, RecastBlenderAddon.Враг должен уметь правильно преследовать игрока и не застревать. Чтобы дать врагу дополнительные знания о том, как он может перемещаться по уровню, вы можете использовать поиск пути. Для этого требуется создать Navigation Mesh, который определяет области, доступные для навигации, и может быть сгенерирован на основе FBX модели сцены с использованием специальных инструментов, например, RecastBlenderAddon.
The previously added EnemyLogic component includes pathfinding logic which calculates the route for the robot. When the robot is in the Chase state, instead of going directly to the last seen target position, it will follow a path using a navigation mesh added to the scene. The path consists of a queue of route points calculated by using the PathRoute class functionality.Ранее добавленный компонент EnemyLogic включает логику поиска пути, которая вычисляет маршрут для робота. Когда робот находится в состоянии Chase, вместо того, чтобы перейти непосредственно к последней видимой целевой позиции, он будет следовать по пути, используя Navigation Mesh, добавленный в сцену. Путь состоит из очереди точек маршрута, вычисленных с использованием функционала класса PathRoute.
In the data/fps/navmesh_import folder you can find a navigation mesh created for this project.В папке data/fps/navmesh_import вы можете найти Navigation Mesh, созданный для этого проекта.
- To place the mesh in the scene, click Create->Navigation->NavigationMesh in the Menu Bar and specify the navmesh_import/navmesh.fbx/navmesh.002.mesh file.Чтобы поместить меш в сцену, нажмите Create->Navigation->NavigationMesh в строке меню и укажите файл navmesh_import/navmesh.fbx/navmesh.002.mesh.
- Align the mesh with the area.Совместите меш с пространством.
- In the Parameters window, set the Height of the navigation mesh to 3 for proper route calculation. В окне Parameters для Navigation Mesh установите значение Height равное 3 для правильного расчета маршрута.
Implement pathfinding logic in the EnemyLogic.cs component. Replace the existing code with the following one:Реализуйте логику поиска пути в компоненте EnemyLogic.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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public NavigationMesh navigationMesh = null; 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; 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; // create a queue of the 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 * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { // initialize parameters of the point moving along the route 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; // grab the EnemyFireController component fireController = node.GetComponent<EnemyFireController>(); shouldUpdateRoute = true; lastCalculationTime = Game.Time; } private void Update() { UpdateTargetState(); UpdateOrientation(); UpdateRoute(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus 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) { // if the target point of the route is reached if (route.IsReached) { // clear the 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 route.Create2D(node.WorldPosition, lastSeenPosition, 1); } } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } 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(); vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition); direction.z = 0.0f; direction.Normalize(); currentVelocity.x = direction.x * speed; currentVelocity.y = direction.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; // start firing if (fireController) fireController.StartFiring(); } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; // stop firing if (fireController) fireController.StopFiring(); } } }
- Drag and drop the NavigationMesh node to the NavigationMesh field of the EnemyLogic component assigned to the robot. Перетащите ноду NavigationMesh в поле NavigationMesh компонента EnemyLogic, назначенного роботу.
You can visualize the route points as black squares for debugging by enabling Visualizer.Вы можете визуализировать точки маршрута в виде черных квадратов для отладки, включив Visualizer.
Controlling HealthКонтроль за состоянием здоровья#
The player and enemies should have a health level that will decrease each time they are hit by a bullet.Игрок и враги должны иметь уровень здоровья, который будет уменьшаться каждый раз, когда в них попадает пуля.
Create a Health.cs component and copy the following code (or use the existing one in data/fps/components):Создайте компонент Health.cs и скопируйте следующий код (или используйте существующий в data/fps/components):
Health.cs
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 Health : Component { public int health = 5; // flag indicating that the health value is less or equal to 0 public bool IsDead => health <= 0; public void TakeDamage(int damage) { // calculate damage to health health = MathLib.Max(health - damage, 0); } }
- Add it to the visuals node of the robot_enemy.Добавьте его в ноду visuals ноды robot_enemy.
- Add it to the player_hit_box node of the player.Добавьте его в ноду player_hit_box ноды player.
- Modify the WeaponController.cs, EnemyLogic.cs and Bullet.cs components in order to use logic of the Health.cs.
WeaponController.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 [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 mask [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { // spawn a muzzle flash if (weaponMuzzle) vfx.OnShoot(weaponMuzzle.WorldTransform); // initialize the camera point (p0) and the point of the mouse pointer (p1) Vec3 p0, p1; shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y); // create an intersection normal WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // get the first object intersected by the (p0,p1) line Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); // spawn a hit prefab at the intersection point vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject); // apply damage Health health = hitObject.GetComponent<Health>(); if (health) health.TakeDamage(damage); } } private void Update() { // handle input: check if the fire button is pressed if (shootInput.IsShooting()) Shoot(); } }
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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public NavigationMesh navigationMesh = null; 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; 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 the 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 * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { // initialize parameters of the point moving along the route 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; // grab the EnemyFireController component fireController = node.GetComponent<EnemyFireController>(); // grab the Health component health = node.GetComponentInChildren<Health>(); shouldUpdateRoute = true; lastCalculationTime = Game.Time; } private void Update() { // check the enemy health if (health != null && health.IsDead) // delete the enemy if it's dead node.DeleteLater(); UpdateTargetState(); UpdateOrientation(); UpdateRoute(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus 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) { // if the target point of the route is reached if (route.IsReached) { // clear the 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 route.Create2D(node.WorldPosition, lastSeenPosition, 1); } } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } 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(); vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition); direction.z = 0.0f; direction.Normalize(); currentVelocity.x = direction.x * speed; currentVelocity.y = direction.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; // start firing if (fireController) fireController.StartFiring(); } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; // stop firing if (fireController) fireController.StopFiring(); } } }
Измените компоненты WeaponController.cs, EnemyLogic.cs и Bullet.cs, чтобы использовать логику компонента Health.cs.Bullet.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 [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component public class Bullet : Component { public float speed = 10.0f; public int damage = 1; [ParameterFile] public string hitPrefab = null; [ParameterMask] public int intersectionMask = ~0; private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); private void Init() { } private void Update() { // set the current position of the bullet Vec3 currentPosition = node.WorldPosition; // set the current world direction vector of the bullet pointing along the Y axis vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y); // change the bullet position node.WorldPosition += currentDirection * speed * Game.IFps; // get the first intersected object Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo); // if no intersections are found, do nothing if (hitObject == null) return; // load a prefab for hit visualization Node hitEffect = World.LoadNode(hitPrefab); // place the prefab in the hit point and set its direction according to the hit normal hitEffect.WorldPosition = hitInfo.Point; hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y); // apply damage from the bullet Health health = hitObject.GetComponent<Health>(); if (health != null) health.TakeDamage(damage); // delete the bullet node.DeleteLater(); } }
WeaponController.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 [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 mask [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { // spawn a muzzle flash if (weaponMuzzle) vfx.OnShoot(weaponMuzzle.WorldTransform); // initialize the camera point (p0) and the point of the mouse pointer (p1) Vec3 p0, p1; shootingCamera.GetDirectionFromMainWindow(out p0, out p1, Input.MousePosition.x, Input.MousePosition.y); // create an intersection normal WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // get the first object intersected by the (p0,p1) line Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); // spawn a hit prefab at the intersection point vfx.OnHit(hitInfo.Point, hitInfo.Normal, hitObject); // apply damage Health health = hitObject.GetComponent<Health>(); if (health) health.TakeDamage(damage); } } private void Update() { // handle input: check if the fire button is pressed if (shootInput.IsShooting()) Shoot(); } }
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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public NavigationMesh navigationMesh = null; 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; 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 the 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 * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { // initialize parameters of the point moving along the route 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; // grab the EnemyFireController component fireController = node.GetComponent<EnemyFireController>(); // grab the Health component health = node.GetComponentInChildren<Health>(); shouldUpdateRoute = true; lastCalculationTime = Game.Time; } private void Update() { // check the enemy health if (health != null && health.IsDead) // delete the enemy if it's dead node.DeleteLater(); UpdateTargetState(); UpdateOrientation(); UpdateRoute(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus 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) { // if the target point of the route is reached if (route.IsReached) { // clear the 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 route.Create2D(node.WorldPosition, lastSeenPosition, 1); } } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } 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(); vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition); direction.z = 0.0f; direction.Normalize(); currentVelocity.x = direction.x * speed; currentVelocity.y = direction.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; // start firing if (fireController) fireController.StartFiring(); } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; // stop firing if (fireController) fireController.StopFiring(); } } }
Bullet.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 [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component public class Bullet : Component { public float speed = 10.0f; public int damage = 1; [ParameterFile] public string hitPrefab = null; [ParameterMask] public int intersectionMask = ~0; private WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); private void Init() { } private void Update() { // set the current position of the bullet Vec3 currentPosition = node.WorldPosition; // set the current world direction vector of the bullet pointing along the Y axis vec3 currentDirection = node.GetWorldDirection(MathLib.AXIS.Y); // change the bullet position node.WorldPosition += currentDirection * speed * Game.IFps; // get the first intersected object Unigine.Object hitObject = World.GetIntersection(currentPosition, node.WorldPosition, intersectionMask, hitInfo); // if no intersections are found, do nothing if (hitObject == null) return; // load a prefab for hit visualization Node hitEffect = World.LoadNode(hitPrefab); // place the prefab in the hit point and set its direction according to the hit normal hitEffect.WorldPosition = hitInfo.Point; hitEffect.SetWorldDirection(hitInfo.Normal, vec3.UP, MathLib.AXIS.Y); // apply damage from the bullet Health health = hitObject.GetComponent<Health>(); if (health != null) health.TakeDamage(damage); // delete the bullet node.DeleteLater(); } }
Deleting the KilledУдаление убитого#
The nodes 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 true, the node will be deleted. We need to implement the same check for the player.Компонент Health имеет флаг IsDead, который проверяется компонентом EnemyLogic робота. Если флаг равен true, нода будет удалена. Нам нужно реализовать такую же проверку для игрока.
Create a PlayerLogic.cs component (or use the existing one in data/fps/components)Создайте компонент PlayerLogic.cs (или используйте существующий компонент в data/fps/components)
PlayerLogic.cs
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; private void Init() { // grab the Health component health = node.GetComponentInChildren<Health>(); } private void Update() { // apply damage to the player's health if (health != null && health.IsDead) { // delete the player node.DeleteLater(); Game.Player = null; } } }
- Add the PlayerLogic component to the player node.Добавьте компонент PlayerLogic к ноде player.
Switching Game StatesПереключение игровых состояний#
The game should have different states depending on the occurrence of certain events. For example, you can obtain a list of enemies, and if the list becomes empty, you win. You lose if the player gets killed or time runs out.Игра должна иметь разные состояния в зависимости от наступления определенных событий. Например, вы можете получить список врагов, и если список станет пустым, вы выиграете. Вы проиграете, если игрок будет убит или закончится время.
To switch between Play and Win/Lose states, we have a GameController component.Для переключения между состояниями Play и Win/Lose у нас есть компонент GameController.
Create a GameController.cs component and copy the following code (or use the existing one in data/fps/components):Создайте компонент GameController.cs и скопируйте следующий код (или используйте существующий в data/fps/components):
GameController.cs
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 = GameState.Gameplay; }
Modify the EnemyLogic.cs and PlayerLogic.cs components in order to use logic of the GameCotroller.cs.Измените компоненты EnemyLogic.cs и PlayerLogic.cs, чтобы использовать логику 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")] // <-- this line is generated automatically for a new component public class EnemyLogic : Component { public GameController gameController = null; public NavigationMesh navigationMesh = null; 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; 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 the 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 * 2.0f; Unigine.Object hitObject = World.GetIntersection(p0, p1, playerIntersectionMask, hitExcludes, hitInfo); if (hitObject == null) return false; return player.ID == hitObject.ID; } private void Init() { // initialize parameters of the point moving along the route 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; // grab the EnemyFireController component fireController = node.GetComponent<EnemyFireController>(); // grab the Health component health = node.GetComponentInChildren<Health>(); shouldUpdateRoute = true; lastCalculationTime = Game.Time; } private void Update() { // check the game state if (gameController.state != GameState.Gameplay) return; // check the enemy health if (health != null && health.IsDead) // delete the enemy if it's dead node.DeleteLater(); UpdateTargetState(); UpdateOrientation(); UpdateRoute(); // switch between the enemy states switch (currentState) { case EnemyLogicState.Idle: ProcessIdleState(); break; case EnemyLogicState.Chase: ProcessChaseState(); break; case EnemyLogicState.Attack: ProcessAttackState(); break; } // switch the colors indicating the enemy states 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 radus 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) { // if the target point of the route is reached if (route.IsReached) { // clear the 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 route.Create2D(node.WorldPosition, lastSeenPosition, 1); } } 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() { // check Idle -> Chase transition if (targetIsVisible) { // change the current state to Chase currentState = EnemyLogicState.Chase; // remember the player last seen position lastSeenPosition = player.WorldPosition; } } 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(); vec3 direction = (vec3)(calculatedRoute.Peek() - node.WorldPosition); direction.z = 0.0f; direction.Normalize(); currentVelocity.x = direction.x * speed; currentVelocity.y = direction.y * speed; } // check Chase->Idle transition if (!targetIsVisible) { currentState = EnemyLogicState.Idle; } // check Chase -> Attack transition if (lastSeenDistanceSqr < attackInnerRadius * attackInnerRadius && targetIsVisible) { currentState = EnemyLogicState.Attack; currentVelocity.x = 0.0f; currentVelocity.y = 0.0f; // start firing if (fireController) fireController.StartFiring(); } bodyRigid.LinearVelocity = currentVelocity; } private void ProcessAttackState() { // check Attack -> Chase transition if (lastSeenDistanceSqr > attackOuterRadius * attackOuterRadius || !targetIsVisible) { currentState = EnemyLogicState.Chase; // stop firing if (fireController) fireController.StopFiring(); } } }
PlayerLogic.cs
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; public GameController gameController = null; private void Init() { // grab the Health component health = node.GetComponentInChildren<Health>(); } private void Update() { // apply damage to the player's health if (health != null && health.IsDead) { // delete the player node.DeleteLater(); Game.Player = null; // change the game state to Lose gameController.state = GameState.Lose; } } }
Create a NodeDummy, name it gameplay_systems, and assign the GameController component to it.Создайте NodeDummy, назовите его gameplay_systems и назначьте ему компонент GameController.
Drag and drop gameplay_systems to the Game Controller field of the player and robot_enemy.Перетащите gameplay_systems в поле Game Controller нод player и robot_enemy.
Trying OutПроведем испытания#
Now you are ready to add more enemies and see if you can fight off an attack by a gang of angry robots!Теперь можно добавить больше врагов и посмотреть, сможете ли вы отбить атаку банды разъяренных роботов!