Dookie Dodger Tutorial

This is a game where we move our car left and right to dodge the bird poos dropping down on us. This project will teach you how to use many important basic features of Unity like how to edit scenes and objects, how to set up prefabs, physics and UI, and how to write simple code.

Create Unity Project

Layout and Windows:

To build games in Unity you need to use different 'Windows' to edit each part of the game. Windows can be moved, opened and closed, but the default layout has everything you need to make simple games. You can reset to the default layout by opening the layout dropdown in the top right.

Assets and Scenes:

Editing Objects:

Set up the Scene:

Road:

Power-lines:

We have the power poles now, but we want power lines for the bird to sit on.

Bird Model:

Camera:

Next we need to make the 'poop' projectiles that the player will have to dodge. We'll do this by making special objects called prefabs that we can duplicate using some code. The first object will be for when the poop is falling, and the second will be for when it splats on the ground (or the car).

Poop Projectile:

This will be the poop that comes out of the bird, but it wouldn't look right if it stayed like this after it hit the ground, so lets make a different poop for it to turn into when it hits something:

Squished Poop:

We can use this new the poop to splat on the ground. A common trick to create effects like this is to delete the moving object and create a new object in the same place. This is often done with bullets and the bullet-holes that appear where they land, or missiles and explosions, or enemies and their dying effects.

The 'Poop' code should look like this:

public GameObject squishedVersion;

private void OnCollisionEnter(Collision collision)
{
    Instantiate(squishedVersion, transform.position, transform.rotation);
    Destroy(gameObject);
}

Make the poop fall and splat:

This isn't quite right though - the squished poop is floating above the ground! We'll need to make the code a little bit more complex to fix this:

The 'Poop' code should now look like this:

public GameObject squishedVersion;

private void OnCollisionEnter(Collision collision)
{
    Vector3 collisionPoint = collision.GetContact(0).point;
    Instantiate(squishedVersion, collisionPoint, transform.rotation);
    Destroy(gameObject);
}

Bird Code:

Now we want to make the bird move back and forth along the power-line, and drop a dookie every few seconds.

The code should look like this:

public Transform leftPoint;
public Transform rightPoint;
public float speed = 2f;

private bool movingRight = true;

void Update()
{
    if (movingRight)
    {
        transform.position = Vector3.MoveTowards(transform.position, rightPoint.position, speed * Time.deltaTime);
        if (transform.position == rightPoint.position)
            movingRight = false;
    }
    else
    {
        transform.position = Vector3.MoveTowards(transform.position, leftPoint.position, speed * Time.deltaTime);
        if (transform.position == leftPoint.position)
            movingRight = true;
    }
}

Test the code!

Make the bird poop:

Now we want the bird to poop every few seconds.

The 'Bird' code should look like this now:

public Transform leftPoint;
public Transform rightPoint;
public float speed = 2f;
public GameObject poopPrefab;
public float poopDelay = 3f;
private float lastPoop = 0f;

private bool movingRight = true;

void Update()
{
    if (movingRight)
    {
        transform.position = Vector3.MoveTowards(transform.position, rightPoint.position, speed * Time.deltaTime);
        if (transform.position == rightPoint.position)
            movingRight = false;
    }
    else
    {
        transform.position = Vector3.MoveTowards(transform.position, leftPoint.position, speed * Time.deltaTime);
        if (transform.position == leftPoint.position)
            movingRight = true;
    }

    if (Time.time >= lastPoop)
    {
        Instantiate(poopPrefab, transform.position, poopPrefab.transform.rotation);
        nextPoop = Time.time + poopDelay;
    }
}

CAR:

Now we need to make the car that the player controls.

The car looks good, but now we need to make it move.

The 'Car' code should look like this:

public Transform leftPoint;
public Transform rightPoint;
public float speed = 5f;

// Update is called once per frame
void Update()
{
    if (Input.GetKey(KeyCode.LeftArrow))
    {
        transform.position = Vector3.MoveTowards(transform.position, leftPoint.position, speed * Time.deltaTime);
    }

    if (Input.GetKey(KeyCode.RightArrow))
    {
        transform.position = Vector3.MoveTowards(transform.position, rightPoint.position, speed * Time.deltaTime);
    }
}

Now set up the points for the car to move between:

Poop on the Car:

The car should be moving nicely between the poles now, but the poops go right through it! Lets fix that.

Game Over:

Lets make the game end if a poop lands on the car. If you did the sumo game, this will be familiar to you, but there's one thing that will need to be different. Since the poop is the thing that checks for collisions and knows when the game should end, we can't just put a reference to a gameover screen in the script, because the poop gets created during the game.

Singleton GameOverManager Script:

A nice easy way to deal with this is by using a singleton. This is when we put a reference to a specific object into a static variable, because static variables can be accessed easily from any script. They are called singletons because there will be problems if you have more or less than a single copy of the specific object.

The GameOverManager code should look like this:

...
using UnityEngine.SceneManagement;

public class GameOverManager : MonoBehaviour
{
    public static GameOverManager singleton;
    public GameObject gameOverPanel;

