Firing a Projectile (with Physics)
This lesson covers how to spawn a projectile when the user presses the default fire button (left mouse click) using the Input System.
You should know how to / already have:
- First Person player controller
- Input Actions with a Fire action binding already setup.
- A level to test the scripts and code on.
You can download an example of this scene here.
First setup your level with your player on it and check the player moves.
For maximum benefit you want to be able to look in multiple directions including up and down.
Now we will create the enemy object that we will destroy when we hit the object.
Create an empty prefab. This one is called DummyEnemy
Add objects to the enemy. Make sure they have colliders. You can use a premade object here.
We want this code to be as modular and reusable as possible.
To achieve this we are going to create a new layer called Damageable this layer will be applied to all object that can take damage.
On the root object of the prefab click the drop down and select Add Layer.
Add in the layer.
Apply it to the object.
Click Yes Change Children when asked. This applied the Layer to all child objects of this prefab.
Save the prefab.
Now we will create a generic CharacterProperties script to store common actions for all characters (enemies and the player). This will include things like health, taking damage, death, and anything else you choose to add to the script. Having this common class will make it easier to carry out these actions on our game.
- We have a variable to store the health (this doesn't need to be public)
- We also have a public function that we will call whenever we want to take damage. For example when a bullet hits an object we will pass the damage inflicted from the bullet and reduce our health by that amount.
- This is where we reduce the amount of health by the damage inflicted.
- Now we check to see if the player has any health left. We only check this when we are damaged to reduce the load on the CPU which makes our code more efficient.
- If the health is 0 or less than 0 we run the Destroy function and pass it the gameObject for the player prefab. This removes the object from the scene.
If we wanted to respawn the item we could do this here as well.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/* 1.
This class is designed to be a universal helper class for
managing characters both the player and enemy objects and
handling their common functions like health and taking damage.
It must be placed on the root object of the prefab.
*/
public class CharacterProperties : MonoBehaviour
{
// 2. The health of this object
public float health = 50.0f;
// 3. Function to enable taking damage, take the damage to inflict
public void TakeDamage(float damage) {
// 4. Update the value of health
health = health - damage;
// 5. If health is below 0 destroy this object
if(health <= 0 ) {
// 6. Destroy the object and it's children.
Destroy(this.gameObject);
}
}
}
Attach this script to the root of your Dummyenemy prefab.
Now that we have the target that we will shoot setup we need to setup our player to shoot projectiles.
First we will create a projectile.
We'll call this ProjectileBullet.
First create an empty prefab.
Then add another empty prefab (or add a model) and call this bullet. We will use this to help orientate our bullet correctly.
Note that the bullet is heading towards the blue Z axis.
As children of bullet we have added a cylinder and a sphere.
Return to the root of the prefab.
Add a Rigidbody to the object. This will add physics to the object so that it can fall with gravity (if you don't want this to be impacted by gravity you can ignore this, however you will need to adjust how you move your object).
Add a BoxCollider to the root object. This will be used to handle collision detection to determine if the bullet has hit a valid object.
Set this to be a trigger.
Edit the collider so that it can impact other objects.
Now we are going to setup some properties for our projectile.
Create a script called ProjectileDamage or similar.
Attach this to the root of the prefab.
Open the script in your editor.
Add in the following points according to the numbered sections in the code.
- Damage the button will inflict
- How long the bullet will be in the game for. This is used with the coroutine and IEnumerator
- The velocity / speed of the bullet initially. Use to apply force to the z axis of the rigidbody on the root of the projectile prefab.
- IEnumerator for the bullet timer
- Stores the layer that we want to detect collisions on
- An int variable for the layer index value so that we don't have to recalculate on every collision. This makes the code more efficient.
- Inside the Start function we assign the index value to the placeholder variable in 6
- We get the Rigidbody that is attached to the root of the projectile prefab and apply force along the z axis by creating a new Vector3 with the z axis value being the velocity set in 3.
- Now we want to handle detecting if the object has collided with something and then check if it is something we inflict damage to.
We use the OnTriggerEnter method for this. - We check if the layer on the game object is the layer we want
if(col.gameObject.layer == damageLayer) { ... }
- If it is we get the root gameobject of the prefab using:
GameObject go = col.transform.root.gameObject;
This is a really useful call as the collider we hit could have been a child of the prefab. - Now we get the CharacterProperties script component that is attached to the root object. This has a TakeDamage functin which we pass the damage from the bullet.
go.GetComponent().TakeDamage(damage);
This will then update the health of the object we hit and inflict damage to it. - Finally for this function we destroy the bullet. Make sure to use this.gameObject for this.
- Now we need to timeout the bullet (if you don't want the bullets to disappear you can skip the remainder of the code for this file).
Create an IEnumerator function bullerLifeTimer which takes a float parameter for the length of time the bullet will exist for. - Yield control of the program until the time has passed
- Destroy the bullet when it has been around for the set amount of time.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class ProjectileDamage : MonoBehaviour
{
// 1. The damage the bullet will do on impact
public float damage = 15;
// 2. The amount of time the bullet will exist for
public float lifeTime = 2.0f;
// 3. Set the velocity for the projectile
public float launchVelocity = 1000f;
// 4. IEnumerator for bullet, will destroy after two seconds.
IEnumerator bulletTimer;
// 5. Get the layermask that we want this object to interact with
public LayerMask layerMask;
// 6. This stores the value of the layermask to improve code efficiency
public int damageLayer;
void Start() {
/* 7.
Get the index value of the layer. We have to convert the value
to an index using a log function due to how Unity stores the
LayerMask indexes (they're not the same as the value).
*/
damageLayer = Mathf.RoundToInt(Mathf.Log(layerMask.value, 2));
/* 8.
Apply a force to the projectile and move it at the
velocity specified o nthe z axis (away from the player)
Note we haven't spawned / instantiated the object here,
we will do that on the player when they press the fire
button.
*/
this.GetComponent<Rigidbody>().AddRelativeForce(new Vector3 (0, 0, launchVelocity));
// 17. Create the IEnumerator for the coroutine
bulletTimer = bulletLifeTimer(lifeTime);
// 18. Start the bullet timer coroutine.
StartCoroutine(bulletTimer);
}
/* 14.
This is for the coroutine that will destroy the bullet
after a set period of time
*/
IEnumerator bulletLifeTimer(float time) {
// 15. Yield control of the program and wait for the set period of time
yield return new WaitForSeconds(time);
// 16. Destroy the projectile
Destroy(this.gameObject);
}
// 9. This is used to handle checking what the projectile has hit.
void OnTriggerEnter(Collider col) {
/* 10.
Check if the projectile collided with an object
with the specified layer (damageable)
*/
if(col.gameObject.layer == damageLayer) {
Debug.Log("Hit damageable object!"); //Debug
/* 11.
Really useful get the top level object from the prefab
that was hit.
This us useful as the CharacterProperties is only on
the root object.
*/
GameObject go = col.transform.root.gameObject;
Debug.Log(go); //Debug
/* 12.
Get the CharacterProperties script and run the
TakeDamage function which we pass the bullets damage variable
*/
go.GetComponent<CharacterProperties>().TakeDamage(damage);
// 13. Destroy the bullet / projectile
Destroy(this.gameObject);
}
}
}
You can now customise this bullet / projectile.
Open the Player Prefab.
Add an empty gameobject to use as the position to spawn the projectile.
It should be in front of the player object. If you have a camera or object that you move to handle looking you should set the spawn point as a child of this so that it's transform is automatically updated.
Now we need to create a script to fire the projectile when we press the fire button.
Create a script called ShootProjectile and attach it to the root of the player prefab.
The Shoot script works similar to movement scripts. We are using the Input System
- Add in the Input System package
- Create a variable to store the prefab of the projectile that we want to spawn
- This is optional, we store the projectile variable (Line 36 changes if we don't include this)
- Create a variable to store the spawn point for the projectile.
- We will use the OnFire function
- Use the Instantiate function to spawn / create an instance of the prefab at the position of the projectile spawn and set the objects rotation to that of the prefab
The code on the projectile will take over from here and add the velocity.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 1. Add in the Input System
using UnityEngine.InputSystem;
public class ShootProjectile : MonoBehaviour
{
// 2. The projectile prefab that we will shoot / spawn
public GameObject projectilePrefab;
/* 3.
A variable to store the projcetile, not used here.
This could be modified to allow only one projectile
to be spawned at a time.
*/
GameObject projectile;
// 4. The point that we want to spawn / create the projectile at
public Transform projectileSpawn;
/* 5.
This is linked to the Input Actions, Make sure there
if one called Fire in the Input Actions file.
*/
void OnFire() {
Debug.Log("Projectile Fire pressed"); // Debug
/* 6.
Create the projectile using the proJectilePrefab.
This takes three arguments
- the prefab to spawn
- The position to spawn the projecting (in this case
at the transform of the projectileSpawn variable)
- The direction / rotation to spawn the projectile
*/
projectile = Instantiate(projectilePrefab, projectileSpawn.transform.position,
projectileSpawn.transform.rotation);
}
}
Save the script and return to the Unity Editor.
Drag in the projectile prefab and the spawn point into the script variable boxes.
Test your program, you should now be able to inflict damage on other objects.