INTD 450 Game Development Learning Plan

2025 Jan - Apr

Learning Plan

1. Researching Procedural Content Generation

I only have a little understanding of PCG from some class, but I am interested in learning more about it. Possibly PCG is a good way to create a lot of content for a game with a small team. I want to learn how to implement PCG in Unity and how to use it to create interesting levels and game mechanics.

2. Improving my understanding of projectile physics in Unity with Linear algebra

I am taking a graphics class this semester, and I want to apply what I learn to my game. Since our project is a 2D shooter game, It would be a good opportunity to practice my understanding of linear algebra to implement projectile physics in Unity.

3. Improving my collaboration and leadership skills by working on a team project

When I was in CMPUT250, it was not easy to work as a team. Sometimes, someone has to say “No” to the team to make the project successful. However, everyone was too nice to say something like that. That is needed from time to time to make sure the project is on track. I am going to try to improve my leadership and communication skills by communicating with my team members effectively and clearly. For example, I am going to use this project management tool called “Plane” to organize our project and make sure everyone is on the same page. I am also going to try to communicate with my team members as soon as possible to make sure we are on the same page instead of waiting for others to bring up issues.

4. Improving my understanding of Audio system in Unity and better music experience with FMOD

I have never used FMOD before, but I heard it is a powerful tool for implementing sound in games. I want to learn how to use FMOD to implement sound effects and background music for our game. The knowledge I acquired from MUSIC 245 (for example, stero audio, various plug-ins such as EQ) will be helpful to make our game more immersive and enjoyable.

Weekly Journal

Week of Jan 13

Our team members are not confirmed yet. I visited Eric and asked him about concerns about our team project (short of team members). After that, I shared some sample games on our discord server to explore ideas for our game projects. We are not sure what view we are going to use for our game. More likely to be Top-down view.


Week of Jan 20

I researched a variety of project management tools and DAWs (for music). “Plane” has become our choice for a project management tool. I tried to organize our project with Plane. I expect the quality of our project to be better than the last project I worked on with Plane and my approach to communication.

I would have to study more about FMOD and Unity’s new input system to implement the sound and input system for our game.


Week of Jan 27

I had to spend some time studying FMOD and Unity’s new input system to implement the sound and input system for our game. I also had to spend some time studying the Plane project management tool to organize our project. I also had to spend some time studying the DAWs to implement sound for our game.


Week of Feb 3

I was sick this week, so I couldn’t do much. I ended up spending a lot of time searching appropriate sound effects for our game. I also had to spend many hours dealing with merge conflicts from FMOD and new Input system in our project.

Our input system and sound system are ready to be used in our game. In the following week, I will focus on finishing basic sound effects and environment sounds.


Week of Feb 10

I have added my new learning objective, which is about FMOD after talking to Eric. I am trying to add variations of sound effects such as jumpGroan, footstep for our game. I am also working on the background music as well as small details such as landing sound effects from the jump.

I was supposed to work on environment sounds or some background music in the week of Feb 17. However, I produced one background music already. I will focus on more environment sounds and some sound effects for the game.


Week of Feb 17

Based on what other members have done, I have added parameters and replaced some sound effects with FMOD so that we can have more control over the sound effects. We have this BulletTime feature in our game, which slows down the time. When the BulletTime is activated, all the sound is also slowed down (with pitch shift).

BulletTime with FMOD Pitch change
IEnumerator ActivateBulletTime()
{
    _audio.SetPitch(0.5f);
    isBulletTimeActive = true;

    // Global slow-motion (Enemy, Bullet...etc)
    Time.timeScale = enemyTimeScale;
    Time.fixedDeltaTime = 0.02f * enemyTimeScale;

    // Adjust player animation speed
    anim.speed = playerTimeMultiplier;

    yield return new WaitForSecondsRealtime(bulletTimeDuration);

    _audio.SetPitch(1.0f);
    // Restore speed setting
    Time.timeScale = 1f;
    Time.fixedDeltaTime = 0.02f;
    bulletTimeGauge = 0f;
    isBulletTimeActive = false;

    // Restore player animation speed
    anim.speed = 1f;
}
public void SetPitch(float pitch)
{
Bus masterBus = RuntimeManager.GetBus("bus:/");
masterBus.getChannelGroup(out FMOD.ChannelGroup masterChannelGroup);
masterChannelGroup.setPitch(pitch);
}
timeScale bug

When SetPitch(float pitch) is called, The master bus and channel group from FMOD are acquired, and the pitch of the channel group is set to the pitch value. Any sound coming from the master bus will be affected by the pitch value.

