Skip to content

ReimuDodge Tutorial Part 3

Brian Intile edited this page Oct 20, 2017 · 34 revisions

Scripts

Now that we have our scene objects and graphics, the next step will be to make our objects move around and react to things. We do this by attaching C# scripts to them as components, just like a SpriteRenderer or an Animator.

1. Import a shared scripts

To start things off, we'll use one of NitorInc's shared scripts. Navigate to Assets > Scripts > Generic Microgame Behaviors in the project window and find the FollowCursor script. This contains a few lines of code that will cause whatever object it's placed on to follow the user's mouse pointer at all times.

Drag this script directly onto the Player object in the scene hierarchy or the Player object's inspector window to add it as a component.

Now hit play and see it in action. The Player object, along with its Rig object should now be following your mouse cursor like magic.

Hopefully, this gives you a good idea of how simple scripts can be. There are quite a few other shared scripts in the Generic Microgame Behaviors folder that you might be able to use in creative ways, whilst saving yourself the trouble of writing the code yourself.

2. Write the bullet script

Next, we're going to write our own script and add it to the Bullet object to make it fire towards the player.

2a. Make a new script

First, we need to add a location to store scripts for the game. Make a Scripts folder inside ReimuDodge then right click and Create > C# Script. Call the script "ReimuDodgeBullet".

Warning: For those unfamiliar, C# has a concept of a code namespace. For NitorInc, we keep all of our classes in the common "root" namespace. So, to prevent name conflicts, all of your microgame's scripts need to have the codename of the appended to the beginning and your classes should have the same name as the file they are in.

Open up the script by double-clicking it in the project window. This will open it in whatever editor Unity has linked itself to. Typically, Microsoft Visual Studio. Your script should start out looking something like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ReimuDodgeBullet : MonoBehaviour {

	// Use this for initialization
	void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}
}

A lot of the work has been done for us; the UnityEngine library has been imported and the class we'll be editing has been written and already inherits GameObject. We can place this script onto an object in our scene and it will run the Start() method when the game begins (very similar to a constructor) and Update() every game tick.

We're going to edit both of these methods to make our bullet fly towards the player when the game begins.

At this point, we can go back into Unity and drag the bullet script onto the Bullet object in the scene that we made earlier. It wont do anything yet, but while editing the script it's useful to be able to hit the play button and see if your changes are having the correct effect.

2b. Targeting the player

First of all we'll update our Start() method to target the player's location and calculate a trajectory to towards them:

// Stores the direction of travel for the bullet
private Vector2 trajectory;

// Use this for initialization
void Start ()
{
    // Find the player object in the scene and calculate a trajectory towards them
    GameObject player = GameObject.Find("Player");
    trajectory = (player.transform.position - transform.position).normalized;
}

Lets break this code down.

private Vector2 trajectory;

To start off, we define a private instance variable which will be our bullet's trajectory. Once set, the value of this will persist throughout our bullet's lifetime.

GameObject player = GameObject.Find("Player");

The first thing we do in the start function is use GameObject.Find(), part of the UnityEngine library, to get a reference to our Player object in the scene. This method is effective because we know that our player object has the name "Player", and that there will only ever be one of them.

Later, we'll look at other ways to get references to scene objects and components.

trajectory = (player.transform.position - transform.position).normalized;

Next, we calculate the "trajectory" that the bullet would need to take in order to hit the player.

This line is a bit mathy. We're exploiting the fact that every object in Unity has a transform and we're creating a "normalized" 2D vector from the difference between them.

Don't worry if you don't understand exactly what's happening here. The line is taken straight from a question on the Unity answers community page. Google is your friend!

Ultimately, we want to use the Start() method to set our trajectory variable. Then, we'll use the Update() method to move in that direction.

2c. Moving an object using Update()

Update is called every frame and can be used to perform simple operations such as moving objects or checking victory conditions. It shouldn't be used for "slow" operations as this can have a significant impact on performance. An example of a "slow" operation in Unity would be getting a reference to a scene component using GameObject.Find(). If you need a reference to another object it is best to do this in Start() or some other method that isn't run very often and then save the reference as an instance variable.

Here's our new Update method:

// Update is called once per frame
void Update()
{
    // Only start moving after the trajectory has been set
    if (trajectory != null)
    {
        // Move the bullet a certain distance based on trajectory speed and time
        Vector2 newPosition = (Vector2)transform.position + (trajectory * Time.deltaTime);
        transform.position = newPosition;
    }
}

And, step by step:

if (trajectory != null)

To start off, we only want to move the bullet after we have set its trajectory. For that, we write a simple if statement.

Vector2 newPosition = (Vector2)transform.position + (trajectory * Time.deltaTime);

Next, we calculate a new Vector2 position for the the bullet. It's possible to add a Vector2 to another Vector2 to get a new position. So, getting the new position is as simple as adding our Vector2 trajectory variable to the current bullet position.

(Vector2)transform.position 

This part casts the bullet's current position from a Vector3 into a Vector2; we don't need the Z value because we're working in 2D.

(trajectory * Time.deltaTime)

Remember that Update() is happening every frame. If we just added trajectory to the position every frame our bullet would move extremely fast! We get around this by multiplying it by Time.deltaTime, which is the exact amount of game time that has passed since the previous Update().

