Day 26 of 100 Days of VR: Adding Missing Audio, UI, and Creating Enemy Victory State

Josh Unity VR Development Leave a Comment

Welcome back to day 26! We just finished implementing our Enemy Spawning System, but before we move on to the next thing, I would like to go and fix/update some minor things that we glimpsed over.

Specifically, I like to address these 3 points:

  1. When we get hit, there’s no player hit sound effect
  2. We should fix the crosshair to be something nice
  3. When the player dies, I would like the enemy knights to stop moving and enter their idle state

That means today, we’re going back to the Unity Asset Store!

Step 1: Creating punching sound impacts

The first thing we want to do is play a sound effect for when our knight punches our player.

Luckily for us, we already the Action SFX Vocal Kit asset that we installed from Day 14. Inside the asset pack, we have a variety of options for punching sound effects!

Playing the punch sound effect in EnemyAttack

We only want to play the sound effect to play when our player gets hit, so to do that, we’re going to update EnemyAttack so that when the player takes damage, we can play our sound effect.

Here’s the updated EnemyAttack:

using UnityEngine;


public class EnemyAttack : MonoBehaviour
{
    public FistCollider LeftFist;
    public FistCollider RightFist;
    public AudioClip[] AttackSfxClips;

    private Animator _animator;
    private GameObject _player;
    private AudioSource _audioSource;

    void Awake()
    {
        _player = GameObject.FindGameObjectWithTag("Player");
        _animator = GetComponent<Animator>();
        SetupSound();
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == _player)
        {
            _animator.SetBool("IsNearPlayer", true);
        }
        print("enter trigger with _player");
    }

    void OnTriggerExit(Collider other)
    {
        if (other.gameObject == _player)
        {
            _animator.SetBool("IsNearPlayer", false);
        }
        print("exit trigger with _player");
    }

    private void Attack()
    {
        if (LeftFist.IsCollidingWithPlayer() || RightFist.IsCollidingWithPlayer())
        {
            PlayRandomHit();
            _player.GetComponent<PlayerHealth>().TakeDamage(10);
        }
    }

    private void SetupSound()
    {
        _audioSource = gameObject.AddComponent<AudioSource>();
        _audioSource.volume = 0.2f;
    }

    private void PlayRandomHit()
    {
        int index = Random.Range(0, AttackSfxClips.Length);
        _audioSource.clip = AttackSfxClips[index];
        _audioSource.Play();
    }
}

New Variables added

We added 2 new variables and they are:

  • _audioSource – our sound player
  • AttackSfxClip – an array of punching music sound clips that we’ll later add in.

Walking through the code changes.

Here’s the new addition we added into our code:

  1. In Start() we call SetupSound() which is where we create an AudioSource component in code and then set the volume.
  2. In Attack(), which you might recall is called by an event from our Knight’s attacking animation, we call PlayRandomHit() to play a random punch effect.
  3. In PlayRandomHit(), we get a random index from our array of Sound Clips, we set it to our audio source and then we play it.

Step 2: Setting up the Audio Clips for EnemyAttack

We made a slot in our EnemyAttack script for us to put in Audio Clips, so now we’ll put them in.

Where?

We’ll put in our Knight prefab, which is in the Prefabs folder.

Select our Knight prefab, under the Enemy Attack (Script) component, expand Attack Sfx Clips and set Size to be 3.

Then either drag and drop or open the selector to add Punch_01-Punch_03 into those slots.

Now when we play the game, if we were ever hit by the knight, the punching sound will play. Great!

Step 3: Adding A Better Crosshair

The next thing we want to do today is to add an overall better crosshair UI instead of the white square that we have.

Step 3.1: Getting the crosshair from the asset store

To do that, I went to the Unity Asset Store and I found a free crosshair pack: Simple Modern Crosshairs

Download and import the asset to our game. Everything will be in the SMC Pack 1 in our Assets folder

Step 3.2: Using the crosshairs pack

I’m not going to be picky on my crosshair, so I just chose the first one.

Setting our Crosshair image

Inside our hierarchy, go to HUD > Crosshair and in the Image (Script) component, change Source Image to be one of the crosshairs you like. I chose 1 because it’s the first one.

Changing our crosshair to be red

Right now, it’s White, I decided to change it to Red. Also, it’s hard to see it when it’s so small, so I resized our crosshair. I set the width and height of our image to be 40.

Re-center our crosshair

Finally, after we adjusted the size of our image, we should reposition the crosshair to be in the middle of the screen again.