The green screen indicates that the BulletTime is activated. The sound and animation are also slowed down. The blue bar on the top left corner is the BulletTime gauge, that show how much time is left for the BulletTime. However, there is a bug that the BulletTime gauge is not updated when the BulletTime is activated.

This is related to Time.timeScale and Time.fixedDeltaTime. We will have to revisit ActivateBulletTime() to fix this bug.

We have more parameters controling sounds but I would have to discuss with other members regarding what each parameter would mean. For example, WeaponType parameter (discrete number) would be used to control the sound of the weapon.


Week of Feb 24

I added an in_game debug console. It would be useful for debugging our game in build since I notice that our game behaves differently in build and editor (for example, it seems like FMOD has less cache memory in build).

Issue with Drone

I also added a sound effect for enemies drone (flying sound), and wanted to play the sound at their location. However, I ran into a problem because I have so many drones in the scene.

// Move Drone
private void MoveDrone()
{
timer += Time.deltaTime;
if (timer >= changeDirectionTime)
    {
        ChangeDirection();
        timer = 0;
    }
float floatingY = Mathf.Sin(Time.time _ 2f) _ floatStrength;

        RaycastHit2D wallCheck = Physics2D.Raycast(transform.position, moveDirection, 1f, LayerMask.GetMask("Terrain"));
        if (wallCheck.collider != null)
            {
                ChangeDirection();
            }
        rb.linearVelocity = (moveDirection + new Vector3(0, floatingY, 0)) * moveSpeed;
    }

Firstly, the problem is that MoveDrone() is called every frame, and PlayOneShot() is called every frame. Secondly, FMOD cache memory was not enough to load all the sounds.

I added some cooldown time to this specific sound effect. However, it did not solve the problem in build (it was okay in editor). I had to remove the sound effect for Demo. After Demo, I need to come up with a better solution to not put functions in Update().

Others mentioned that sometimes enemies(drone) are not shooting. I investigated the problem and found that the drones were not shooting because they are not aligned with the player when they are randomly generated.

On the left image, the green line indicates player and drone are very close based on their x and y position.

private bool CheckNearbyPlayers()
{
    if (player == null)
        return false;
    return Vector3.Distance(transform.position, player.position) <= detectionRange;
}

However, CheckNearbyPlayers returns false. Because drone has different z position from player, it is far away from the player although it looks close. Because of this z position issue, drones sometimes pass through walls like the right image.

Thanks to CMPUT 411 (computer graphics), I was able to recognize the problem and fix it. I had to make sure matching z position of the player. After the Demo, we need to make sure that all objects are aligned with the player.


Week of Mar 3

I have produced different SFXs for rarity of weapons. After the Demo, one of the feedbacks was control is a bit stif. Esepcailly, it is not responsive when the player tries to use the shop.

One of the reasons is that we were using Update() to track the player’s input and how long they have been pressing the button. However, Update() is called every frame, and it was not a good idea to use Update() to track the player’s input. I had to change the way we track the player’s input.

We were using Update() to check if the player is in the range of the shop and timing how long the player has been pressing the button.

Simple way with InputManager
// From InputManager.cs
public bool FInput { get; private set; } = false;

private void Update()
{
    FInput = Input.Player.F.WasPressedThisFrame();
}

This way, I could easily check if F key was pressed this frame. I noticed that I can’t track how long the player has been pressing the button. There is another function called IsPressed() that returns true if the button is pressed. Even with this function, I have to track how long the player has been pressing the button from Update().

Another way with InputManager
public bool FInput { get; private set; } = false;

private void OnEnable()
    {
        _input.Player.F.performed += SetFInput;
        _input.Player.F.canceled += SetFInput;
    }
private void OnDisable()
    {
        _input.Player.F.performed -= SetFInput;
        _input.Player.F.canceled -= SetFInput;
    }
public void SetFInput(InputAction.CallbackContext ctx)
    {
        FInput = ctx.started;
        if (!FInput)
        {
            _fPressTime = 0f;
        }
        else
        {
            _fPressTime = ctx.startTime;
        }
    }
public double GetFPressTime()
    {
        return _fPressTime;
    }

Basically, I subscribe to the performed and canceled event of the F key. When the F key is pressed, SetFInput() is called. I can track when the player started pressing the button with ctx.startTime.

All I have to do is to check if the shop is ready to take the player’s input (Checking if the animation is done stateInfo.IsName("opened") with OnTriggerEnter2D()).

