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.cs component and copy the following code to it.

    ShootInput.cs

    Source code (C#)
    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 the current state of the LMBUTTON and check mouse capture on the screen
    		return Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT) && Input.MouseGrab;
    	}
    }
  2. Add the ShootInput.cs component to the player Dummy Node.

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

    HandAnimationController.cs

    Source code (C#)
    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
    {
    	// player's controller with first person view (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;
    
    	// animation settings
    	[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 idle animation, 1 walking animation
    	private float currentShootMix = 0.0f; // 0 combination of idle/walking, 1 shooting 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;
    
    	// set the number of animation layers
    	private const int numLayers = 6;
    
    	private void Init()
    	{
    		// take the node to which the component is assigned
    		// and cast it to ObjectMeshSkinned type
    		meshSkinned = node as ObjectMeshSkinned;
    
    		// set the number of animation layers for each object
    		meshSkinned.NumLayers = numLayers;
    
    		// set animation for each 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);
    	}
    
    	public void Shoot()
    	{
    		// enable shooting animation
    		currentShootMix = 1.0f;
    		// set the animation layer frame to 0 
    		currentShootFrame = 0.0f;
    	}
    
    	private void Update()
    	{
    		vec2 movementVector = LocalMovementVector;
    
    		// check whether the character is moving
    		bool isMoving = movementVector.Length2 > MathLib.EPSILON;
    ${#HL}$
    		// input processing: checking if the 'fire' button is pressed
    		if(shootInput.IsShooting())
    			Shoot();  ${HL#}$
    		// calculate target values for 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 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 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 start playing again
    		meshSkinned.SetLayerFrame(5, currentShootFrame);
    
    		currentWalkIdleMixFrame += moveAnimationSpeed * Game.IFps;
    		currentShootFrame = MathLib.Min(currentShootFrame + shootAnimationSpeed * Game.IFps, numShootAnimationFrames);
    
    		// smoothly update 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);
    	}
    }
  4. 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.

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.cs component and copy the following code:

    WeaponController.cs

    Source code (C#)
    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 int damage = 1;
    	// Intersection mask to define which objects bullets can hit
    	[ParameterMask(MaskType = ParameterMaskAttribute.TYPE.INTERSECTION)]
    	public int mask = ~0;
    
    	public void 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.WorldPosition;
    		Vec3 p1 = shootingCamera.WorldPosition + shootingCamera.GetWorldDirection()  * 100;
    
    		// create an intersection-normal storage object
    		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 to the surface in the hit point using Visualizer
    			Visualizer.RenderVector(hitInfo.Point, hitInfo.Point + hitInfo.Normal, vec4.RED, 0.25f, false, 2.0f);
    		}
    	}
    
    	private void Update()
    	{
    		// handle user input: check if the 'fire' button is pressed
    		if (shootInput.IsShooting())
    			Shoot();
    	}
    }
  2. Add the component to the player Dummy Node.
  3. Assign PlayerDummy to the Shooting Camera field so that the component could get information from the camera.
  4. Assign the player Dummy Node 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-08-26
Build: ()