Global Game Jam 2023! - Part 3

Global Game Jam 2023! - Part 3

Final jamming days, and EVEN MORE BUGS!

Context

In the last article in this series, I went over how I set up the physics collisions between the main bunny character and the enemy beetles:

And then I went over how I designed and started to build the necessary code to create enemy wave data using Scriptable Objects. And finally, I went over how you can create a new enemy wave data asset through the right-click menu:

Let's continue this development story by first looking at what these assets look like to the user...

Day 4 - Feb. 2nd - Continued

So once you create a new BeetleEnemyWave, this is what the file looks like in the editor:

You can open this list of TimeBeetleEnemyPair objects, and modify the values! Please note, that this is just data. We need a game object in the gameplay scene to use that data when spawning enemies.

To make that game object, we'll create a new script that uses this enemy wave data to allocate enemy beetle objects:

using System.Collections.Generic;
using UnityEngine;

public class BeetleWaveManager : MonoBehaviour
{
    [SerializeField]
    private List<BeetleEnemyWave> m_beetleEnemyWavesToChooseFrom;

    private BeetleEnemyWave m_currentBeetleEnemyWave = null;

    private bool isWaitingInBetweenWaves = false;

    ...
}

I created a new game object (also called BeetleWaveManager) and added this new script. Look at how it looks to use the data on the editor, we could now use the enemy wave files created earlier:

Let's look at the rest of the BeetleWaveManager class implementation:

using System.Collections.Generic;
using UnityEngine;

public class BeetleWaveManager : MonoBehaviour
{
    [SerializeField]
    private List<BeetleEnemyWave> m_beetleEnemyWavesToChooseFrom;

    private BeetleEnemyWave m_currentBeetleEnemyWave = null;

    private bool m_isWaitingInBetweenWaves = false;
    private float m_timeToWaitInBetweenWaves;

    private float m_currentTimeToWaitInBetweenEnemies;
    private int m_currentEnemyIndex;

    private GameObject m_carrotHouse = null;

    private bool isReadyForNextWave;

    private void Start()
    {
        // TODO: IMPROVE THIS
        // EXPENSIVE AND NOT EFFICIENT, but only once per game session
        m_carrotHouse = GameObject.Find("House_SM");

        PickAnewEnemyWave();
    }

    void Update()
    {
        if(m_isWaitingInBetweenWaves)
        {
            m_timeToWaitInBetweenWaves -= Time.deltaTime;

            if(m_timeToWaitInBetweenWaves <= 0f)
            {
                m_isWaitingInBetweenWaves = false;

                PickAnewEnemyWave();
            }
        }
        else
        {
            m_currentTimeToWaitInBetweenEnemies -= Time.deltaTime;

            if(m_currentTimeToWaitInBetweenEnemies <= 0f)
            {
                SpawnBeetleEnemyFromWave(
                    m_currentEnemyIndex,
                    m_currentBeetleEnemyWave.m_timeBeetleEnemyPairs);

                isReadyForNextWave =
                    m_currentEnemyIndex + 1 >= 
                        m_currentBeetleEnemyWave.
                            m_timeBeetleEnemyPairs.Count;

                if (isReadyForNextWave)
                {
                    m_isWaitingInBetweenWaves = true;
                    m_timeToWaitInBetweenWaves = Random.Range(3f, 6f);

                    return;
                }

                UpdateEnemyIndex();
            }
        }
    }

    private void PickAnewEnemyWave()
    {
        m_currentBeetleEnemyWave =
            m_beetleEnemyWavesToChooseFrom[
                Random.Range(
                    0,
                    m_beetleEnemyWavesToChooseFrom.Count - 1)
            ];

        // Prepare for frst enemy
        m_currentTimeToWaitInBetweenEnemies = 
            m_currentBeetleEnemyWave.m_timeBeetleEnemyPairs[0].
                                        m_timeToWaitBeforeSpawning;

        m_currentEnemyIndex = 0;
    }

