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

  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

Step 1: Creating punching sound impacts

Playing the punch sound effect in 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

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

Walking through the code changes.

  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

Step 3: Adding A Better Crosshair

Step 3.1: Getting the crosshair from the asset store

Step 3.2: Using the crosshairs pack

Setting our Crosshair image

Changing our crosshair to be red

Re-center our crosshair

Step 4: Stopping Enemies in Game Over

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

  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.
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

Walking through the code

  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().

Step 4.2: Creating PlayVictory() in 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

  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

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

Walking through the code

  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.

Step 5: Stop Enemy Spawing On Death

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

Walking through the code

  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

--

--

--

Software Engineer by day, side hustler wannabee by night! https://leetdev.io

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Running XCTests from the Command Line

Ditching tables out of some email design

CSS — The Styling Language.

ER-To-Relational Mapping

Explore the Activity Life Cycle

How to build product: Let’s start with the business

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Josh Chang

Josh Chang

Software Engineer by day, side hustler wannabee by night! https://leetdev.io

More from Medium

Guide to Making a Beautiful Game: Altering Texture Maps

Where’d He Goooooo…

My journey becoming a Unity game developer: Make a game look beautiful-Putting up the walls to make…

3D Survival FPS Game