Very Important: When the stage in NitorInc speeds up, it makes use of Time.timeScale, which controls the speed of the game. Many Unity features are scaled to it automatically, such as animations and physics. For moving objects manually in code like this, however, multiplying by time.deltaTime is required! To test if your function works at higher speeds, press F while playing the scene to restart it at a faster speed. We'll go more into these debug features later.

3. Test the script

Phew. Hopefully, you kept up with that. Next it's time to test the code you just wrote by hitting the play button in Unity. Make sure your script is a component of your bullet object or nothing will happen! Also make sure your script is saved. Unity won't compile any changes in the code until you save, so save frequently!

If you get errors, refrain from pulling of your hair out. The error will show up in the console window and should tell you what line of code the error is complaining about. The yellow ones can be ignored for the most part, since the project's codebase accrues them sometimes, but the red ones are your problem. You can double click the message to jump straight to the file and double check your C# syntax. If you're still not sure what went wrong, google the error; chances are it's a common one that lots of people will have a solution to.

The bullet should now move towards the player! Well... sort of.

You'll notice the bullet moves to where the player is before the microgame is started, instead of where the cursor actually is. This is because the "SetTrajectory" method is called before the FollowCursor script actually does any updates. Start() is called in every object before Update() is called anywhere. There's also LateUpdate(), which is called on every frame after all Update() functions are finished (this is the one FollowCursor uses). We'll work around this bug in the following section, but keep in mind what order these script functions are called.

4. Create editor variables

Having some of our scripts variables be editable from inside the Unity scene editor can be invaluable when testing and tweaking gameplay. For instance, we could give our bullet a "speed" variable and change its value at any time.

We'll now make some changes to the bullet script:

using System.Collections;
using UnityEngine;

public class ReimuDodgeBullet : MonoBehaviour
{

    [Header("How fast the bullet goes")]
    [SerializeField]
    private float speed;

    [Header("Firing delay in seconds")]
    [SerializeField]
    private float delay;

    // Stores the direction of travel for the bullet
    private Vector2 trajectory;

    // Use this for initialization
    void Start()
    {
        // Invoke the setTrajectory method after the delay
        Invoke("SetTrajectory", delay);
    }

    // Update is called once per frame
    void Update()
    {
        // Only start moving after the trajectory has been set
        if (trajectory != null)
        {
            // Move the bullet a certain distance based on trajectory speed and time
            Vector2 newPosition = (Vector2)transform.position + (trajectory * speed * Time.deltaTime);
            transform.position = newPosition;
        }
    }

    void SetTrajectory()
    {
        // Find the player object in the scene and calculate a trajectory towards them
        GameObject player = GameObject.Find("Player");
        trajectory = (player.transform.position - transform.position).normalized;
    }
}

Firstly, we've added some new instance variables:

[Header("How fast the bullet goes")]
[SerializeField]
private float speed;

[Header("Firing delay in seconds")]
[SerializeField]
private float delay;

Public variables or variables tagged with [SerializeField] will be visible in the script component inside the inspector window of the object the script is attached to. This lets you change them at any time and save the values with the scene.

Additionally, the [Header()] tag lets you display some text above the variable. Very handy for keeping track once you start getting lots of custom variables.

Note: While it's common in Unity to use public variables to edit in the inspector, it's generally preferable to use private variables and the [SerializeField] tag instead. This stops other classes from accessing them. This is called "encapsulation" and is an important aspect of Object Oriented Programming.

Now lets break down what each variable does and how they were implemented.

4a. Adding a speed

The speed will control how fast the bullet goes. This functions in exactly the same manner as our Time.deltaTime variable and we can implement it by simply adding it to the multiplication happening in Update():

Vector2 newPosition = (Vector2)transform.position + (trajectory * speed * Time.deltaTime);

4b. Adding a delay

The delay variable will delay the calculation of bullet trajectory a little while. This will let us customize how long it takes before the action starts and allow us to add more bullets with different firing delays in harder stages.

This also fixes the bug from earlier where the bullet wasn't aiming properly. Now that the script waits before calling SetTrajectory, it will take fire at Reimu's current position when the delay is over.

void SetTrajectory()
{
    // Find the player object in the scene and calculate a trajectory towards them
    GameObject player = GameObject.Find("Player");
    trajectory = (player.transform.position - transform.position).normalized;
}

First of all, we're going to refactor our code to move the trajectory calculation to its own separate method. This is just general good coding practice but it also lets us schedule this method using the Invoke() method:

// Use this for initialization
void Start()
{
    // Invoke the setTrajectory method after the delay
    Invoke("SetTrajectory", delay);
}

Also take note of two things:

  • The SetTrajectory method is taken as a string, in Invoke() and it doesn't allow for any parameters. In the case of our bullet, Invoke() works perfectly and is simple, but that may not always be the case. Some alternate ways to implement delay are to use a coroutine, or to lower the delay variable manually in Update() until it reaches 0 (subtract Time.deltaTime every frame).
  • We didn't include Time.deltaTime at all in our invoke, even though it's timing-based. This is because Unity's Invoke function actually automatically adjusts to the timescale as it increases, no deltaTime scaling needed! Coroutines in Unity do this as well.

5. Test the variables

Now, if everything worked, you should be able to change the speed of the bullet and the delay from the inspector window and run the game to see the effect.

Commit

This is a good time to commit the contents of our new Scripts directory and our changes to the Bullet object.

Clone this wiki locally