Get the FULL version

Unity3D: Programming a machine gun – Part 1

Unity3D: Programming a machine gun – Part 1 thumbnail

This is the first of two posts that will explain how to program a machine gun at the Unity3D game engine. This post will focus on coding the element that defines a weapon of this type: continuous automatic firing. It will show two different ways to achieve this behavior: one based on the Invoke() method call, and the other based on Unity3D’s coroutines. As any other post series in this website, a Unity3D project with all the source code will be available at the last post of the series.

All code in this post will be in C#, but for those who don’t use it, a JavaScript version of the code will be available for download at the end of the post.

So, the code below shows how to capture a button input, and then, fire projectiles while the trigger is being held down. The first way to do it is to use Unity3D’s Invoke() method, as hinted by the official documentation. While this works, it is not necessarily the cleanest and the simplest solution. Here’s the code:

using UnityEngine;
using System.Collections;

public class MachineGunInvoke : MonoBehaviour
{
	//A variable that will count how many bullets were shot
	private int counter = 0;

	// Update is called once per frame
	void Update ()
	{
		//If the "Fire1" button has been pressed
		if(Input.GetButtonDown("Fire1"))
		{
			//Shoot the bullet
			Shoot();
			//Cancel any Shoot() method code execution
			CancelInvoke("Shoot");
		}

		//while the "Fire1" button is being held down
		if(Input.GetButton("Fire1") && !IsInvoking("Shoot"))
		{
			//Execute the Shoot() method in the next 0.2 seconds.
			Invoke("Shoot",0.2f);
		}

		//If the "Fire1" has been released, cancel any scheduled Shoot() method executions
		if(Input.GetButtonUp("Fire1"))
		{
			//Cancel any Shoot() method code execution
			CancelInvoke("Shoot");
		}
	}

	//The shoot method, basically it increments the counter, for now
	void Shoot()
	{
		//display how many bullets were shot
		Debug.Log(counter);
		//increase the number of fired bullets
		counter++;
	}
}

Here’s how this code works: it starts by declaring an integer (line 7) that will count the number of bullets that have been shot. The Shoot() method is declared at line 37, that basically updates the console with the number of bullets that were fired.

There are three if statements blocks, one for each button state (pressed, held down and released). The first if statement (line 13) checks whether the “Fire1” button has been pressed. If it does, it has to fire a bullet immediately (line 16). This block can’t be removed, as it would make the gun delay 0.2 seconds before firing the first bullet.

The second block tests if the “Fire1” button is being held down and if there isn’t any other bullets scheduled to be shot (line 22). If that is the case, it calls the Invoke() method with a method name to schedule execution (line 25) of the Shoot() method. The second parameter in the Invoke() method schedules the Shoot method to execute in the next 0.2 seconds, making the machine gun work as expected.

The last if statement (line 29) checks if the button “Fire1” has been released, canceling all the scheduled Shoot() method calls, case it’s true.

It’s an awful lot of work to program a “simple” behavior, plus we end up with three if statements. At first, it may not appear to be a problem, but if we want to create a more complex behavior, such as locking the gun as its temperature builds up, it would be hard to continue using this algorithm, as it would get even more complex.

Another solution to this problem is to not use the Invoke() method at all. To achieve the same machine gun behavior, without resorting to this method, we need to call Shoot() as a coroutine. Unity3D coroutine is a tool that allows the execution of methods in parallel, kind of like a thread.

Here’s the same code, using coroutines:

using UnityEngine;
using System.Collections;

public class MachineGunCoroutine : MonoBehaviour
{
	//A variable that will count how many bullets were shot
	private int counter = 0;

	// Update is called once per frame
	void Update ()
	{
		//while the "Fire1" button is being held down
		if(Input.GetButton("Fire1"))
		{
			//Start the DelayedShot method as a coroutine
			StartCoroutine("DelayedShot",0.2f);
		}
	}

	//A method that returns a IEnumerator so it can be yield
	IEnumerator DelayedShot(float delay)
	{
		//wait the time defined at the delay parameter
		yield return new WaitForSeconds(delay);
		//display how many bullets were shot
		Debug.Log(counter);
		//increase the number of fired bullets
		counter++;
		//Stop this coroutine
		StopCoroutine("DelayedShot");
	}
}

This code works the same as the other one, except it’s much more simpler and smaller. First, we create a variable on line 7 that will count the number of bullets that were fired. Than, we define the DelayShot() method, which has to return a IEnumerator, because it is yielding code execution. Returning a IEnumerator type is only required when using C#, the DelayedShot() method doesn’t have to return anything when JavaScript is being used. There is a JavaScript version of this code, at the end of the post. (For more information on differences between Unity3D’s JavaScript and C#, read the JavaScript vs C# post series).

