Shooting Implementation
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.
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.
- In the WeaponController component to start the shooting logic.
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.
-
Create the ShootInput component and copy the following code to it.
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 the current state of the LMBUTTON and check mouse capture on the screen return Input::isMouseButtonDown(Input::MOUSE_BUTTON_LEFT) && Input::isMouseGrab; }
-
Add the ShootInput property to the player Dummy Object.
-
Modify the HandAnimationController component in order to use logic of the ShootInput. Replace your current code with the following one:
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); // animation parameters PROP_PARAM(File, idleAnimation); PROP_PARAM(File, moveForwardAnimation); PROP_PARAM(File, moveBackwardAnimation); PROP_PARAM(File, moveRightAnimation); PROP_PARAM(File, moveLeftAnimation); PROP_PARAM(File, shootAnimation); // declare methods to be called at the corresponding stages of the execution sequence COMPONENT_INIT(init); COMPONENT_UPDATE(update); Unigine::Math::vec2 getLocalMovementVector(); void shoot(); protected: // world main loop overrides void init(); void update(); private: FirstPersonController *fpsController = nullptr; ${#HL}$ ShootInput * shootInput = nullptr; ${HL#}$ Unigine::ObjectMeshSkinnedPtr meshSkinned = nullptr; float currentIdleWalkMix = 0.0f; // 0 idle animation, 1 walking animation float currentShootMix = 0.0f; // 0 combination of idle/walking, 1 shooting animation 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; // setting the number of animation layers 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#}$ // take the node to which the component is assigned // and cast it to ObjectMeshSkinned type meshSkinned = checked_ptr_cast<Unigine::ObjectMeshSkinned>(node); // set the number of animation layers for each object meshSkinned->setNumLayers(numLayers); // set animation for each layer 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); // enable all animation layers for (int i = 0; i < numLayers; ++i) meshSkinned->setLayerEnabled(i, true); } void HandAnimationController::shoot() { // enable shooting animation currentShootMix = 1.0f; // set the animation layer frame to 0 currentShootFrame = 0.0f; } void HandAnimationController::update() { vec2 movementVector = getLocalMovementVector(); // chack wether the character is moving bool isMoving = movementVector.length2() > Math::Consts::EPS; // input processing: checking if the 'fire' button is presse ${#HL}$if (shootInput->isShooting()) shoot(); ${HL#}$ // calculate target values for layer weights 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); // apply current animation 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 animation frames: set the same frame for all layers to ensure synchronization meshSkinned->setLayerFrame(0, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(1, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(2, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(3, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(4, currentWalkIdleMixFrame); // set the current frame for each animation layer to 0, to begin playback from the start meshSkinned->setLayerFrame(5, currentShootFrame); currentWalkIdleMixFrame += moveAnimationSpeed * Game::getIFps(); currentShootFrame = Math::min(currentShootFrame + shootAnimationSpeed * Game::getIFps(), (float)numShootAnimationFrames); // smoothly update current weight values 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.
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).
-
Create a WeaponController component and copy the following code:
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 mask to define objects to be affected by bullets int mask = ~0; // declare methods to be called at the corresponding stages of the execution sequence COMPONENT_INIT(init); COMPONENT_UPDATE(update); void shoot(); protected: // world main loop overrides void init(); void update(); };
WeaponController.cpp
#include "WeaponController.h" REGISTER_COMPONENT(WeaponController); using namespace Unigine; using namespace Math; void WeaponController::shoot() { // set the line starting point (p0) in the camera position and end point (p1) in the point 100 units away in the camera view direction Vec3 p0 = shootingCamera->getWorldPosition(); Vec3 p1 = shootingCamera->getWorldPosition() + shootingCamera->getWorldDirection() * 100; // create an intersection-normal storage object WorldIntersectionNormalPtr hitInfo = WorldIntersectionNormal::create(); get the first object intersected by the (p0,p1) line Unigine::ObjectPtr hitObject = World::getIntersection(p0, p1, mask, hitInfo); // if the intersection is found if (hitObject) { // render the intersection normal to the surface in the hit point using Visualizer Visualizer::renderVector(hitInfo->getPoint(), hitInfo->getPoint() + hitInfo->getNormal(), vec4_red, 0.25f, false, 2.0f); } } void WeaponController::init() { // getting the camera to which the ShootInput component is assigned shootingCamera = checked_ptr_cast<Unigine::PlayerDummy>(shooting_camera.get()); } void WeaponController::update() { // handle user input: check if the 'fire' button is pressed 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.
- Add the component to the player Dummy Object.
- Assign PlayerDummy to the Shooting Camera field so that the component could get information from the camera.
-
Assign the player Dummy Object to the Shoot Input field.
To view the bullet-surface intersection points and surface normals in these points, you can enable Visualizer when the application is running:
- Open the console by pressing ~
- Type show_visualizer 1