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 would 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!
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 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:
- 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.
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 EnemyMovement
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;
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;
_totalWaves = Waves.Length - 1;
_enemyContainer = new GameObject("Enemy Container");
StartNextWave();
}
void StartNextWave()
{
_currentWave++;
if (_currentWave > _totalWaves)
{
_gameManager.Victory();
return;
}
_totalEnemiesInCurrentWave = Waves[_currentWave].EnemiesPerWave;
_enemiesInWaveLeft = 0;
_spawnedEnemies = 0;
StartCoroutine(SpawnEnemies());
}
IEnumerator SpawnEnemies()
{
GameObject enemy = Waves[_currentWave].Enemy;
while (_spawnedEnemies < _totalEnemiesInCurrentWave)
{
_spawnedEnemies++;
_enemiesInWaveLeft++;
int spawnPointIndex = Random.Range(0, SpawnPoints.Length);
GameObject newEnemy = Instantiate
(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation);
newEnemy.transform.SetParent(_enemyContainer.transform);
yield return new WaitForSeconds(TimeBetweenEnemies);
}
yield return null;
}
public void EnemyDefeated()
{
_enemiesInWaveLeft--;
if (_enemiesInWaveLeft == 0 && _spawnedEnemies == _totalEnemiesInCurrentWave)
{
StartNextWave();
}
}
public void DisableAllEnemies()
{
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 (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 whose 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
:
- 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.
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;
}
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:
- 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.
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 are 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!
Step 5: Stop Enemy Spawning 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 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;
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;
_totalWaves = Waves.Length - 1;
_enemyContainer = new GameObject("Enemy Container");
_isSpawning = true;
StartNextWave();
}
void StartNextWave()
{
_currentWave++;
if (_currentWave > _totalWaves)
{
_gameManager.Victory();
return;
}
_totalEnemiesInCurrentWave = Waves[_currentWave].EnemiesPerWave;
_enemiesInWaveLeft = 0;
_spawnedEnemies = 0;
StartCoroutine(SpawnEnemies());
}
IEnumerator SpawnEnemies()
{
GameObject enemy = Waves[_currentWave].Enemy;
while (_spawnedEnemies < _totalEnemiesInCurrentWave)
{
_spawnedEnemies++;
_enemiesInWaveLeft++;
int spawnPointIndex = Random.Range(0, SpawnPoints.Length);
if (_isSpawning)
{
GameObject newEnemy = Instantiate
(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation);
newEnemy.transform.SetParent(_enemyContainer.transform);
}
yield return new WaitForSeconds(TimeBetweenEnemies);
}
yield return null;
}
public void EnemyDefeated()
{
_enemiesInWaveLeft--;
if (_enemiesInWaveLeft == 0 && _spawnedEnemies == _totalEnemiesInCurrentWave)
{
StartNextWave();
}
}
public void DisableAllEnemies()
{
_isSpawning = false;
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 (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:
- 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()
.
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
CodeProject
The post Day 26 of 100 Days of VR: Adding Missing Audio, UI, and Creating Enemy Victory State appeared first on Coding Chronicles.