Реализация стрельбы
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.h
#pragma once #include <UnigineComponentSystem.h> class ShootInput : public Unigine::ComponentBase { public: COMPONENT_DEFINE(ShootInput, Unigine::ComponentBase); bool isShooting(); };
ShootInput.cpp
#include "ShootInput.h" REGISTER_COMPONENT(ShootInput); using namespace Unigine; using namespace Math; bool ShootInput::isShooting() { // возвращаем текущее состояние левой кнопки мыши и проверяем захват мыши в окне return Input::isMouseButtonDown(Input::MOUSE_BUTTON_LEFT) && Input::isMouseGrab; }
Add the ShootInput property to the player Dummy Object.Добавьте свойство (property) ShootInput к ноде player (Dummy Object).
-
Modify the HandAnimationController component in order to use logic of the ShootInput. Replace your current code with the following one:Измените компонент HandAnimationController, чтобы использовать логику ShootInput. Замените ваш текущий код следующим:
HandAnimationController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineGame.h> #include "FirstPersonController.h" #include "ShootInput.h" class HandAnimationController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(HandAnimationController, Unigine::ComponentBase); PROP_PARAM(Node, player_node, nullptr); PROP_PARAM(Float, moveAnimationSpeed, 30.0f); PROP_PARAM(Float, shootAnimationSpeed, 30.0f); PROP_PARAM(Float, idleWalkMixDamping, 5.0f); PROP_PARAM(Float, walkDamping, 5.0f); PROP_PARAM(Float, shootDamping, 1.0f); // параметры анимации PROP_PARAM(File, idleAnimation); PROP_PARAM(File, moveForwardAnimation); PROP_PARAM(File, moveBackwardAnimation); PROP_PARAM(File, moveRightAnimation); PROP_PARAM(File, moveLeftAnimation); PROP_PARAM(File, shootAnimation); // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); Unigine::Math::vec2 getLocalMovementVector(); void shoot(); protected: // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); private: FirstPersonController *fpsController = nullptr; ${#HL}$ ShootInput * shootInput = nullptr; ${HL#}$ Unigine::ObjectMeshSkinnedPtr meshSkinned = nullptr; float currentIdleWalkMix = 0.0f; // 0 анимация покоя, 1 анимация ходьбы float currentShootMix = 0.0f; // 0 комбинация бездействие/ходьба, 1 анимация стрельбы float currentWalkForward = 0.0f; float currentWalkBackward = 0.0f; float currentWalkRight = 0.0f; float currentWalkLeft = 0.0f; float currentWalkIdleMixFrame = 0.0f; float currentShootFrame = 0.0f; int numShootAnimationFrames = 0; // задаем число анимационных слоев const int numLayers = 6; };
HandAnimationController.cpp
#include "HandAnimationController.h" REGISTER_COMPONENT(HandAnimationController); using namespace Unigine; using namespace Math; Unigine::Math::vec2 HandAnimationController::getLocalMovementVector() { return Math::vec2( Math::dot(fpsController->getSlopeAxisY(), fpsController->getHorizontalVelocity()), Math::dot(fpsController->getSlopeAxisX(), fpsController->getHorizontalVelocity()) ); } void HandAnimationController::init() { fpsController = ComponentSystem::get()->getComponent<FirstPersonController>(player_node); ${#HL}$ shootInput = ComponentSystem::get()->getComponent<ShootInput>(player_node); ${HL#}$ // берем ноду, которой назначена компонента // и преобразовываем ее к типу ObjectMeshSkinned meshSkinned = checked_ptr_cast<Unigine::ObjectMeshSkinned>(node); // устанавливаем количество анимационных слоев для каждого объекта meshSkinned->setNumLayers(numLayers); // устанавливаем анимацию для каждого слоя meshSkinned->setLayerAnimationFilePath(0, FileSystem::guidToPath(FileSystem::getGUID(idleAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(1, FileSystem::guidToPath(FileSystem::getGUID(moveForwardAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(2, FileSystem::guidToPath(FileSystem::getGUID(moveBackwardAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(3, FileSystem::guidToPath(FileSystem::getGUID(moveRightAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(4, FileSystem::guidToPath(FileSystem::getGUID(moveLeftAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(5, FileSystem::guidToPath(FileSystem::getGUID(shootAnimation.getRaw()))); int animation = meshSkinned->getLayerAnimationResourceID(5); numShootAnimationFrames = meshSkinned->getLayerNumFrames(5); // включаем все анимационные слои for (int i = 0; i < numLayers; ++i) meshSkinned->setLayerEnabled(i, true); } void HandAnimationController::shoot() { // включаем анимацию стрельбы currentShootMix = 1.0f; // устанавливаем кадр анимационного слоя в 0 currentShootFrame = 0.0f; } void HandAnimationController::update() { vec2 movementVector = getLocalMovementVector(); // проверяем, движется ли персонаж bool isMoving = movementVector.length2() > Math::Consts::EPS; // обработка ввода: проверка нажатия клавиши 'огонь' ${#HL}$if (shootInput->isShooting()) shoot(); ${HL#}$ // рассчитываем целевые значения для весовых коэффициентов слоев float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f; float targetWalkForward = (float)Math::max(0.0f, movementVector.x); float targetWalkBackward = (float)Math::max(0.0f, -movementVector.x); float targetWalkRight = (float)Math::max(0.0f, movementVector.y); float targetWalkLeft = (float)Math::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::getIFps(); currentShootFrame = Math::min(currentShootFrame + shootAnimationSpeed * Game::getIFps(), (float)numShootAnimationFrames); // плавно обновляем текущие значения весовых коэффициентов currentIdleWalkMix = Math::lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game::getIFps()); currentWalkForward = Math::lerp(currentWalkForward, targetWalkForward, walkDamping * Game::getIFps()); currentWalkBackward = Math::lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game::getIFps()); currentWalkRight = Math::lerp(currentWalkRight, targetWalkRight, walkDamping * Game::getIFps()); currentWalkLeft = Math::lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game::getIFps()); currentShootMix = Math::lerp(currentShootMix, 0.0f, shootDamping * Game::getIFps()); }
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 component and copy the following code:Создайте компонент WeaponController и скопируйте следующий код:
WeaponController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineVisualizer.h> #include "ShootInput.h" class WeaponController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(WeaponController, Unigine::ComponentBase); PROP_PARAM(Node, shooting_camera, nullptr); PROP_PARAM(Node, shoot_input_node, nullptr); Unigine::PlayerDummyPtr shootingCamera = nullptr; ShootInput *shootInput = nullptr; int damage = 1; // маска Intersection чтобы определить, в какие объекты могут попадать пули int mask = ~0; // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); void shoot(); protected: // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); };
WeaponController.cpp
#include "WeaponController.h" REGISTER_COMPONENT(WeaponController); using namespace Unigine; using namespace Math; void WeaponController::shoot() { // задаем начало отрезка (p0) в позиции камеры и конец (p1) - в точке удаленной на 100 единиц в направлении взгляда камеры Vec3 p0 = shootingCamera->getWorldPosition(); Vec3 p1 = shootingCamera->getWorldPosition() + shootingCamera->getWorldDirection() * 100; // создаем объект для хранения intersection-нормали WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create(); // ищем первый объект, который пересекает отрезок (p0, p1) Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo); // если пересечение найдено if (hitObject) { // отрисовываем нормаль к поверхности в точке попадания при помощи Visualizer Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f); } } void WeaponController::init() { // получаем камеру, которой назначен компонент ShootInput shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get()); } void WeaponController::update() { // обработка пользовательского ввода: проверяем нажата ли клавиша 'огонь' if (shootInput->isShooting()) shoot(); }
Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, в которые мы внесли изменения, а затем соберить и запустите приложение, нажав в IDE Ctrl + F5, чтобы Компонентная система сгенерировала property для связи компонента с нодой. После запуска приложения закройте его и вернитесь в UnigineEditor.
- Add the component to the player Dummy Object.Добавьте компонент к Dummy Object 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 Object to the Shoot Input field.Назначьте Dummy Object 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