When to check holdTime
IEnumerator GenerateMultipleGuns()
    {
        if (isGeneratingGuns || isRecycling) yield break;
        isGeneratingGuns = true;
        holdTime = _input.GetFPressTime();
        while (_input.GetFPressTime() != 0)
        {
            yield return null;
            if (Time.fixedUnscaledTime - holdTime >= threshold)
            {
                for (int i = 0; i < 10; i++)
                {
                    TryGenerateGun();
                    yield return new WaitForSeconds(0.3f);
                }
                isGeneratingGuns = false;
                yield break;
            }
        }
        if (Time.fixedUnscaledTime - holdTime < threshold)
        {
            TryGenerateGun();
        }
        isGeneratingGuns = false;
    }

Within GenerateMultipleGuns(), I handle creating single or multiple guns based on how long the player has been pressing the button. holdTime only updates when the player presses the button so this way is better than our previous way.


Week of Mar 10

Drone passing through walls Bug

I thought this bug was fixed since I made sure that all objects are aligned with the player. However, I found that the drone was still passing through walls. I had to investigate the problem again.

Old ChangeDirection()
private void ChangeDirection()
    {
        float randomX = Random.Range(-1f, 1f);
        moveDirection = new Vector3(randomX, 0, 0).normalized;

        if (moveDirection.x > 0)
        {
            transform.localScale = new Vector3(1, 1, 0);
            turret.localScale = new Vector3(1, 1, 0);
        }
        else if (moveDirection.x < 0)
        {
           transform.localScale = new Vector3(-1, 1, 0);
           turret.localScale = new Vector3(-1, 1, 0);
        }
    }

This code is supposed to change the direction of each drone (random direction). This was problematice because there are two cases where the drone changes its direction. One is when the drone hits the wall, and the other is when the drone spends some time with the facing direction.

OnTriggerEnter2D()
private void OnTriggerEnter2D(Collider2D other)
    {
        if (isFalling && other.CompareTag("Terrain"))
        {
            Vector3 explosionPos = transform.position + new Vector3(0, -1f, 0);
            GameObject explosionInstance = Instantiate(explosion, explosionPos, Quaternion.identity);
            _audio.PlayOneShot(_audio.Explosion, transform.position);
            Destroy(explosionInstance.gameObject, 5f);
            base.Die(resourceAmount);
        }
        else if (!isFalling && other.CompareTag("Terrain"))
        {
            ChangeDirection();
        }
    }

When the drone hits the wall, ChangeDirection() is called. Let’s say the drone is facing right. When the drone hits the wall, the drone is supposed to change its direction to the left. However, the drone’s next direction depends on randomX = Random.Range(-1f, 1f);. This can cause the drone to face right again (to the wall) after hitting the wall, and ChangeDirection() is called again. This can cause the drone to pass through the wall.

New ChangeDirection()
private void ChangeDirection(bool collisonHappen = default(bool))
    {
        if (!collisonHappen)
        {
            moveDirection = Random.Range(0, 2) == 1 ? Vector3.right : Vector3.left;
        }
        else
        {
            moveDirection.x = -rb.linearVelocity.normalized.x;
        }
        transform.localScale = new Vector3(Mathf.Sign(moveDirection.x), 1, 1);
        turret.localScale = new Vector3(Mathf.Sign(moveDirection.x), 1, 1);
    }

I added a parameter to ChangeDirection() to check if the drone hits the wall. If the drone hits the wall, the drone’s direction is set to the opposite direction of the wall (not random). This way, the drone will not pass through the wall.

CineMachine Virtual Camera Transition

I wanted to add some camera transition from the starting menu so people could see the character closer and the whole room after the transition. I used CineMachine to create this transition.

Revisiting new Input System

I wanted to make sure that the player faces to the right before starting the game. However, the default value for InputManager was making the player face to the left. I thought I could disable input (in Start() or Awake()) until the player clicks the start button. The order of function calls was Awake() -> OnEnable() -> Start(). My solution was to set some condition in OnEnable() to enable input when conditions are met or I could use public void EnableInput() which subscribes input events.

Light2D and ParticleSystem

I also experimented with Light2D and ParticleSystem. Because we have this ricochet effect when the bullet hits the wall, I wanted to add Light2D and ParticleSystem to it.

I have researched how to add Light2D and ParticleSystem. After spending some time, I figured out that Light2D is not compatible with ParticleSystem, and it seems like Unity is not planning to support Light2D with ParticleSystem. The last comment was from 2023. I am using Unity 6 which is the newest version of Unity in 2025. This is still not delt with.

I thought I might be able to use 3D objects and 2D objects in the same scene. However, it seems like that is only possible when my project is a 3D project. I tried to use different shaders and custom shaders to make it work, but it did not work. I assume this is related to the rendering pipeline.

I had to find another way to add Light2D to the bullet.


Week of Mar 17

