Jump to content
  • Advertisement
  • entries
    44
  • comments
    19
  • views
    2425

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

Josh Chang

909 views

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_03into those slots.

 
0*qK39QK6EYQEXuZDD.png

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

 
0*YxIFYz1UmobqEA7j.png

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:

 
0*suXQO72XtENl8clg.png

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

 
0*GO4E4nzuBk60Fein.png

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!

Source: Day 26

Visit the 100 Days of Unity VR Development main page.

Visit our Homepage



0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • Blog Entries

  • Similar Content

    • By LonelyStep
      PROGRESS
      Thought I'd reach out for some feedback on my little project. This is an android two-player game for top-down boxing matches. The core mechanics are all that I have accomplished thus far, later I want to add different configurations for your boxer (special effects, increased health or damage, etc) and maybe even a single player AI to practice against.

      SCREENSHOTS
      #1

      *****************************************************************

      #2
       
      FEEDBACK
      Right now the gameplay feedback I'm looking for is on the fundamental mechanics of the game.

      One area of particular concern is the window of opportunity for a player to block after their opponent has begun a punch. Currently that window is one-tenth of a second, otherwise you will be to late to counter their blow. I want it to be difficult to successfully pull off a block and have it be something that requires real attention to accomplish. I'm wondering though if it isn't too difficult right now, which may encourage mindless button mashing, which is certainly not what I want to condone.

      Another consideration is whether having three main actions (block, attack, charge) provides a good balance. My idea with having three main areas is that your two thumbs will have to leave one section uncovered at all times. This should result in hand movements that telegraph actions to the opponent.

      Aside from these, any other constructive criticism is very welcome.

      LINK
      My game can be found at the Google Play Store here:
      https://play.google.com/apps/testing/com.meatandgrain.PunchABunch

      NOTE There is a known graphics error when performing a special punch. There are no doubt other unknown errors as well.

        Thank you for your time, I hope you'll be able to find some fun in this little game!
    • By Vandallord
      Link: https://www.kongregate.com/games/Vandallord/idle-stone-history

      A wicked witch turned the main hero into the stone, you need to find a way to break the spell.
      INSTRUCTIONS
      Click on the objects and they will open.
      Click on the stone and its skill to attack the animals will grow as well as a the losses from clicking on animals.
      Click on the river to improve the function to manage time and clicks on the animals will be more precise.
      Click on the tree and it will give you things from animals and the level of luck will grow which give you an opportunity to Harder hits.
      The higher the level of monsters the more animal can visit the stone and give presents which are essential to make a portion.
      Screenshots:
       
    • By Znippy
      Hello everyone!  
      This is my submission for the Frogger challenge.
      The final build for the project can be found here!
      I hope I have fulfilled all requirements for this challenge. Sadly, I do not have time to create a gameplay video. I am not sure if this is a must. I have added a couple of screenshots from my blog series.
      My post-mortem post will be done next week. 
      As I already mentioned on the project page, I also could offer a Linux build if somebody needs one!
      I hope you like it and I am excited to see your high scores!
      Please tell me if there is anything missing!
    • By horror_man
      Hello, I'm currently searching for a talented and passionate programmer to create a small but great horror game that would take around 3 months to be done.
       
      About the game: The game would be a sci-fi/post-apocalyptic survival horror 3D game with FPS (First person shooter) mechanics and an original setting and story based in a book (which I'm writing) scene, where a group of prisoners are left behind in an abandoned underground facility. It would play similar to Dead Space combined with Penumbra and SCP: Secret Laboratory, with the option of playing solo or multiplayer.
       
      Engine that'd be used to create the game: Unity
       
      About me: I'm a music composer with 4 years of experience and I'm fairly new in this game development world, and I'm currently leading the team that'd be creating this beautiful and horrifying game. I decided that making the book which I'm writing into a game would be really cool, and I got more motivated about doing so some time ago when I got a bunch of expensive Unity assets for a very low price. However, I researched about how to do things right in game development so I reduced the scope of it as much as I could so that's why this game is really based in a scene of the book and not the entire thing (and also that's why it would take 3 months). Also I'm currently learning how to use Unity and how to model things with Blender.
       
      Our team right now consists of: Me (Game Designer, Creator, Music Composer, Writer), 3 3D Modelers, 1 Sound Effect Designer, 1 Concept Artist and 1 Programmer.
       
      Who am I looking for:
      - A programmer that's experienced in C# and with Unity.
       
      Right now the game is very early in its development (GDD is completed and all 3D Items, Music and Sound Effects are completed).
       
      If you are interested in joining, contributing or have questions about the project then let's talk. You can message me in Discord: world_creator#9524
    • By Nilmani Gautam
      Welcome every one from this section we are going to develop a new 3D game Cube Race
       
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!