Still on the DelayedShot() method, the first thing it does is to schedule a yield (pause) in the code execution (line 24), meaning that the first time it executes, there is no pause. Than, the method displays the number of bullets fired and increases the bullet counter. It then stops all coroutines named “DelayShot” that may be pending to be executed.

At line 16, we start a coroutine that will execute the DelayedShot() method, and define the time that will take to execute this coroutine again.

That’s it! Here’s the code explained in this post, both in JavaScript and C#:

The next and last post of the series will feature how make the bullets actually be fired by the gun and how to make a simple script to aim it. As promised, a Unity3D project with all the code will be available for download, at the last post. You can read it here: Part 2 – Gun and Bullets.

9 Comments to “Unity3D: Programming a machine gun – Part 1”

  1. nofu says:

    Actually, these two scripts do not seem to behave the same, at least the second one does not work as advertised, since there is no immediate fire upon the first mouse down.

    With the first script, you should always get the shot off with a mouse down thanks to:

    if(Input.GetButtonDown(“Fire1”))
    {
    //Shoot the bullet
    Shoot();
    ..
    }

    But with the second script, there is always a delay, ive just tested this.

    • DimasTheDriver says:

      Yes, there’s a delay. But that should not be a problem in most of the cases. However, if the first shot has to occur at the same moment the button gets pressed, just add the following after line 17:


      if(Input.GetButtonDown("Fire1"))
      {
      instantiatedBullet=(GameObject)Instantiate (bullet, muzzleTransform.position, muzzleTransform.rotation * bulletRotation);
      instantiatedBullet.rigidbody.velocity = muzzleTransform.TransformDirection(Vector3.forward * 75 );
      }

  2. nofu says:

    Well that way, you get 2 shots off for a single click, one for the GetButtonDown and second one for the GetButton (with a delay), so i guess you would need to stop the coroutine with every mouse up – (Input.GetButtonUp(“Fire1”)), but then you end up with a code about as long as the one you are trying to improve.

    • DimasTheDriver says:

      Yes, there will be two shots. To make it fire only a single bullet on the button press, add a else if after line 17 instead of a simple if statement:


      else if(Input.GetButtonDown("Fire1"))
      {
      instantiatedBullet=(GameObject)Instantiate (bullet, muzzleTransform.position, muzzleTransform.rotation * bulletRotation);
      instantiatedBullet.rigidbody.velocity = muzzleTransform.TransformDirection(Vector3.forward * 75 );
      }

      • nofu says:

        If you do that, ‘(Input.GetButtonDown(“Fire1”))’ is never gonna get called, since you can’t have Input.GetButton(“Fire1”) FALSE and Input.GetButtonDown(“Fire1”) TRUE at the same time.(you can have the first one TRUE and the other FALSE though)

        But even if you try and turn the order around, you find out that for every GetButtonDown, you will get a couple of GetButton calls spred across the following frames, because the mouse stays down for more than just a frame. So thats a no go.

        I think if you really wanna use coroutines, the best bet is to cancel it with mouse up the way i suggest above.

  3. It would be simpler to start the coroutine in Start().

    Then within the coroutine you do something like this:

    float timeBetweenShot = 0.2f;

    IEnumerator Shooting()
    {
    while (true)
    {
    if (Input.GetButton(“Fire1”))
    {
    Shoot();
    yield return new WaitForSeconds(timeBetweenShot); // 0.2f
    }
    yield return null;
    }
    }

    void Shoot()
    {
    instantiatedBullet=(GameObject)Instantiate (bullet, muzzleTransform.position, muzzleTransform.rotation * bulletRotation);
    instantiatedBullet.rigidbody.velocity = muzzleTransform.TransformDirection(Vector3.forward * 75 );
    }

  4. eriihine says:

    Thanks for this tutorial series ! Just what I needed to implement bullets & guns in my game.

  5. Rich says:

    Hey there,

    I’ve just downloaded and played the Machine Gun project. Great tutorial and has helped me understand the process. However, when I press the Fire1 button – which is set up as P in my case – it comes back with this:

    NullReferenceException
    UnityEngine.Object.Internal_InstantiateSingle (UnityEngine.Object data, Vector3 pos, Quaternion rot)
    UnityEngine.Object.Instantiate (UnityEngine.Object original, Vector3 position, Quaternion rotation)
    MachineGun+c__Iterator1.MoveNext () (at Assets/Scripts/Post 2/C#/MachineGun.cs:48)

    I also went through the whole tutorial and set up myself and it came back with the same result. Could you help me to understand where I’m going wrong here?

    Many thanks for your time.

    Rich

    • DimasTheDriver says:

      Probably, the muzzleTransform hasn’t been assigned to at the Inspector. To fix that, just drag and drop a Transform into the muzzleTransform slot at the ‘MachineGun’ script.

      If that’s not the case, check if you have a prefab named ‘Bullet’ at the ‘Resources’ folder. Maybe the script isn’t finding the bullet at the ‘Resources’ folder.

Leave a Comment

Post Comments RSS