This page has been translated automatically.
Unigine Basics
1. Introduction
2. Managing Virtual Worlds
3. Preparing 3D Models
4. Materials
5. Cameras and Lighting
6. Implementing Application Logic
7. Making Cutscenes and Recording Videos
8. Preparing Your Project for Release
9. Physics
10. Optimization Basics
12. PROJECT3: Third-Person Cross-Country Arcade Racing Game
13. PROJECT4: VR Application With Simple Interaction

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.

  1. Create the ShootInput component and copy the following code to it.

    ShootInput.h

    Source code (C++)
    #pragma once
    #include <UnigineComponentSystem.h>
    class ShootInput :
        public Unigine::ComponentBase
    {
    public:
    	COMPONENT_DEFINE(ShootInput, Unigine::ComponentBase);
    
    	bool isShooting();
    };

    ShootInput.cpp

    Source code (C++)
    #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;
    }
  2. Add the ShootInput property to the player Dummy Object.

  3. Modify the HandAnimationController component in order to use logic of the ShootInput. Replace your current code with the following one:

    HandAnimationController.h

    Source code (C++)
    #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

    Source code (C++)
    #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).

  1. Create a WeaponController component and copy the following code:

    WeaponController.h

    Source code (C++)
    #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

    Source code (C++)
    #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.

  2. Add the component to the player Dummy Object.
  3. Assign PlayerDummy to the Shooting Camera field so that the component could get information from the camera.
  4. 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:

  1. Open the console by pressing ~
  2. Type show_visualizer 1

Last update: 2024-12-13
Build: ()