The need of optimization (Light2D and ParticleSystem)

As you can see, it requires a lot of computation to render Light2D and ParticleSystem.

Previous spark VFX
void LateUpdate()
    {
        int count = m_ParticleSystem.GetParticles(m_Particles);

        while (m_Instances.Count < count)
            m_Instances.Add(Instantiate(m_Prefab, m_ParticleSystem.transform));

        bool worldSpace = (m_ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World);
        for (int i = 0; i < m_Instances.Count; i++)
        {
            if (i < count)
            {
                if (worldSpace)
                    m_Instances[i].transform.position = m_Particles[i].position;
                else
                    m_Instances[i].transform.localPosition = m_Particles[i].position;
                m_Instances[i].SetActive(true);
            }
            else
            {
                m_Instances[i].SetActive(false);
            }
        }
    }

What happens with Particle System (Spark) is that it creates a random number of particles around the particle system. Depending on the lifetime of the particles, the number of particles gradually decreases.

The previous approach is accurate because it attaches the Light2D to every single particle and updates positio. However, it requires a lot of computation to render Light2D and ParticleSystem. Simply, it means that I need to track of every single particle system * P! (almost factorial the max number of particles).

Avg spark VFX
void LateUpdate()
    {
        int particleCount = m_ParticleSystem.GetParticles(m_Particles);
        if (particleCount == 0)
        {
            return;
        }

        // Calculate the average position of all particles
        Vector3 averagePosition = Vector3.zero;
        float totalLifetime = 0f;
        float totalRemainingLifetime = 0f;


        for (int i = 0; i < particleCount; i++)
        {
            averagePosition += m_Particles[i].position;
            totalLifetime += m_Particles[i].startLifetime;
            totalRemainingLifetime += m_Particles[i].remainingLifetime;
        }
        averagePosition /= particleCount;
        m_LightInstance.transform.SetParent(m_ParticleSystem.transform);

        bool worldSpace = (m_ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World);
        if (worldSpace)
            m_LightInstance.transform.position = averagePosition;
        else
            m_LightInstance.transform.localPosition = averagePosition;

        // Adjust intensity based on remaining lifetime percentage
        float lifetimeRatio = totalRemainingLifetime / totalLifetime;
        light2D.intensity = Mathf.Lerp(0f, 1f, lifetimeRatio); // Adjust the max intensity as needed

        // Activate light
        m_LightInstance.SetActive(true);
    }

The new approach is to calculate the average position of all particles and set one Light2D to that position. This way, I can reduce the number of Light2D objects to 1. I also added a light intensity based on the remaining lifetime of the particles.

Lava Shader

I also experimented with a lava shader. We are planning to add some raft so that the player can cross the lava. I used Noise to create lava effect. I also added some UV distortion to make it look like lava is moving.


Week of Mar 24

I have mixed and added extra SFXs (missileLaunch, Lava, bouncePad and etc) for our game. Although more adjustments are needed, I added lava and shader before QA day.

Lava with Raft

There are only two scenes in our games. However, these two scenes are frequently loaded and unloaded as player goes through the game. Player object is DontDestroyOnLoad and it is not destroyed when the scene is changed (unless player dies or goes back to mainMenu).

In any new scene, Player object is not destroyed. However, other objects should be able to refer to the original player object (which was created from a previous scene). SceneManager.sceneLoaded += OnSceneLoaded; is called in Awake() and OnSceneLoaded() is called when a new scene is loaded. I had to make sure that the original player object is not destroyed when a new scene is loaded.

Adding visual effect to Environment

I added Light2D to one of our map segments. Plus, I added some ParticleSystem to one of our interacable objects. Our map tile was not complete yet so I could not add more visual effects to the environment. However, I wanted to make sure that we have some visual effects before QA day.

FMOD discrete parameter bug

In FMOD, I could manage SFXs with parameters. As you can see, I have a parameter called WeaponType which is a discrete parameter. In our game, there are 3 different types of guns (pistol, SMG, HandCanon). I thought it would be a good idea to manage these SFXs with parameters instead of having them separately. With this parameter, all I need to do is to set the parameter value to 0, 1, or 2 and manage one Event Instance. However, there has been a bug where the audio pops when the parameter value is changed while the sound is playing. I was not sure what caused the problem. I have to revisit this bug or have to separate SFXs for each gun.


Week of Mar 31

(Revisited) FMOD discrete parameter bug

After researching this issue, I found that the problem was related to the way FMOD handles discrete parameters. Somehow, it retrigers the sound when the parameter value is changed. According to the solution I found from FMOD community, I need to use nested events to prevent retriggering.