    // Start is called before the first frame update
    void Awake()
    {
        singleton = this;
        gameOverPanel.SetActive(false);
    }

    public void GameOver()
    {
        gameOverPanel.SetActive(true);
        Time.timeScale = 0;
    }

    public void RestartGame()
    {
        Time.timeScale = 1;
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

Now we need to trigger this code when the poops hit the car, but not when they hit the ground:

The new Poop script looks like this:

public GameObject squishedVersion;

private void OnCollisionEnter(Collision collision)
{
    if (collision.collider.CompareTag("Car"))
        GameOverManager.singleton.GameOver();

    Vector3 collisionPoint = collision.GetContact(0).point;
    Instantiate(squishedVersion, collisionPoint, transform.rotation);
    Destroy(gameObject);
}

Gameover Screen:

Now we need to set up the game over screen that shows when the game ends, as well as a restart button.

The project is now finished!

Bonus Activities!

Sounds:

Games are better with sounds, so lets add some to Dookie Dodger. We're going to need a sound for the car, the bird pooping, the poop hitting the ground, and for the poop hitting the car (or the player losing). After that we can also add music to the background.

Custom sounds:

If you're not making custom sounds, you can use the sounds in the 'Original' folder instead.

Car:

Lets start with the car sounds:

The sound should be playing, and it should sound pretty good - except that it stops after the sound is finished - we want it to keep playing instead.

Bird (Pooping):

Next we want to add the pooping sound:

Now we need to tell the sound to play when the bird poops.

Poop Landing:

We can now do the same thing for the poop landing.

Since this prefab gets created when the falling poop hits the ground, we can just make it play the sound right away and it will work without any script changes.

Game Over:

The last sound we want to add is for when when a poop hits the car and the player loses. We could make the falling poop play it when it hits the car, but it makes a bit more sense to have the 'GameOverManager' play the game over sound - since it is in charge of the game ending.

Random Sounds:

When exactly the same sound is playing regularly in a game it can often start to get annoying and sound very fake and distracting, so game developers often use some simple tricks to help fix that - randomly choosing between a few different sounds, and changing the pitch of the sound a little bit so it doesn't sound exactly the same. Lets do these for the bird, since it makes sounds quite often:

...

if (Time.time >= nextPoop)
{
    Instantiate(poopPrefab, transform.position, poopPrefab.transform.rotation);
    nextPoop = Time.time + poopDelay;

    GetComponent<AudioSource>().pitch = Random.Range(0.8f, 1.2f);

    int randomSoundNumber = Random.Range(0, randomPoopSounds.Count);
    GetComponent<AudioSource>().clip = randomPoopSounds[randomSoundNumber];

    GetComponent<AudioSource>().Play();
}

The 'GetComponent' function is useful, but using it too often can make our game run slowly, and it's also quite long - so since we have a lot of code doing things to the same component let's save it to a variable so we only have to do it once:

...

if (Time.time >= nextPoop)
{
    Instantiate(poopPrefab, transform.position, poopPrefab.transform.rotation);
    nextPoop = Time.time + poopDelay;

    AudioSource audioSource = GetComponent<AudioSource>();
    audioSource.pitch = Random.Range(0.8f, 1.2f);

    int randomSoundNumber = Random.Range(0, randomPoopSounds.Count);
    audioSource.clip = randomPoopSounds[randomSoundNumber];

    audioSource.Play();
}

Much better - now you could try doing one or both of these tricks for the poops landing, since that sound also plays very often.

Unpredictable Bird:

Let's make the bird poop randomly so that the game is a bit harder.

Now lets make the bird sometimes change direction.

The full 'Bird' code should now look like this:


public Transform leftPoint;
public Transform rightPoint;
public float speed = 2f;
public GameObject poopPrefab;
public float poopDelay = 2f;
public float randomDelay = 0.5f;
public float changeDirectionChance = 0.4f;

public List<AudioClip> randomPoopSounds;

private float nextPoop = 0f;
private bool movingRight = true;

// Update is called once per frame
void Update()
{
    if (movingRight)
    {
        transform.position = Vector3.MoveTowards(transform.position, rightPoint.position, speed * Time.deltaTime);
        if (transform.position == rightPoint.position)
            movingRight = false;
    }
    else
    {
        transform.position = Vector3.MoveTowards(transform.position, leftPoint.position, speed * Time.deltaTime);
        if (transform.position == leftPoint.position)
            movingRight = true;
    }

    if (Time.time >= nextPoop)
    {
        Instantiate(poopPrefab, transform.position, poopPrefab.transform.rotation);
        nextPoop = Time.time + poopDelay + Random.Range(-randomDelay, randomDelay);
        AudioSource audioSource = GetComponent<AudioSource>();
        audioSource.pitch = Random.Range(0.8f, 1.2f);
        int randomSoundNumber = Random.Range(0, randomPoopSounds.Count);
        audioSource.clip = randomPoopSounds[randomSoundNumber];
        audioSource.Play();

        if (Random.value <= changeDirectionChance)
            movingRight = !movingRight;
    }
}

The game is still a little too easy, so try duplicating the bird so there are two of them at once!