    private void SpawnBeetleEnemyFromWave(
        int enemyIndex,
        List<TimeBeetleEnemyPair> timeBeetleEnemyPairs)
    {
        GameObject newBeetleEnemy = 
            Instantiate(
                timeBeetleEnemyPairs[enemyIndex].m_enemyBeetleToSpawn,
                gameObject.transform.position,
                Quaternion.identity);

        if(newBeetleEnemy.TryGetComponent(
            out MoveStraightTowardsTarget moveStraightTowardsTarget))
        {
            moveStraightTowardsTarget.SetNewTarget(m_carrotHouse);
        }

    }

    private void UpdateEnemyIndex()
    {
        ++m_currentEnemyIndex;

        m_currentTimeToWaitInBetweenEnemies =
            m_currentBeetleEnemyWave.m_timeBeetleEnemyPairs[
                m_currentEnemyIndex].m_timeToWaitBeforeSpawning;
    }
}

πŸ˜… A bit long, this script file. Please let me know if you have any questions!

And finally, we have a wave of enemies spawning:

Indeed, a lot was done this day, but still, 2 more development days to go!

Day 5 - Feb. 3rd

So overnight the designer created this awesome start screen UI:

This was the only day out of the week that I wasn't able to do any contributions to the project. 3/4 of our team members have full-time jobs, and even though we were all committed to making this the best game possible, we had to deal with our priorities.

I'll take this opportunity to make note of the fact that the game jam organizers did daily work-in-progress streams full of advice and instructions for later days. and on this day they discussed and went over the instructions for how to submit a game project:

And here's a quick screenshot of when they were showing the progress of our project, they went over all of the teams that showed screenshots, GIFs, videos, etc.

As I said, I wasn't able to contribute to the project on this day, but thankfully, my code let the designer come in and start placing instances of beetle wave managers around the main scene:

Scriptable objects to the rescue!! πŸŽ‰

Day 6 - Feb. 4th

Start Screen UI

Pumped up by the fact that I woke up the day before and there was already UI generated on the unity project by the designer, I decided to start this day by tying the start screen design with some simple UI code:

using UnityEngine;

public class StartMenuInteractions : MonoBehaviour
{
    [SerializeField]
    public Camera m_titleCamera;

    [SerializeField]
    public Camera m_gameplayCamera;

    public void OnNewGamePressed()
    {
        if(m_titleCamera == null)
            return;

        Camera.SetupCurrent(m_gameplayCamera);

        m_titleCamera.enabled = false;
    }
}

We created a StartMenuManager game object added this script to it. Afterward, we set up the serialized field values in the editor:

And we now have an interactable menu:

Bunny Attack VFX!

Now let's work on that bunny attack, because it's currently invisible, and that doesn't help the player at all. I created this quick "explosion" particle fx:

And let's add a small modification to the BunnyHammer script so that it plays the particle effect when the player attacks:

using Unity.VisualScripting;
using UnityEngine;

[RequireComponent(typeof(KeyboardMovement3D))]
public class BunnyHammer : MonoBehaviour
{
    ...

    [SerializeField]
    private ParticleSystem m_hammerHitVFX;

    ...

    void Update()
    {
        if (m_canAttack && UserWantsToAttack())
        {
            ...

            // playing bunny attack VFX when bunny attacks
            m_hammerHitVFX.Play();

            ...
        }

        else if (!m_canAttack)
        {
            ...
        }
    }
    ...
}

And now with those simple changes, we have a bomb-like explosion attack:

Quick error❗

At this point, I noticed that when you started the game, the beetles started spawning immediately, even with the game start UI.

We needed to first disable the game object parent that holds all of the beetle enemy waves:

And we need a reference to that GO in the StartMenuInteractions script. This way we can enable it when the user starts the game:

using UnityEngine;

public class StartMenuInteractions : MonoBehaviour
{
    ...