The idea is that I need to put discrete parameters in a nested event. As you can see above, there are two parameters now. One is global parameter (WeaponType) and the other is a nested parameter (WeaponTypeLocal). The key point here is that global parameter should be able to change the value during playback while nested parameter should not be able to change the value during playback.

In the nested parameter, it can track the global parameter value and set the nested parameter value to the global parameter value using automation. This way, I can prevent retriggering the sound when the parameter value is changed.

Camera Transition with Lava

I added a camera transition by chaning camera priority. This was from one of the feedbacks from QA day. With this camera transition, it would provide the player a better visual experience.

SpotLight asset

I requested Youngwoo to add this spotlight asset. I hope this would help to improve the visual experience of our game.

Because spotlight head can be rotated, I can easily change each spotlight head with script over time. I expect chaning lights with script will improve our game experience.

IEnumerator RotateLightHead()
{
    while (true)
    {
        int direction = _rotateLeft ? 1 : -1;

        // First rotation
        yield return RotateByAngle(direction * _rotationAngle);
        // Return to origin
        yield return RotateByAngle(-direction * _rotationAngle);

        // Second rotation (opposite direction)
        yield return RotateByAngle(-direction * _rotationAngle);
        // Return to origin
        yield return RotateByAngle(direction * _rotationAngle);
    }
}

IEnumerator RotateByAngle(float angle)
{
    float startAngle = _lightHead.localRotation.eulerAngles.z;
    if (startAngle > 180f) startAngle -= 360f; // Normalize angle
    float targetAngle = startAngle + angle;
    float elapsedTime = 0f;

    while (elapsedTime < _time)
    {
        float currentAngle = Mathf.Lerp(startAngle, targetAngle, elapsedTime / _time);
        _lightHead.localRotation = Quaternion.Euler(0f, 0f, currentAngle);
        elapsedTime += Time.deltaTime;
        yield return null;
    }

    _lightHead.localRotation = Quaternion.Euler(0f, 0f, targetAngle);
}

I use Coroutine to rotate the light head instead of using Update(), which is called every frame. To rotate the light head, that is not necessary. RotateLightHead() rotates the light head by a certain angle and returns to the original position.


Week of Apr 7

Portal VFS SFX

I added a portal VFX/SFX. I used ParticleSystem to create this effect. There are three different types of VFXs for portal. One is the portal itself with circle texture. Another is inner embers and the last one is the outer embers. I used Light2D to create a light effect for the portal.


void UpdateAppearance()
{
    // Display inactive appearance
    if (_isCoolingDown)
    {
        // Set the material color to red to indicate inactive state
        var innerEmbersMain = _innerEmbers.main;
        innerEmbersMain.startColor = new ParticleSystem.MinMaxGradient(gradientRed);

        var _outterEmbersMain = _outterEmbers.main;
        _outterEmbersMain.startColor = new ParticleSystem.MinMaxGradient(redPortal);

        StartCoroutine(ChangeLightColor(_light2D, _light2D.color, redPortal, 0.5f));
    }
    else
    {
        // Adjust the particle system to reflect inactive state
        var innerEmbersMain = _innerEmbers.main;
        innerEmbersMain.startColor = new ParticleSystem.MinMaxGradient(gradientBlue);

        var _outterEmbersMain = _outterEmbers.main;
        _outterEmbersMain.startColor = new ParticleSystem.MinMaxGradient(bluePortal);

        StartCoroutine(ChangeLightColor(_light2D, _light2D.color, bluePortal, 0.5f));
    }
}

private IEnumerator ChangeLightColor(Light2D light, Color startColor, Color endColor, float duration)
{
    float elapsedTime = 0f;
    while (elapsedTime < duration)
    {
        light.color = Color.Lerp(startColor, endColor, elapsedTime / duration);
        elapsedTime += Time.deltaTime;
        yield return null;
    }
    light.color = endColor;
}

With script, I update the position of the portal and the light effect. Blue light means the portal is ready to be used. When the player enters the portal, the light effect is changed to red and the portal is not ready yet. When the color changes, not only light but also particles change. The SFX for portal is played where player comes out of the portal.

Heal VFS SFX

Previously, healing effect was to flash the whole screen with green. It seemed too much so I decided to come up with a better solution. It was created with ParticleSystem, and I had to recreate the healing SFX mixing different waves to match the healing VFX.

Final touch VFS SFX

Right before the final submission, I made sure I added all the SFXs and VFXs so that our game is ready to be submitted. I wish I had more time to decorate the environment. However, I think it is good enough for the final submission because it has all weapon SFXs and basic VFXs for the environment. Especially, LaserSweep SFX came out really well.