Реализация стрельбы
Now that our character is ready, let's implement shooting, add shooting controls, and use raycasting (intersections) to check if the bullet hits the target.Теперь, когда наш персонаж готов, реализуем стрельбу, добавим управление стрельбой и используем рейкастинг (intersections) для определения попадания пули в цель.
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 the ShootInput component and copy the following code to it.Создайте компонент ShootInput и скопируйте в него следующий код.
ShootInput.cs
using System; using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента public class ShootInput : Component { public bool IsShooting() { // возвращаем текущее состояние левой кнопки мыши и проверяем захват мыши в окне return Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT) && Input.MouseGrab; } }
Add the ShootInput.cs component to the player Dummy Node.Добавьте компонент ShootInput.cs к ноде player (Dummy Node).
-
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")] // <-- идентификатор генерируется автоматически для нового компонента public class HandAnimationController : Component { // контроллер игрока с видом от первого лица (FirstPersonController) 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; // параметры анимации [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 анимация покоя, 1 анимация ходьбы private float currentShootMix = 0.0f; // 0 комбинация бездействие/ходьба, 1 анимация стрельбы 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; // задаем число анимационных слоев private const int numLayers = 6; private void Init() { // берем ноду, которой назначена компонента // и преобразовываем ее к типу ObjectMeshSkinned meshSkinned = node as ObjectMeshSkinned; // устанавливаем количество анимационных слоев для каждого объекта meshSkinned.NumLayers = numLayers; // устанавливаем анимацию для каждого слоя 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); // включаем все анимационные слои for (int i = 0; i < numLayers; ++i) meshSkinned.SetLayerEnabled(i, true); } public void Shoot() { // включаем анимацию стрельбы currentShootMix = 1.0f; // устанавливаем кадр анимационного слоя в 0 currentShootFrame = 0.0f; } private void Update() { vec2 movementVector = LocalMovementVector; // проверяем, движется ли персонаж bool isMoving = movementVector.Length2 > MathLib.EPSILON; ${#HL}$ // обработка ввода: проверка нажатия клавиши 'огонь' if(shootInput.IsShooting()) Shoot(); ${HL#}$ // рассчитываем целевые значения для весовых коэффициентов слоев 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); // применяем текущие весовые коэффициенты 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); // обновляем анимационные кадры: устанавливаем один и тот же кадр для всех слоев, чтобы обеспечить их синхронизацию meshSkinned.SetLayerFrame(0, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(1, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(2, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(3, currentWalkIdleMixFrame); meshSkinned.SetLayerFrame(4, currentWalkIdleMixFrame); // устанавливаем текущий кадр для каждого анимационного слоя в 0, чтобы начать воспроизведение сначала meshSkinned.SetLayerFrame(5, currentShootFrame); currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps; currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames); // плавно обновляем текущие значения весовых коэффициентов 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, and select the ShootInput component.Выберите ноду hands, перетащите Dummy Node player в поле Shoot Input в разделе HandAnimationController и выберите компонент ShootInput соответственно.
Using RaycastingИспользование рейкастинга#
To implement shooting, you can use the properties of the PlayerDummy camera. This camera has its -Z axis pointing at the center of the screen. So, you can cast a ray from the camera to the center of the screen, get the intersection, and check if you hit anything.Для реализации стрельбы вы можете использовать свойства камеры 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 if the ray intersects with any object's surface (that has the matching Intersection mask to restrict the check results). If the intersection with such surface is detected, the method returns the hitObject and hitInfo values (the intersection point and normal).В приведенном ниже коде компонента мы сохраним две точки (p0, p1): точку камеры и точку указателя мыши. Метод GetIntersection() проводит воображаемый луч из p0 в p1 и проверяет, пересекает ли луч поверхность какого-либо объекта (с заданной маской Intersection, которая позволяет ограничить результаты проверок). Если пересечение с такой поверхностью найдено, метод возвращает значения 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")] // <-- идентификатор генерируется автоматически для нового компонента public class WeaponController : Component { public PlayerDummy shootingCamera = null; public ShootInput shootInput = null; public int damage = 1; // маска Intersection чтобы определить, в какие объекты могут попадать пули [ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)] public int mask = ~0; public void Shoot() { // задаем начало отрезка (p0) в позиции камеры и конец (p1) - в точке удаленной на 100 единиц в направлении взгляда камеры Vec3 p0 = shootingCamera.WorldPosition; Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection() * 100; // создаем объект для хранения intersection-нормали WorldIntersectionNormal hitInfo = new WorldIntersectionNormal(); // ищем первый объект, который пересекает отрезок (p0, p1) Unigine.Object hitObject = World.GetIntersection(p0, p1, mask, hitInfo); // если пересечение найдено if (hitObject) { // отрисовываем нормаль к поверхности в точке попадания при помощи Visualizer Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f); } } private void Update() { // обработка пользовательского ввода: проверяем нажата ли клавиша 'огонь' 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 bullet-surface intersection points and surface normals in these points, you can enable Visualizer when the application is running:Для просмотра точек пересечения пуль и нормалей к поверхности в этих точках при стрельбе, можно включить Visualizer во время работы приложения:
- Open the console by pressing ~Откройте консоль, нажав ~
- Type show_visualizer 1Введите: show_visualizer 1