Skip to content

ReimuDodge Tutorial Part 4

Barley edited this page Oct 29, 2020 · 47 revisions

Fleshing out the game

1. Colliders and listeners

Ok, enough messing with code. It's time to add colliders to our objects and get them hitting one-another. Colliders are used to define a "hitbox". What we're going to do is add colliders to the Player object and the Bullet object and then add a trigger method in a new script to do something when they both collide.

Important distinction: All of the colliders and physics components we'll be using here will end with "2D" in the name. These are the distinction between unity's 2D and 3D physics, the latter we won't be needing for our 2D game. Make sure you're using these and not the regular collider/rigidbody components without "2D" appended, and if we forget to type the "2D" you can assume it should be there.

1a. Bullet collider

Start by selecting the bullet and, in the inspector, hit the "Add Component" button. Find the Physics2D sub-menu and select Circle Collider 2D.

The new collider should now be visible on the bullet as a circle:

Feel free to edit the size of the collider to better match the bullet sprite. There's so need to be super exact; what matters is that it roughly fills the visible portion of the bullet. If it's too big, it might be too difficult to dodge. But, maybe that's what you want.

1b. Player collider

Next, do the same thing for the player object. This time feel free to try out a different collider shape. Having an overly complex collider shape is usually not necessary, especially for a simple game like this. A CapsuleCollider2D or rectangular BoxCollider2D would fit the player shape just fine.

For this example, we'll stick with the Touhou classic circular hitbox with a radius of 0.6:

We need this collider to act as a trigger, so that something happens in a script when the collider gets hit. Add a RigidBody component from Components > Physics 2D > RigidBody 2D. Set the Body Type to Kinematic.

Finally, check the Is Trigger checkbox on the CircleCollider2D.

Note: When you use Unity's default physics, checking isTrigger on a collider will result in that object no longer being solid and everything going right through it. This feature is for when you want a collider whose only function is to call a script when touched, as in our case here.

Giving the object a RigidBody will enable collision detection. Kinematic means that physics (e.g. gravity) will be disabled. Is Trigger will let any scripts attached to the object listen for collisions.

2. The player script

Now, create one more script for the player object, just like we did for the bullet. We'll call this script "ReimuDodgePlayer". This time, we aren't going to need the Start() or Update() methods, so we can delete them.

using UnityEngine;

public class ReimuDodgePlayer : MonoBehaviour
{
    
    // This will happen when the player's hitbox collides with a bullet
    void OnTriggerEnter2D(Collider2D other)
    {
        // Test that this works
        print("Player was hit!");
    }
    
}

Breakdown:

void OnTriggerEnter2D(Collider2D other)

This method is another built-in Unity method inherited from UnityEngine. It will automatically be run as soon as a trigger collider attached to the same object as this script is hit. Similar functions include OnTriggerStay2D (called every frame when the colliders are colliding) and OnTriggerExit2D (called when the colliders are no longer touching).

print("Player was hit!");

This will print a message to the console window in Unity, so we can see if it worked. Very handy when debugging.

Another way to debug scripts is to use Visual Studio's build in debugging capability. Double clicking the start of a line will put a red dot there:

Then, after hitting the Attach to Unity button and running the game in Unity, any time that line of code is run it will stop the game and let you step through the code within Visual Studio.

The usefulness of this feature cannot be overstated.

Once you've written the player script and you've attached it to the Player object go ahead and run the game to see if our message displays in the console when the player and bullet come into contact.

"Hey, my code won't display the message! What do I do?"