To do that we click on the Anchor Presets (the box on the top left corner of our Rect Transform component) and hit ctrl + shift in the middle center box.

Now when we’re done, we should have something like this:

Step 4: Stopping Enemies in Game Over

Now we’re on the final and more complicated part of today.

Currently, in the state of our game, when the player loses, the enemies will continue attacking the player and the spawner would continue spawning.

While leaving that as is could be considered a feature, for practice, we’re going to learn how to disable enemy spawning and movement when the game is over.

The first thing we’re going to do is stop the enemies from moving after we win.

Note: this is only when the player loses. If they win, all the enemies are already dead and there’s already no more enemies to spawn.

Step 4.1: Spawn all enemies inside a parent holder in SpawnManager

If we want to disable our enemy knights after the game is over, a simple change we could do inside our EnemyHealth or EnemyMovement script is to check our GameManager to see if the game is over in Update(), over and over and over again…

As you can imagine doing this for ALL our enemies could become computationally expensive. So instead, I think a better solution is to store all enemies in a parent game object and then when we’re done, cycle through all of them.

The best way for us to have this parent container is to create it in our SpawnManager and then push all enemies that we spawn in there.

For our code to work, we need to access the EnemyHealth and EnemyMovmenet script to:

  1. Check if the enemy is still alive
  2. If alive, set them to an idle state and stop them from moving, all of which is controlled in our EnemyMovement script.

Here’s the code, note that it won’t compile yet until we change EnemyMovement:

using System.Collections;
using UnityEngine;

[System.Serializable]
public class Wave
{
    public int EnemiesPerWave;
    public GameObject Enemy;
}

public class SpawnManager : MonoBehaviour
{
    public Wave[] Waves; // class to hold information per wave
    public Transform[] SpawnPoints;
    public float TimeBetweenEnemies = 2f;

    private GameManager _gameManager;

    private int _totalEnemiesInCurrentWave;
    private int _enemiesInWaveLeft;
    private int _spawnedEnemies;

    private int _currentWave;
    private int _totalWaves;

    private GameObject _enemyContainer;

    void Start ()
    {
        _gameManager = GetComponentInParent<GameManager>();

        _currentWave = -1; // avoid off by 1
        _totalWaves = Waves.Length - 1; // adjust, because we're using 0 index

        _enemyContainer = new GameObject("Enemy Container");

        StartNextWave();
    }

    void StartNextWave()
    {
        _currentWave++;
        // win
        if (_currentWave > _totalWaves)
        {
            _gameManager.Victory();
            return;
        }

        _totalEnemiesInCurrentWave = Waves[_currentWave].EnemiesPerWave;
        _enemiesInWaveLeft = 0;
        _spawnedEnemies = 0;

        StartCoroutine(SpawnEnemies());
    }

    // Coroutine to spawn all of our enemies
    IEnumerator SpawnEnemies()
    {
        GameObject enemy = Waves[_currentWave].Enemy;
        while (_spawnedEnemies < _totalEnemiesInCurrentWave)
        {
            _spawnedEnemies++;
            _enemiesInWaveLeft++;

            int spawnPointIndex = Random.Range(0, SpawnPoints.Length);

            // Create an instance of the enemy prefab at the randomly selected spawn point's position and rotation.
            GameObject newEnemy = Instantiate(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation);
            newEnemy.transform.SetParent(_enemyContainer.transform);
            yield return new WaitForSeconds(TimeBetweenEnemies);
        }
        yield return null;
    }
    
    // called by an enemy when they're defeated
    public void EnemyDefeated()
    {
        _enemiesInWaveLeft--;
        // We start the next wave once we have spawned and defeated them all
        if (_enemiesInWaveLeft == 0 && _spawnedEnemies == _totalEnemiesInCurrentWave)
        {
            StartNextWave();
        }
    }

    public void DisableAllEnemies()
    {
        // cycle through all of our enemies
        for (int i = 0; i < _enemyContainer.transform.childCount; i++)
        {
            Transform enemy = _enemyContainer.transform.GetChild(i);
            EnemyHealth health = enemy.GetComponent<EnemyHealth>();
            EnemyMovement movement = enemy.GetComponent<EnemyMovement>();

            // if the enemy is still alive, we want to disable it
            if (health != null && health.Health > 0 && movement != null)
            {
                movement.PlayVictory();
            }
        }
    }
}

New variable used

The only new variable that we used is to a GameObject that we call _enemyContainer.

_enemyContainer, is literally an empty game object that we create that’s sole purpose is to function as a container.

Walking through the code

