Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
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 such as: audio, ui, and enemy victory state,
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:
When we get hit, there’s no player hit sound effect
We should fix the crosshair to be something nice
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!
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!
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();
}
}
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.
Here’s the new addition we added into our code:
In Start() we call SetupSound() which is where we create an AudioSource component in code and then set the volume.
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.
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.
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_03into those slots.
Now when we play the game, if we were ever hit by the knight, the punching sound will play. Great!
The next thing we want to do today is to add an overall better crosshair UI instead of the white square that we have.
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
I’m not going to be picky on my crosshair, so I just chose the first one.
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.
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.
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:
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.
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:
Check if the enemy is still alive
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();
}
}
}
}
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.
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:
In Start(), we create a new instance of a GameObject, which will put _enemyContainer in our actual game. It’ll be called “Enemy Container”
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.
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");
}
}
For possibly the first time in a long time, we aren’t introducing new variables, we just have new code:
We implemented PlayVictory() that our SpawnManager will call. It’s pretty basic, we set our state to be idle.
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.
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;
}
}
We add _spawnManager so that we can stop all enemies when they win.
The changes here is simple:
In Start(), we grab our SpawnManager script, nothing new or surprising here (remember that our SpawnManager is a child of GameManager)
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!
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();
}
}
}
}
I introduced 1 new variable, a boolean _isSpawning which we’ll use in our code to stop our enemy from spawning.
Here’s another update with some minor changes:
In Start(), we instantiate _isSpawning to be true.
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.
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().
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!
Source: Day 26
Visit the 100 Days of Unity VR Development main page.
Visit our Homepage
You May Also Like