    [SerializeField]
    private GameObject m_beetleEnemyWaveManager;

    public void OnNewGamePressed()
    {
        ...
        m_beetleEnemyWaveManager.SetActive(true);
        gameObject.SetActive(false);
    }
}

And finally, set that value in the editor:

With those small changes, we fixed this quick error πŸŽ‰!

Gameplay UI

Before, our designer also created the necessary assets for a simple Gameplay UI:

So similar to the start screen. I created some gameplay screen logic:

using UnityEngine;
using TMPro;
using UnityEngine.Events;

public class GameplayUImanager : MonoBehaviour
{
    [SerializeField]
    private TextMeshProUGUI m_timerText;
    private float m_currentTime = 0f;

    [SerializeField]
    private TextMeshProUGUI m_greenBeetleTextKilled;
    private int m_greenBeetlesKilled = 0;

    [SerializeField]
    private TextMeshProUGUI m_redBeetleTextKilled;
    private int m_redBeetlesKilled = 0;

    public readonly UnityEvent OnGreenBeetleKilled = new();
    public readonly UnityEvent OnRedBeetleKilled = new();

    private void Start()
    {
        OnGreenBeetleKilled.AddListener(OnGreenEnemyBeetleKilled);
        OnRedBeetleKilled.AddListener(OnRedEnemyBeetleKilled);
    }

    // Update is called once per frame
    void Update()
    {
        m_currentTime += Time.deltaTime;
        m_timerText.text = GetFormattedTime();
    }

    private void OnGreenEnemyBeetleKilled()
    {
        m_greenBeetlesKilled++;
        m_greenBeetleTextKilled.text = $" x {m_greenBeetleTextKilled}";
    }

    private void OnRedEnemyBeetleKilled()
    {
        m_redBeetlesKilled++;
        m_redBeetleTextKilled.text = $" x {m_redBeetlesKilled}";
    }

    private string GetFormattedTime()
    {
        int minutes = (int)(m_currentTime / 60f);
        int seconds = ((int)(m_currentTime)) % 60;

        return $"Time - {minutes}:{seconds}";
    }
}

As you can see, we decided to create an enemy kill counter for 3 types of beetle enemies.

So I did a quick change to the EnemyBeetleDamageController script to also add the type of enemy beetle to the script:

using System;
using UnityEngine;

[Serializable]
public enum BEETLE_ENEMY_TYPE
{
    GREEN,
    RED,
    BLUE
}

public class EnemyBeetleDamageController : MonoBehaviour, IDamageable
{
    [SerializeField]
    private BEETLE_ENEMY_TYPE m_beetleEnemyType;

    ...
}

We'll come back to these new details later.

Bunny House UI

In one of our sync meetings, we talked about ways that we could communicate to the player that their bunny house was being attacked. I saw that the structure of the house contain 5 separate leaves, so I thought, "what would it look like if the leaves started to fall off?" πŸ€”

I got excited about this type of visual effect, and I immediately had a strong theory of how I could pull it off! I went ahead and added a Rigidbody component to each leaf, which allows control of an object's position through physics simulation, but they were disabled by default.

Day 6... to be continued. We'll look at how this random idea ended up! πŸ‘€

Indeed, a lot was done in this post. But just 1 more day to go to finish the game jam, and I'll finish this development story in the next post! πŸ˜„

Lessons learned

  • Re-learned that I truly enjoy creating VFX, or ways to visually communicate game state changes

  • Teams should definitely strive to keep large scenes with many items organized. Perhaps next time, divide the static environment into separate scenes, working with multiple scenes at once.

Please let me know what you think!

  • What do you think of the project's progress so far?

  • If you participated in GGJ, how was your team doing by this point?

  • Can you think of other ways that one can communicate damage to the bunny's carrot house?

πŸŽ‰πŸŽ‰πŸŽ‰ Happy building! πŸŽ‰πŸŽ‰πŸŽ‰

References & Inspirations

Β