The complexity of this specific feature isn’t the code itself, it’s changing multiple pieces that intermingle with each other.

Here’s what we need to know about the changes done to SpawnManager:

  1. In Start(), we create a new instance of a GameObject, which will put _enemyContainer in our actual game. It’ll be called “Enemy Container”
  2. We create a new public function called DisableAllEnemies(), in here, we check all child game objects in our _enemyContainer. We make sure they all have our EnemyHealth and EnemyMovement If they all do, we’ll call the currently non-existent PlayVictory().

Once again, currently our code does not compile, we need to add PlayVictory() to our EnemyMovement script.

Step 4.2: Creating PlayVictory() in EnemyMovement

In SpawnManager, we’re essentially disabling all enemy movements after the game has ended. To do that we’re putting that logic in a function that we’ll call PlayVictory()

Here are the changes that we made to EnemyMovement:

using UnityEngine;
using UnityEngine.AI;

public class EnemyMovement : MonoBehaviour
{
    public float KnockBackForce = 1.1f;
    public AudioClip[] WalkingClips;
    public float WalkingDelay = 0.4f;

    private NavMeshAgent _nav;
    private Transform _player;
    private EnemyHealth _enemyHealth;
    private AudioSource _walkingAudioSource;
    private Animator _animator;
    private float _time;

    void Start ()
    {
        _nav = GetComponent<NavMeshAgent>();
        _player = GameObject.FindGameObjectWithTag("Player").transform;
        _enemyHealth = GetComponent<EnemyHealth>();
        SetupSound();
        _time = 0f;
        _animator = GetComponent<Animator>();
    }
    
    void Update ()
    {
        _time += Time.deltaTime;
        if (_enemyHealth.Health > 0 && _animator.GetCurrentAnimatorStateInfo(0).IsName("Run"))
        { 
            _nav.SetDestination(_player.position);
            if (_time > WalkingDelay)
            {
                PlayRandomFootstep();
                _time = 0f;
            }
        }
        else
        {
            _nav.enabled = false;
        }
    }

    private void SetupSound()
    {
        _walkingAudioSource = gameObject.AddComponent<AudioSource>();
        _walkingAudioSource.volume = 0.2f;
    }

    private void PlayRandomFootstep()
    {
        int index = Random.Range(0, WalkingClips.Length);
        _walkingAudioSource.clip = WalkingClips[index];
        _walkingAudioSource.Play();
    }

    public void KnockBack()
    {
        _nav.velocity = -transform.forward * KnockBackForce;
    }

    // plays our enemy's default victory state 
    public void PlayVictory()
    {
        _animator.SetTrigger("Idle");
    }
}

Walking through the code

For possibly the first time in a long time, we aren’t introducing new variables, we just have new code:

  1. We implemented PlayVictory() that our SpawnManager will call. It’s pretty basic, we set our state to be idle.
  2. In Update() I’ve moved the animation state check to the outer if statement. The reason is that the moment we change our state, we’ll disable our Nav Mesh Agent so our enemy won’t move anymore.

Step 4.3 Updating GameManager to use DisableAllEnemies() from the SpawnManager

Now we have everything setup, the last and final thing that we need to do is to set our GameMangager script to use our new SpawnManager.

Here’s the code for that:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public Animator GameOverAnimator;
    public Animator VictoryAnimator;

    private GameObject _player;
    private SpawnManager _spawnManager;

    void Start()
    {
        _player = GameObject.FindGameObjectWithTag("Player");
        _spawnManager = GetComponentInChildren<SpawnManager>();
    }

    public void GameOver()
    {
        GameOverAnimator.SetBool("IsGameOver", true);
        DisableGame();
        _spawnManager.DisableAllEnemies();
    }

    public void Victory()
    {
        VictoryAnimator.SetBool("IsGameOver", true);
        DisableGame();
    }

    private void DisableGame()
    {
        _player.GetComponent<PlayerController>().enabled = false;
        _player.GetComponentInChildren<MouseCameraContoller>().enabled = false;
        _player.GetComponentInChildren<PlayerShootingController>().enabled = false;
        Cursor.lockState = CursorLockMode.None;
    }
}

New variables

We add _spawnManager so that we can stop all enemies when they win.

Walking through the code

The changes here is simple:

  1. In Start(), we grab our SpawnManager script, nothing new or surprising here (remember that our SpawnManager is a child of GameManager)
  2. In GameOver() we use our _spawnManager to disable all the enemies.

Now if we play the game, you should see our enemies enter their idle state (and stop punching our poor body).

Yay!

Step 5: Stop Enemy Spawing On Death