Don't panic! Since this bit so often goes wrong and can be very frustrating, here is a checklist of things that absolutely must be true for this to work:

  • The script needs to be attached to one of the colliding objects in the scene. (ReimuDodgePlayer on the Player object in our case)
  • The scene object with the script also needs a Collider2D component (CircleCollider2D, BoxCollider2D, etc). It wont work if, for instance, the collider is on a child object.
  • The other colliding object needs a Collider2D component as well
  • At least one of the two colliding object's Collider2D must have Is Trigger checked
  • At least one colliding object must also have a RigidBody2D component with body type set to Kinematic (Kinematic is not necessary for the function to work, but we don't want Unity's default gravity and all that nonsense in our scene)
  • The function name must be exactly "OnTriggerEnter2D" with "2D" at the end, otherwise Unity will not call it
  • The function must also have a single Collider2D parameter (the console should even give you an error if you don't do this).

The Bullet object can just have any basic 2D collider.

3. Sound effects and particles

Nothing currently happens when the player is hit. Let's change that by adding some cool death effects.

We're going to update the player script as follow:

using UnityEngine;

public class ReimuDodgePlayer : MonoBehaviour
{

    [SerializeField]
    private AudioClip deathSound;

    private bool alive = true;

    // This will happen when the player's hitbox collides with a bullet
    void OnTriggerEnter2D(Collider2D other)
    {
        // Only kill Reimu if she's still alive
        if (alive)
        {
            Kill();
        }
    }

    void Kill()
    {
        // Kill Reimu
        alive = false;

        // Get a reference to the player's sprite
        SpriteRenderer spriteRenderer = GetComponentInChildren<SpriteRenderer>();
        // Make the sprite disappear by disabling the the GameObject it's attached to
        spriteRenderer.gameObject.SetActive(false);

        // Finally, disable the FollowCursor script to stop the object from moving
        FollowCursor followCursor = GetComponent<FollowCursor>();
        followCursor.enabled = false;

        // Play the death sound effect
        // At a custom volume
        // And panned to the player's X Posision
        MicrogameController.instance.playSFX(deathSound, volume: 0.5f,
            panStereo: AudioHelper.getAudioPan(transform.position.x));

        // Now get a reference to the death exposion and start it
        ParticleSystem particleSystem = GetComponentInChildren<ParticleSystem>();
        particleSystem.Play();
    }

}

Here we add a new instance variable:

private bool alive = true;

And an if statement so that all of our effects will only run if the player is alive:

if (alive)
{
    Kill();
}

Finally, in our Kill() method, we set alive to false so that it wont run twice:

void Kill()
{
    // Kill Reimu
    alive = false;

We can then safely handle all the death effects after this line.

SpriteRenderer spriteRenderer = GetComponentInChildren<SpriteRenderer>();
spriteRenderer.gameObject.SetActive(false);

Here we make Reimu disappear by finding and disabling her sprite renderer.

GetComponentInChildren() looks through all of the object's children (including itself) until it finds a matching component. In this case, a sprite renderer. This is a "slow" operation, so it's not recommended for frequent use, but with the small number of children Player has and the fact that we're only calling this once it's no biggie.

FollowCursor followCursor = GetComponent<FollowCursor>();
followCursor.enabled = false;

Then, we disable the script that makes her follow the mouse cursor.

GetComponent() does the same thing as GetComponentInChildren() but it only looks at the script's object.

After this we can add some further effects to make the death more visceral!

3a. Death sound

Go back to the ZIP folder containing demo assets and find ReimuDodge_Death.ogg in the Sounds folder. Import this into a new folder in ReimuDodge, also called "Sounds".

To make the sound play on death, we're going to add an editor variable containing an AudioClip.

[SerializeField]
private AudioClip deathSound;

Once you've added this to your script, go ahead and drag the file from the project window into the Death Sound property of your ReimuDodgePlayer script component:

Then, in the script, play it on death using this shortcut method from the NitorInc MicrogameController instance, like so:

MicrogameController.instance.playSFX(deathSound, volume: 0.5f,
    panStereo: AudioHelper.getAudioPan(transform.position.x));

Let's dissect this and look at two optional parameters we specified.

  • volume: The source file is a bit loud, so this simply plays it at half volume (which is auto-multiplied when you lower the audio settings in the main game).
  • panStereo: Panning sounds to the left or right is a good way to make your game audio sound authentic and not so cluttered. This field is a float with input ranging from -1 (all the way on the left ear) to 1 (all the way on the right ear), with 0 being center. For our argument, we're calling a static helper function from the AudioHelper class that automatically returns a float in that range based on a given x position (the player's transform.position.x). This conveniently and neatly makes the sound appear to come from whatever side of the screen Reimu died on. Try it out!

Note: You may be used to playing sounds with AudioSource components on objects. It's fine to do that, but any new AudioSource you create must have its output set to Microgame Mixer -> SFX, so that the game can take care of the pitch and volume automatically.

3b. Death explosion

Now lets add some particle effects.

Right click the Player object in the scene and create a new child object, alongside Rig. Call it something like "Death Explosion".

In the object's inspector, add a new Particle System component from Effects > Particle System. It should be visible straight away:

There are a LOT of settings to mess with here. And, I encourage you to do so. For now, though, just scroll down to the Renderer downdown and press the dotted circle next to Material. This will let you choose a new material to apply to each individual particle.

We could make our own material, but to save time we will just select the BWStar material (from the PaperThief boss stage) to make our particles look like stars.

Now, to make it work as a death explosion, here are some essential settings:

  • Set Duration to 1.00
  • Uncheck Looping
  • Set Start Lifetime to 1
  • Important: Uncheck Play On Awake, otherwise the explosion will play at the start of the game.
  • Under Emission, add a new burst with the + symbol

Finally, we play the animation using this bit of code:

ParticleSystem particleSystem = GetComponentInChildren<ParticleSystem>();
particleSystem.Play();

Now, hit play in the scene editor and get hit by a bullet. Hopefully, all the effects will happen and you now have a very-nearly complete Microgame.

4. Test and tweak

If you're not happy with how it looks or sounds, keep adjusting until you are.

For example:

  • Try adding a Size Over Lifetime curve or a Gravity modifier to the particle system.
  • Move the Bullet object off-screen so that it's not sitting awkwardly next to the player.
  • If you want a nicer color for the background, select the Main Camera object in the scene and change the background color to something else:

Remember, game development is iterative!

5. Winning and losing

There's one last piece of code to mention. Even thought we succeed in killing the merciless executioner Reimu, we didn't do anything to tell the game that the player has now lost. The MicrogameController object in our scene is capable letting us win or lose a stage.

In the next part of the tutorial, we'll specify that our game should result in a victory by default. That means the only code we'll need to add is a way to lose the game.

Modify the OnTriggerEnter2D() method in the player script like so:

// This will happen when the player's hitbox collides with a bullet
void OnTriggerEnter2D(Collider2D other)
{
    // Only kill Reimu if she's still alive
    if (alive)
    {
        Kill();

        // Now tell the MicrogameController in the scene that the game is over
        // and we've lost forever
        MicrogameController.instance.setVictory(victory: false, final: true);
    }
}

Simple, right? The setVictory() method will tell the base game when the player has failed. Otherwise, we assume the player has succeeded. The "final" field tells us whether the player has no way to change the victory status from now on; it's set to true in most cases. You can tell pretty easily that your victory function works because you'll hear some sass from Nitori when you die.

Note: From MicrogameController.instance, you can also call "GetVictory()" to check what the current victory status is set to, or "GetVictoryDetermined()" to check whether the "final" field was set to true. Both are good for changing something's behavior when the player wins or loses.


Commit

It's been a while since our last commit, so now is a good time to commit the sound effect, player script and scene changes.