Have you ever wanted to build your own first-person shooter game?
You got the character and movement setup in Unity but need some kind of shooting functionality?
This can easily be accomplished using raycasts.
Step 1:
- Create a new script called AttackController.cs
- Add the script to your Unity character. (TIP: if you don't have a script for movement,
and drag the "RigidBodyFPSController.prefab" into the scene.
Step 2:
- Create another script called Weapon.cs
- Add a game object to the scene which is going to represent a weapon.
- Add Weapon.cs as a component to the game object.
- Add a 3D cube inside the weapon's game object and scale it to the size of the gun barrel.
- Move the 3D cube to the point where the bullet is supposed to be fired from.
- Remove the Mesh- and Mesh filter component from the cube.
- Rename the cube to "GunBarrel"
Step 3:
- Open Weapon.cs
- Add 5 public variables at the top of the class:
public class Weapon : MonoBehaviour
{
public float damage;
public float fireRate; // This variable determines how fast the weapon can shoot
public float fireLength; // This variable determines how far the bullet fly when fired
public float reloadTime; // This variable determines how fast the weapon reload
public Transform gunBarrel; // A reference to the weapon's gun barrel game object
}
- Click on the weapon's game object inside Unity and set up the properties of your weapon.
- Fx damage=10, fireRate=0.5 and fireLength=10
- Drag the gun barrel into the 'gunBarrel' field.
Step 4:
- Open AttackController.cs
- Add the following variables:
public class AttackController : MonoBehaviour
{
public Weapon currentWeapon; // The current weapon our player is using
private bool shoot; // Used to check if the player wants to shoot
private bool isShooting; // Used to check if the player should wait before shooting another bullet
private bool isReloading; // Used to check if the player is reloading
}
- Add the following inside an 'Update()' function and add a new function called 'Shoot()':
public class AttackController : MonoBehaviour
{
// ...
void Update () {
// If the player click the fire button (mouse left click) we set 'fire=true'
// and if the player release the fire button we set 'shoot=false'
// and last if shoot is true we run our 'Shoot()' function
// By doing so can we make the player shoot as long the fire button haven't been released.
if (Input.GetButtonDown("Fire1")) {
shoot = true;
}
if (Input.GetButtonUp("Fire1")) {
shoot = false;
}
if (shoot) {
Shoot();
}
}
void Shoot() {
}
}
- Move down to 'Shoot()' and add the following:
public class AttackController : MonoBehaviour
{
// ...
void Shoot() {
// Return until the fire cooldown is gone and if we don't have a weapon
if (isShooting || currentWeapon == null) {
return;
}
// If a weapon doesn't have a fire rate we assume it's a non-automatic weapon // and we set 'shoot=false' to implement a 'tap-to-shoot' functionality
if (currentWeapon.fireRate == 0) {
shoot = false;
}
// We set 'isShooting=true' and run FireCooldown as an IEnumerator to use the WaitForSeconds class isShooting = true;
StartCoroutine(FireCooldown());
}
IEnumerator FireCooldown() {
// The weapon will be ready to fire another bullet when 'isShooting=false'
yield return new WaitForSeconds(currentWeapon.fireRate);
isShooting = false;
}
}
- Move back to the top of the class and add a private RaycastHit called hit:
public class AttackController : MonoBehaviour
{
public Weapon currentWeapon; // The current weapon our player is using
private bool shoot; // Used to check if the player wants to shoot
private bool isShooting; // Used to check if the player should wait before shooting another bullet
private bool isReloading; // Used to check if the player is reloading
private RaycastHit hit;
// This variable will store information about raycast hits. Check out the docs here.
// ...
}
- Move back to 'Shoot()' and add the following:
public class AttackController : MonoBehaviour
{
// ...
void Shoot() {
// Return until the fire cooldown is gone and if we don't have a weapon
if (isShooting || currentWeapon == null) {
return;
}
// If a weapon doesn't have a fire rate we assume it's a non-automatic weapon // and we set 'shoot=false' to implement a 'tap-to-shoot' functionality
if (currentWeapon.fireRate == 0) {
shoot = false;
}
// We set up a Ray which moves towards the center of the main camera
Ray ray = Camera.main.ViewportPointToRay(new Vector2(.5f, .5f));
// We set the origin of the ray to gun barrel position so it doesn't start at the camera position
ray.origin = currentWeapon.gunBarrel.position;
// We fire the raycast in an 'if' statement and pass the 'ray', our RaycastHit variable
// and we limit the distance of the raycast to match the current weapon's fire length
if (Physics.Raycast(ray, out hit, currentWeapon.fireLength))
{
// if the raycast it's a game object we check if it has the player tag
if (hit.collider.CompareTag("Player")) {
// and finally if it is a player, we want to add some damage to him/her
// I won't explain how to build a health controller class in this tutorial,
// but you would be able to access one like this if the player had one:
// var healthCtrl = hit.collider.GetComponent<HealthController>();
// healthCtrl.ApplyDamage(currentWeapon.damage);
}
}
// We set 'isShooting=true' and run FireCooldown as an IEnumerator to use the WaitForSeconds class isShooting = true;
StartCoroutine(FireCooldown());
}
// ...
}
- That's it! You now have a very simple "weapon attack system" which is able to shoot and detect if it hits another player.
Step 5:
- We also want to add a reload functionality to our weapons, so that we have to reload some times between shooting.
- To simplify this in the tutorial, we will create a system where all weapons use the same type of bullets.
- Add the following above our private variables and add a 'Start()' function above 'Update()' where you set 'currentExtraBullets=maxExtraBullets':
public class AttackController : MonoBehaviour
{
public Weapon currentWeapon; // The current weapon our player is using
public int currentExtraBullets; // The number of bullets the player has available for reloading
public int maxExtraBullets = 100; // The number of extra bullets the player CAN have
private bool shoot; // Used to check if the player wants to shoot
private bool isShooting; // Used to check if the player should wait before shooting another bullet
private bool isReloading; // Used to check if the player is reloading
private RaycastHit hit;
// This variable will store information about raycast hits. Check out the docs here.
void Start() {
currentExtraBullets = maxExtraBullets;
}
// ...
}
- Open Weapon.cs and add the following:
public class Weapon : MonoBehaviour
{
public float damage;
public float fireRate; // This variable determines how fast the weapon can shoot
public float fireLength; // This variable determines how far the bullet fly when fired
public float reloadTime; // This variable determines how fast the weapon reload
public Transform gunBarrel; // A reference to the weapon's gun barrel game object
public int currentBullets; // The number of bullets this weapon has
public int maxBullets = 30; // The number of bullets this weapon CAN have
void Start() {
// We set the weapon to start with a full magazin
currentBullets = maxBullets;
}
}
- Open AttackController.cs and move down to 'Shoot()' and add the following in the if statement at the top of the function:
public class AttackController : MonoBehaviour
{
// ...
void Shoot() {
// Return until the fire cooldown is gone and if we don't have a weapon
// OR
// if the player is reloading the weapon or the weapon's magazin is empty
if (isShooting || currentWeapon == null ||
isReloading || currentWeapon != null && currentWeapon.currentBullets <= 0) {
return;
}
// remove a bullet from the weapon's magazin
currentWeapon.currentBullets -= 1;
// ...
}
// ...
}
- At this point do we use 1 bullet every time we shoot, so we only need to implement a new function to start reloading
- Move to the 'Update()' function and add these new lines below our 'if(shoot)' statement:
public class AttackController : MonoBehaviour
{
// ...
void Update () {
// ...
// if we click the 'R' button on the keyboard we run our reload function
if (Input.GetKeyDown(KeyCode.R)) {
}
// ...
}
- Move down below 'Shoot()' and 'FireCooldown()' and add two new functions called 'Reload()' and 'ReloadCooldown()'
- And add the following lines:
public class AttackController : MonoBehaviour
{
// ...
void Reload() {
// return the function if the player already is reloading
// OR the player don't have a weapon
// OR the player have a weapon that already is full
// OR the player don't have any extra bullets to insert into the weapon's magazin
if (isReloading || currentWeapon == null ||
currentWeapon != null && currentWeapon.currentBullets >= currentWeapon.maxBullets ||
currentExtraBullets <= 0) {
return;
}
// The first thing we need to know is how many bullets we can insert into the player's weapon
var diff = currentWeapon.maxBullets - currentWeapon.currentBullets;
// Then we check if the number of bullets missing is less or equal to the number our player has
// we know if this is true, the player must have more or the exact number of extra bullets as
// the current weapon is missing and we can therefore just set the number of bullets on the weapon
// to the max number of bullets the weapon's magazin can store
if (diff <= currentExtraBullets) {
currentExtraBullets -= diff;
currentWeapon.currentBullets = currentWeapon.maxBullets;
} else {
// if the player doesn't have more or the exact number of bullets it's either 0 or greater
// and we can safely add the current number of extra bullets to our weapon and set
// the player's number of extra bullets to 0
currentWeapon.currentBullets += currentExtraBullets;
currentExtraBullets = 0;
}
// And then we do the exact same thing here as in the Shoot() function
// We could just have added a function to handle this, but it might be easier
// to implement other functionality to the reload or shoot function if you keep the seperated
isReloading = true;
StartCoroutine(ReloadCooldown());
}
IEnumerator ReloadCooldown() {
yield return new WaitForSeconds(currentWeapon.reloadTime);
isReloading = false;
}
}
- And that's everything for the reloading functionality.
- The last thing we need is to move the weapon's game object inside the character's at a position where you want him to hold it
and drag the reference of the weapon into the AttackController component on the player's game object.
Find the finished scripts below:
- Weapon.cs:
public class Weapon : MonoBehaviour
{
public float damage;
public float fireRate; // This variable determines how fast the weapon can shoot
public float fireLength; // This variable determines how far the bullet fly when fired
public float reloadTime; // This variable determines how fast the weapon reload
public Transform gunBarrel; // A reference to the weapon's gun barrel game object
public int currentBullets; // The number of bullets this weapon has
public int maxBullets = 30; // The number of bullets this weapon CAN have
void Start() {
// We set the weapon to start with a full magazin
currentBullets = maxBullets;
}
}
- AttackController.cs:
public class AttackController : MonoBehaviour
{
public Weapon currentWeapon; // The current weapon our player is using
public int currentExtraBullets; // The number of bullets the player has available for reloading
public int maxExtraBullets = 100; // The number of extra bullets the player CAN have
private bool shoot; // Used to check if the player wants to shoot
private bool isShooting; // Used to check if the player should wait before shooting another bullet
private bool isReloading; // Used to check if the player is reloading
private RaycastHit hit;
// This variable will store information about raycast hits. Check out the docs here.
void Start() {
currentExtraBullets = maxExtraBullets;
}
void Update () {
// If the player click the fire button (mouse left click) we set 'fire=true'
// and if the player release the fire button we set 'shoot=false'
// and last if shoot is true we run our 'Shoot()' function
// By doing so can we make the player shoot as long the fire button haven't been released.
if (Input.GetButtonDown("Fire1")) {
shoot = true;
}
if (Input.GetButtonUp("Fire1")) {
shoot = false;
}
if (shoot) {
Shoot();
}
// if we click the 'R' button on the keyboard we run our reload function
if (Input.GetKeyDown(KeyCode.R)) {
}
void Shoot() {
// Return until the fire cooldown is gone and if we don't have a weapon
// OR
// if the player is reloading the weapon or the weapon's magazin is empty
if (isShooting || currentWeapon == null ||
isReloading || currentWeapon != null && currentWeapon.currentBullets <= 0) {
return;
}
// remove a bullet from the weapon's magazin
currentWeapon.currentBullets -= 1;
// If a weapon doesn't have a fire rate we assume it's a non-automatic weapon
// and we set 'shoot=false' to implement a 'tap-to-shoot' functionality
if (currentWeapon.fireRate == 0) {
shoot = false;
}
// We set up a Ray which moves towards the center of the main camera
Ray ray = Camera.main.ViewportPointToRay(new Vector2(.5f, .5f));
// We set the origin of the ray to gun barrel position so it doesn't start at the camera position
ray.origin = currentWeapon.gunBarrel.position;
// We fire the raycast in an 'if' statement and pass the 'ray', our RaycastHit variable
// and we limit the distance of the raycast to match the current weapon's fire length
if (Physics.Raycast(ray, out hit, currentWeapon.fireLength))
{
// if the raycast it's a game object we check if it has the player tag
if (hit.collider.CompareTag("Player")) {
// and finally if it is a player, we want to add some damage to him/her
// I won't explain how to build a health controller class in this tutorial,
// but you would be able to access one like this if the player had one:
// var healthCtrl = hit.collider.GetComponent<HealthController>();
// healthCtrl.ApplyDamage(currentWeapon.damage);
}
}
// We set 'isShooting=true' and run FireCooldown as an IEnumerator to use the WaitForSeconds class isShooting = true;
StartCoroutine(FireCooldown());
}
IEnumerator FireCooldown() {
// The weapon will be ready to fire another bullet when 'isShooting=false'
yield return new WaitForSeconds(currentWeapon.fireRate);
isShooting = false;
}
void Reload() {
// return the function if the player already is reloading
// OR the player don't have a weapon
// OR the player have a weapon that already is full
// OR the player don't have any extra bullets to insert into the weapon's magazin
if (isReloading || currentWeapon == null ||
currentWeapon != null && currentWeapon.currentBullets >= currentWeapon.maxBullets ||
currentExtraBullets <= 0) {
return;
}
// The first thing we need to know is how many bullets we can insert into the player's weapon
var diff = currentWeapon.maxBullets - currentWeapon.currentBullets;
// Then we check if the number of bullets missing is less or equal to the number our player has
// we know if this is true, the player must have more or the exact number of extra bullets as
// the current weapon is missing and we can therefore just set the number of bullets on the weapon
// to the max number of bullets the weapon's magazin can store
if (diff <= currentExtraBullets) {
currentExtraBullets -= diff;
currentWeapon.currentBullets = currentWeapon.maxBullets;
} else {
// if the player doesn't have more or the exact number of bullets it's either 0 or greater
// and we can safely add the current number of extra bullets to our weapon and set
// the player's number of extra bullets to 0
currentWeapon.currentBullets += currentExtraBullets;
currentExtraBullets = 0;
}
// And then we do the exact same thing here as in the Shoot() function
// We could just have added a function to handle this, but it might be easier
// to implement other functionality to the reload or shoot function if you keep the seperated
isReloading = true;
StartCoroutine(ReloadCooldown());
}
IEnumerator ReloadCooldown() {
yield return new WaitForSeconds(currentWeapon.reloadTime);
isReloading = false;
}
}
Author:
Nicolai B. Andersen