We’ve stopped the enemy from moving, the next thing we need to do now is to disable the spawner from spawning any more enemies when the game is over.

Luckily for us, in our SpawnManager we already have code that runs at exactly when the game is over: DisableAllEnemies()

Here’s what our code looks like in SpawnManager:

using System.Collections;
using UnityEngine;

[System.Serializable]
public class Wave
{
    public int EnemiesPerWave;
    public GameObject Enemy;
}

public class SpawnManager : MonoBehaviour
{
    public Wave[] Waves; // class to hold information per wave
    public Transform[] SpawnPoints;
    public float TimeBetweenEnemies = 2f;

    private GameManager _gameManager;

    private int _totalEnemiesInCurrentWave;
    private int _enemiesInWaveLeft;
    private int _spawnedEnemies;

    private int _currentWave;
    private int _totalWaves;

    private GameObject _enemyContainer;
    private bool _isSpawning;

    void Start ()
    {
        _gameManager = GetComponentInParent<GameManager>();

        _currentWave = -1; // avoid off by 1
        _totalWaves = Waves.Length - 1; // adjust, because we're using 0 index

        _enemyContainer = new GameObject("Enemy Container");
        _isSpawning = true;
        StartNextWave();
    }

    void StartNextWave()
    {
        _currentWave++;
        // win
        if (_currentWave > _totalWaves)
        {
            _gameManager.Victory();
            return;
        }

        _totalEnemiesInCurrentWave = Waves[_currentWave].EnemiesPerWave;
        _enemiesInWaveLeft = 0;
        _spawnedEnemies = 0;

        StartCoroutine(SpawnEnemies());
    }

    // Coroutine to spawn all of our enemies
    IEnumerator SpawnEnemies()
    {
        GameObject enemy = Waves[_currentWave].Enemy;
        while (_spawnedEnemies < _totalEnemiesInCurrentWave)
        {
            _spawnedEnemies++;
            _enemiesInWaveLeft++;

            int spawnPointIndex = Random.Range(0, SpawnPoints.Length);

            if (_isSpawning)
            {
                // Create an instance of the enemy prefab at the randomly selected spawn point's position and rotation.
                GameObject newEnemy = Instantiate(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation);
                newEnemy.transform.SetParent(_enemyContainer.transform);
            }
            yield return new WaitForSeconds(TimeBetweenEnemies);
        }
        yield return null;
    }
    
    // called by an enemy when they're defeated
    public void EnemyDefeated()
    {
        _enemiesInWaveLeft--;
        // We start the next wave once we have spawned and defeated them all
        if (_enemiesInWaveLeft == 0 && _spawnedEnemies == _totalEnemiesInCurrentWave)
        {
            StartNextWave();
        }
    }

    public void DisableAllEnemies()
    {
        _isSpawning = false;
        // cycle through all of our enemies
        for (int i = 0; i < _enemyContainer.transform.childCount; i++)
        {
            Transform enemy = _enemyContainer.transform.GetChild(i);
            EnemyHealth health = enemy.GetComponent<EnemyHealth>();
            EnemyMovement movement = enemy.GetComponent<EnemyMovement>();

            // if the enemy is still alive, we want to disable it
            if (health != null && health.Health > 0 && movement != null)
            {
                movement.PlayVictory();
            }
        }
    }
}

New variable

I introduced 1 new variable, a boolean _isSpawning which we’ll use in our code to stop our enemy from spawning.

Walking through the code

Here’s another update with some minor changes:

  1. In Start(), we instantiate _isSpawning to be true.
  2. In SpawnEnemies() we add an if statement check to see if we’re spawning, if we are, we’ll spawn an enemy, if not, then we don’t do anything.
  3. Inside DisableAllEnemies(), which is called by SpawnManager when the player’s health drops to 0, we set _isSpawning to false so we’ll stop spawning enemies in SpawnEnemies().

Conclusion

Phew, that was a long drawn out day today! We accomplished a lot today!

We added an enemy punch sound, we fixed our crosshair UI, and then we went and did a large change that stops enemies from moving when they win.

Tomorrow, we’re going to start creating new enemies that we can add into our SpawnManager so we can get some variety in the game!

Until then, I’m off to bed!

Day 25 | 100 Days of VR | Day 27

Home

Subscribe To Our Weekly Newsletter!
Like these coding articles? Join my mailing list for the latest updates and influence the code that I write!
We hate spam. Your email address will not be sold or shared with anyone else.

Leave a Reply

Your email address will not be published. Required fields are marked *