Josh Chang

Members
  • Content count

    42
  • Joined

  • Last visited

Community Reputation

9 Neutral

About Josh Chang

  • Rank
    Member

Personal Information

  1. Welcome back to day 31! Yesterday, we created a simple time-based system that runs on our screen when the game plays. We only increased the time, but we never pause it when the game is over. Today, we’re going to: Pause our time after we win or lose Stop our player’s shooting animation and sound effects when the game is over Today’s going to be a relatively short day, but let’s get to it, shall we? Step 1: Stopping the Timer at Game Over To stop our game from showing updates to the timer, we need to have 2 things: Code to stop our ScoreManager from updating the string Some way for our ScoreManager to know that the game is over. Who’s in charge of that? Our GameManager! Step 1.1: Stopping ScoreManager from Updating Code The first thing we need to do is to get our ScoreManager to stop updating the time when the game is over. Here are the changes we did: using UnityEngine; using UnityEngine.UI; public class ScoreManager : MonoBehaviour { public Text Score; private string _time; private bool _gameOver; void Start () { _time = ""; _gameOver = false; } void Update() { if (!_gameOver) { UpdateTime(); } } private void UpdateTime() { int minutes = Mathf.FloorToInt(Time.time / 60); int seconds = Mathf.FloorToInt(Time.time % 60); float miliseconds = Time.time * 100; miliseconds = miliseconds % 100; _time = string.Format("{0:0}:{1:00}:{2:00}", minutes, seconds, miliseconds); Score.text = _time; } public void GameOver() { _gameOver = true; } } New Variable Used The goal is for us to stop the call to UpdateTime() to do that, we introduced a new bool called _gameOver to indicate that. Walking Through the Code The change that we’re making here is straightforward: In Start() we set instantiate _gameOver In Update() we update our time if we’re still playing otherwise we don’t do anything We created a public GameOver() so that something else (our GameManager) can tell us that the game is over and we can stop updating our score Step 1.2: Updating our GameManager to call GameOver() in ScoreManager Now that we have a way to stop our timer from going, the next thing we’re going to have to do is call it. Luckily for us, we have a GameManager script that takes care of everything regarding the state of the game. Let’s add our code there! Here are our changes: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; public Animator VictoryAnimator; private GameObject _player; private SpawnManager _spawnManager; private ScoreManager _scoreManager; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); _spawnManager = GetComponentInChildren<SpawnManager>(); _scoreManager = GetComponent<ScoreManager>(); } 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; _scoreManager.GameOver(); } } New Variable Used Like what we said earlier, we introduced our ScoreManager object, _scoreManager so we can call GameOver(). Walking Through the Code There aren’t any major or surprising changes that were done: In Start() we got our _scoreManager that was attached to our GameManager game object In DisableGame(), where we go and disable everything, we call GameOver() from our _scoreManager to pause the timer. Step 2: Fixing the Player State After the Game Ends The next thing we’re going to do is fix a problem that I’ve been talking about for the past 2 days or so. When we win, our player, specifically, the gun, is stuck in a perpetual shooting state and the shooting sound effect will keep playing until the game is over. We’re going to fix that. Most of the code is already in place, we just need to: Create a new state transition to stop shooting in our gun Animator Create a new game over function for our gun to stop everything when the game is over Call the game over function from our GameManager Step 2.1: Create Transition to Stop Shooting If you might recall from Day 16 where we added our weapon, we created a state machine that would transition from our shooting animation to our idle information (and vice-versa) whenever we activate our Shoot Trigger We’re going to make a new GameOver state and create a new transition from Any State to GameOver. Here’s what we’re going to do: Select Player > Main Camera > MachineGun_00 and open the Animator tab (Windows > Animator) Create a new parameter, it’ll be a trigger and call it GameOver Create a new state called GameOver Create a new transition from Any State to Select that transition and set the transition condition to be when GameOver is triggered When you’re done, we should have something like this: At this point, you might be wondering, if we don’t have any clip for our GameOver state, why not just re-use our default state? The answer to that is, because this is our finished state where we’re done. We don’t want to accidentally trigger a new transition and move us from the default state to another state. Step 2.2: Add GameOver in PlayerShootingController Now that we have our new transition, it’s time to use it! As we might recall, the script that controls our player’s shooting is PlayerShootingController. We’re going to go in and make some slight changes: using UnityEngine; using System.Collections; public class PlayerShootingController : MonoBehaviour { public float Range = 100; public float ShootingDelay = 0.1f; public AudioClip ShotSfxClips; public Transform GunEndPoint; public float MaxAmmo = 10f; private Camera _camera; private ParticleSystem _particle; private LayerMask _shootableMask; private float _timer; private AudioSource _audioSource; private Animator _animator; private bool _isShooting; private bool _isReloading; private LineRenderer _lineRenderer; private float _currentAmmo; private ScreenManager _screenManager; void Start () { _camera = Camera.main; _particle = GetComponentInChildren<ParticleSystem>(); Cursor.lockState = CursorLockMode.Locked; _shootableMask = LayerMask.GetMask("Shootable"); _timer = 0; SetupSound(); _animator = GetComponent<Animator>(); _isShooting = false; _isReloading = false; _lineRenderer = GetComponent<LineRenderer>(); _currentAmmo = MaxAmmo; _screenManager = GameObject.FindWithTag("ScreenManager").GetComponent<ScreenManager>(); } void Update () { _timer += Time.deltaTime; // Create a vector at the center of our camera's viewport Vector3 lineOrigin = _camera.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, 0.0f)); // Draw a line in the Scene View from the point lineOrigin in the direction of fpsCam.transform.forward * weaponRange, using the color green Debug.DrawRay(lineOrigin, _camera.transform.forward * Range, Color.green); if (Input.GetMouseButton(0) && _timer >= ShootingDelay && !_isReloading && _currentAmmo > 0) { Shoot(); if (!_isShooting) { TriggerShootingAnimation(); } } else if (!Input.GetMouseButton(0) || _currentAmmo <= 0) { StopShooting(); if (_isShooting) { TriggerShootingAnimation(); } } if (Input.GetKeyDown(KeyCode.R)) { StartReloading(); } } private void StartReloading() { _animator.SetTrigger("DoReload"); StopShooting(); _isShooting = false; _isReloading = true; } private void TriggerShootingAnimation() { _isShooting = !_isShooting; _animator.SetTrigger("Shoot"); } private void StopShooting() { _audioSource.Stop(); _particle.Stop(); } private void Shoot() { _timer = 0; Ray ray = _camera.ScreenPointToRay(Input.mousePosition); RaycastHit hit = new RaycastHit(); _audioSource.Play(); _particle.Play(); _currentAmmo--; _screenManager.UpdateAmmoText(_currentAmmo, MaxAmmo); _lineRenderer.SetPosition(0, GunEndPoint.position); StartCoroutine(FireLine()); if (Physics.Raycast(ray, out hit, Range, _shootableMask)) { print("hit " + hit.collider.gameObject); _lineRenderer.SetPosition(1, hit.point); EnemyHealth health = hit.collider.GetComponent<EnemyHealth>(); EnemyMovement enemyMovement = hit.collider.GetComponent<EnemyMovement>(); print(health); if (enemyMovement != null) { enemyMovement.KnockBack(); } if (health != null) { health.TakeDamage(1); } } else { _lineRenderer.SetPosition(1, ray.GetPoint(Range)); } } private IEnumerator FireLine() { _lineRenderer.enabled = true; yield return ShootingDelay - 0.05f; _lineRenderer.enabled = false; } // called from the animation finished public void ReloadFinish() { _isReloading = false; _currentAmmo = MaxAmmo; _screenManager.UpdateAmmoText(_currentAmmo, MaxAmmo); } private void SetupSound() { _audioSource = gameObject.AddComponent<AudioSource>(); _audioSource.volume = 0.2f; _audioSource.clip = ShotSfxClips; } public void GameOver() { _animator.SetTrigger("GameOver"); StopShooting(); } } New Variable Used None Walking Through the Code For our code change this time, we only added a new public function: GameOver(). We do 2 things: We call our GameOver trigger to stop our shooting state We re-use StopShooting() to stop the particle and sound effects. GameOver() is a public function so that means it can be called from somewhere external, in this case, our GameManager script. Step 2.3: Calling GameOver() from PlayerShootingController in GameManager Now that we have the code to stop our player from shooting when the game is over, it’s time to use it in our GameManager. Here are our changes: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; public Animator VictoryAnimator; private GameObject _player; private SpawnManager _spawnManager; private ScoreManager _scoreManager; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); _spawnManager = GetComponentInChildren<SpawnManager>(); _scoreManager = GetComponent<ScoreManager>(); } 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; PlayerShootingController shootingController = _player.GetComponentInChildren<PlayerShootingController>(); shootingController.GameOver(); shootingController.enabled = false; Cursor.lockState = CursorLockMode.None; _scoreManager.GameOver(); } } New Variables Used None Walking Through the Code Everything is already in place, the only thing we want to change is inside DisableGame(). Specifically, instead of just disabling our PlayerShootingController that we already had, we made sure that we called GameOver() and then we disable it to prevent any new user inputs/ Conclusion With all of this in place, our game is 1 step closer to completion! Today we made it so that we would stop counting our time when the game is over and we solved an annoying bug that would have the player constantly firing when the game is over. That’s it for day 31! I think for day 32, we’ll look into creating a high score system and displaying that in our game! Look forward to it! Source: Day 31 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  2. Welcome back to day 30! Another milestone has been made today, we’re 3/10 of the way to the goal! The question is, will we finish our simple first fps and then ship another simple game by the time we hit day 100? However, let’s not look too much into the future, quite yet and finish our simple game. Today, we’re going to implement a new feature into our game. In the current state of the game, after we win, that’s it, no replay value. To fix this, I’m going to implement a time score system to encourage players to beat the game as fast as they can and then keep playing to try and beat their previous time. Our goals today are to: Create the code for our time system Create the UI that shows the time Without any delays, let’s get started! Step 1: Writing the Code for a Time System We’re going to create a point system that will keep track of our time as the game progressive. Luckily for us, Unity already has a very handy time function that takes care of this. It’s Time.time. This nifty little function keeps track of the time elapsed since the game started in seconds. All we need to do is write code to convert our time to seconds and minutes. Let’s get to it! Step 1.1: Creating the ScoreManager To manage our code, we’re going to create a ScoreManager script. We’re going to attach it to our GameManager. To do that, select our GameManager and then click Add Component in the Inspector and create a new ScoreManager script. Our ScoreManager class will be used to show our time formatted in minutes, seconds, and miliseconds. Here’s our code: using System.Collections; using System.Collections.Generic; using UnityEngine; public class ScoreManager : MonoBehaviour { private string _time; void Start () { _time = ""; } void Update () { UpdateTime(); } private void UpdateTime() { int minutes = Mathf.FloorToInt(Time.time / 60); int seconds = Mathf.FloorToInt(Time.time % 60); float miliseconds = Time.time * 100; miliseconds = miliseconds % 100; _time = string.Format("{0:0}:{1:00}:{2:00}", minutes, seconds, miliseconds); print(_time); } } New Variable Used All we’re currently introducing in our code is a string _time that keeps track of the time that took place once the game started. Walking through the code The code is short right now, but here’s what happens. In Start() we instantiate _time to be empty In Update() we call a function UpdateTime() to get our correctly formatted time string. In UpdateTime(), we calculate what our minutes, seconds, and miliseconds are, and then we use String.format to build the string we want. If you aren’t familiar with Format, we pass in a string for where we want variables to replace, and then we put the variables we want to do the replacing afterwards. In this example: {1:00} refers to seconds and we want to always show the string in the format with 2 numbers. For example: 01 or 02 If we were to play the game now and look at the console you can see that we’re printing out the time. Step 2: Creating the UI to show our Score Now that we have the code to create the string for our score, the next thing we’re going to do is to show the time on the player’s score. Step 2.1: Create a new Score Text In our hierarchy, under HUD, create a new UI Text game object. Let’s call it Score. Let’s make some changes to it. Alignment Anchor: Top and Center Text: Time Font Style: Bold Font Size: 24 Alignment: Center and Middle Color: White We should have something like this: We should have this on our scene now: Step 2.2: Using our new Score UI Now that we have our Score UI, it’s time to use it in our script. Back into ScoreManager, we’re going to make some changes…. using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ScoreManager : MonoBehaviour { public Text Score; private string _time; void Start () { _time = ""; } void Update () { UpdateTime(); } private void UpdateTime() { int minutes = Mathf.FloorToInt(Time.time / 60); int seconds = Mathf.FloorToInt(Time.time % 60); float miliseconds = Time.time * 100; miliseconds = miliseconds % 100; _time = string.Format("{0:0}:{1:00}:{2:00}", minutes, seconds, miliseconds); Score.text = _time; } } New Variables Used We added a new Text variable that we call Score. This is the new UI that we just created to show the score in the top center of our page. Note that we need to import UnityEngine.UI to be able to use a Text object. Walking through the code The changes have been simple: In UpdateTime() we set the text of our Score Text UI to be the time. Note: I’m a fond supporter of writing code that sends push notification for changes, however in this, case, because time is always changing, we have no choice but to use Update() every frame to get our new time. Step 2.3: Attach our Score UI to our ScoreManager script Finally, we need to add our new Score UI to our ScoreManager so we can update the time. Just drag the UI into the Score slot in the ScoreManager. When we’re done, we should have this: And when we play the game we’ll have our time increasing: Nice! Conclusion That’ll be all for today! We did some great work today laying the foundation to our score system. Right now we created a time system on the top of our screen so that the player will know how long it’s taking them to beat the enemies. However, if we were to lose or win, you’ll notice that our time will continue to keep running. We will address these issues tomorrow by making some changes in our victory and game over game states, but until then, good night, and I’ll see you all later on Day 31! Source: Day 30 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  3. That's really good to know. Thank you as always for your knowledge. It's always a good feeling knowing that there's someone who knows more, such as yourself offering me better ways of implementing things!
  4. Welcome back to day 29! Yesterday we finished creating our final enemy that we’re going to use for the game. Now that we have everything ready, it’s time to use them! After finishing the spawn system and played the game for a bit, I noticed a small problem. Sometimes when we shoot, the enemy doesn’t get hit. This becomes obvious when we try to switch the health of our enemy to be 1. Our goals today are: Start spawning all 3 of our enemies Fix the shooting bug Without any delays, let’s get started! Step 1: Spawn our new enemies Setting up the spawning system with our new enemies is simple, we already have the SpawnManager script to take care of spawning enemies every wave. Let’s add them in! In our hierarchy, look at GameManager > SpawnManager and to find our SpawnManager script. Set our Waves size to be 3. We want to spawn: 5 Knights at Wave 1 5 Bandits at Wave 2 5 Zombie at Wave 3 When we’re done, we’ll have something like this: Now we can play the game and we’ll be able to fight all of our new enemies on the map! Here might be something we’ll see! You might notice that after we win, our gun doesn’t stop shooting and our shooting sound doesn’t stop. Not today, but some other day, we will go in and disable all of those when the player wins. Step 2: Solving the Mystery Missing Shot Now that we’re spawning enemies and playing the game, we might encounter a very weird problem. When we’re shooting an enemy, sometimes they don’t get hit. I’m not talking about a long distance either, I’m talking very close proximity. What’s happening? After digging around for a bit, I found the problem: our raycast is still hitting the mesh collider of our dead enemies. As we have discussed in Day 20 the Mesh Colliders that we attach to our enemy doesn’t follow the form of our enemies. They’re just still there, as you can see from the image above. What’s happening here is that even though we defeated our enemies, their mesh collider is blocking our shots from hitting other existing enemies. But it’s okay, we can fix this. We must disable all colliders attached to the enemy when they die. Step 2.1: Disabling our Colliders in Code Now, let’s think, where would we know when the enemy is dead? In the EnemyAttack script! In EnemyAttack we have a function called Death() that takes care of everything when the enemy dies. Let’s use that: using UnityEngine; public class EnemyHealth: MonoBehaviour { public float Health = 100; public AudioClip[] HitSfxClips; public float HitSoundDelay = 0.5f; private SpawnManager _spawnManager; private Animator _animator; private AudioSource _audioSource; private float _hitTime; void Start() { _spawnManager = GameObject.FindGameObjectWithTag("SpawnManager").GetComponent<SpawnManager>(); _animator = GetComponent<Animator>(); _hitTime = 0f; SetupSound(); } void Update() { _hitTime += Time.deltaTime; } public void TakeDamage(float damage) { if (Health <= 0) { return; } Health -= damage; if (_hitTime > HitSoundDelay) { PlayRandomHit(); _hitTime = 0; } if (Health <= 0) { Death(); } } private void SetupSound() { _audioSource = gameObject.AddComponent<AudioSource>(); _audioSource.volume = 0.2f; } private void PlayRandomHit() { int index = Random.Range(0, HitSfxClips.Length); _audioSource.clip = HitSfxClips[index]; _audioSource.Play(); } private void Death() { _animator.SetTrigger("Death"); _spawnManager.EnemyDefeated(); foreach (Collider collider in GetComponentsInChildren<Collider>()) { collider.enabled = false; } } } New Variables Used None Walking Through the Code There’s only one major change that we did and that’s in Death() where search through all of the enemy’s child game objects looking for any collider component. If a collider is found, we would just disable it. One interesting thing to note is that we’re looking for anything of type Collider. We’re more used to finding something of exact type, for example, GetComponent<SpawnManager>(); However, taking advantage of the fact that all of the Colliders: Box Collider, Mesh Collider, Capsule Collider, etc all extend from the Collider parent class, they’re all considered of type Collider. What that means is that we can do what we did in Death() and look for anything that is of type Collider or inherits the class. Now after we added the code, if we play the game, we’ll see that dead enemies no longer have their Colliders on anymore: There are things that are sometimes better off staying dead. Conclusion That’s it for today! Today, we incorporated the rest of our enemies to the game and we even went in to fix “ghost” mesh colliders that prevented us from shooting properly. I think we’re going to work on one more feature before I start moving on to the actual VR portion of this series, and that’s making a point system! In our current game, we win and that’s it. There’s no replay value at all! Tomorrow we’re going to start making this game more interesting by adding a scoring system! With that all being said, it’s getting late, I’m headed out! Source: Day 29 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  5. Welcome back to day 28! Yesterday, we started our next task on creating multiple new types of enemies and made a new bandit enemy. Today, we’re going to create our last enemy, a slower enemy with more health that won’t get pushed back as far. Today is going to be very similar to yesterday, however we’re going to try and create a new animator controller for our enemy. We’ve seen this before in the Survival Shooter, which I once again blissfully ignored, however we’re re-visiting this topic today. Let’s get started! Step 1: Acquire new asset Let’s return to our favorite place in the world! The candy store Unity Asset Store! Just like yesterday, we’re going to look for an asset that’s mecanim ready. If you’re not aware of the glory that is the mecanim system, you can visit Day 11 where we first used it. Step 1.1: Get Zombie Asset Browsing around the asset store for another enemy to use and I found a nice barbarian mage to use for our tank enemy! It’s called Zombie01: Download and import the barbarian into our game. It’ll be in Assets > Zombie_0_1. Step 2: Setup the Zombie Yesterday, we created our Bandit enemy that we re-use our existing Knight Animator Controller, however for our Zombie I want him (it?) to have different move set and animations. Today, we’re going to do things differently! Step 2.0: Set Wave 1 SpawnManager to Spawn -1 Enemies If you have set our Wave 1 to spawn -1 enemies already then you don’t have to do anything. However, if not, go to our SpawnManager and make this change: Step 2.1: Creating an Animator Override Controller To achieve the goal of creating an enemy with new animations (but the same states), we’re going to use the Animator Override Controller. An Animator Override Controller allows us to give it an Animator Controller for us to override and put our own animation clips in. This is very handy for us if we want to create an enemy that has the same states from another Animator Controller, but you want to use different animations for each state. Let’s first create the animator. Go to Assets > Animator and right click and create a new Animator Override Controller. Let’s call it Zombie Animator Controller. Set Knight Animator Controller to be our Controller in our Zombie Animator Controller Now you’ll see something like this: See how we have the same 4 states from our Knight Animator Controller? With this we can add our clips in, but which clips? For this, let’s just re-use the existing ones that came along with the Knight Barbarian. In Assets > Zombie_0_1 > Animations, we’ll find Zombie_0_1, if we expand the model, we’ll see these animation clips! As you can see we have 8 animation clips attached: attack0 attack1 death idle0 idle1 run skill0 wound If you want to see what each of the animation does, click on the model and you’ll see the clips that are attached and what they each look like: Unfortunately, because these animations share very similar names, I’m going to have to rename them. The 4 we’ll need are attack01, death, idle0, and run. Click on each of them and hit Ctrl + D to duplicate them. Rename each of them to be ZombieAttack, ZombieDeath, ZombieIdle, and ZombieRun. Move these into our Animation folder. Here’s what we have: Back to our Zombie Animator Controller, let’s add our clips in and we should have this: Step 2.2: Attaching the Animator Controller Now that we have created our Animator controller, let’s create our zombie to use it! In Assets > Zombie_0_1 > Animations drag the Zombie_0_1 model into our game’s hierarchy. Add a new Animator component and inside for the Animator Controlleradd our Zombie Animator Controller. Here’s what we should have now: Note: The Transform position could be anything. I moved it closer to the player, but in the end, it’s not going to matter, because we’re going to spawn it in our present locations. Step 2.3: Attaching the Nav Mesh Agent and the 3 scripts: EnemyHealth, EnemyAttack, and EnemyMovement At this point, everything that we’re going to do is going to be the exact same as the day before. Select the Barbarian game object and add a Nav Mesh Agent Component, Enemy Health, EnemyAttack, and EnemyMovement script to it. Step 2.4: Setting up the Scripts and Nav Mesh Agent Now that we have attached our script, we need to set them up. Nav Mesh Agent For our Nav Mesh Agent component, we want to adjust the speed of how fast it moves. Let’s set the Speed to be 2. Our Knight’s speed is 3 and our Bandit’s speed is 3.25 EnemyHealth For our Enemy Health we want to set: Health: 15 Hit Sfx Clips > Size: 4 Set the 4 new Audio Clips to be Male_Hurt_01 — Male_Hurt_04 Hit Sound Delta: 5 EnemyMovement For the EnemyMovement scripts, we want to set: Walking Clips > Size: 4 Set the 4 new Audio Clips to be Footstep01 — Footstep04 Walking Delay:7 Knock Back Force: 9 EnemyAttack We’re going to do some re-work with our EnemyAttack script, technically speaking, we should use sound effects that are not punching sounds, however, because we’re only practicing, let’s just use the same sound effects we have been using. Attack Sfx Clips > Size: 3 Set the 3 new Audio Clips to be Punch_01 — Punch_03 Leave the Fist Colliders alone for now Now if we were to play the game, our Zombie will start chasing after us, but he won’t attack us yet, we haven’t set up our triggers and colliders yet! Step 2.5: Adding 3 Colliders: Trigger Collider, Mesh Collider, and Fist Colliders At this point, we’ll be doing nearly the exact same thing as we done yesterday. We’re going to add: A Capsule Collider to be used as a trigger to tell our zombie to enter attack mode if he’s near the player. 2 Box Collider that will be attached to the zombie’s weapon to tell when we damage the player. A Mesh Collider to be used for detecting when the player shoots the zombie. Adding the Capsule Collider Select Zombie in the hierarchy and then add the Capsule Collider component. We’ll make it a trigger, set the Y to be 1, and then expand Radius to be 1.5. When you’re done, we should have something like this: Note: the radius is the outer sphere that you see. To do a re-cap, whenever our player enters the circle, in our EnemyAttack script, we’ll hit OnTriggerEnter() which is used to tell the bandit that it’s in range to be able to hit the player. Adding the 2 Box Colliders Next up, we need to attach colliders to our weapon. The process won’t be too different compared to working with fists. In Zombie > Bip001 > Bip001 Pelvis > Bip001 Spine > Bip001 Spine1 > Bip001 Neck Bip001 L Clavicle > Bip001 L UpperArm > Bip001 L Forearm > Bip001 L Hand > Zombie_0_1_002 We want to attach a box collider. We’re just going to make it as big as our weapon. We need to make some changes with the collider. Specifically, the Z value for Center and Size. Set the Z value for Center to be 0.6 Set the Z value for Size to be 0.2 The reason why we’re doing this is because we don’t want our weapon to block our gun raycasting when we try to shoot the enemy. Next, we’ll attach our FistCollider script to our game object. At this point, we really should call it a weapon collider, but we’ll keep it the same for simplicity. When we’re done, we should have something like this: We’ll do the exact same thing for the right weapon as well. Adding the Mesh Collider Next up is our Mesh Collider component. The same as yesterday, the purpose of the Mesh Collider is for 2 reasons, it allows us to: shoot the enemy get pushed by the enemy For the zombie, this is easy. All we need to do is add a Mesh Collider component to Zombie > Zombie_0_1. And for the Mesh, attach Zombie_0_1 When you’re done, you’ll have something like this: Note: it has come to my attention, that using Mesh Colliders wouldn't be the best approach. Instead a better approach that was brought to light to me by Scouting Ninja is to use individual Colliders that covers our enemies. However, seeing how we already have this, I'll leave that as a future optimization we can do. We’re almost there, we still need to make our game object shootable so we can hit him. Step 2.6: Set Zombie Layer to be Shootable Straightforward, just like our Bandit, we did the day before, we want to go to our Zombie and change his layer to be Shootable. Step 2.7: Attach the Fist Colliders to EnemyAttack We attached our First Collider script to the Zombie’s weapons. It’s time to attach both of them to our EnemyAttack Script so we can detect when the weapon makes contact with the player in Attack(). In EnemyAttack set: Left Fist: Zombie_0_1_002 Right Fist: Zombie_0_1_1 With this, we can take damage now. Except for two problems: If we don’t move and let the enemy attack us, he just stops attacking us at one point! We don’t take damage when our enemy attacks! Step 2.7: Allowing Enemies to Continuously Attack Us Fixing the enemy not attacking problem turned out to be straightforward. If we were to look at the Animator of our zombie when he attacks us, you’ll notice something interesting. The zombie is in the right state attack state, but he only attacks us once. One possible area to look at next is the animation clip: ZombieAttack. Looking at it, the problem becomes apparent. Our Loop Time is not checked, which means that our animation won’t loop! Check it and we’ll solve our first problem. Step 2.8: Creating an Event in our Animation to Call Attack() The next problem is that our player character doesn’t seem to notice when they get attacked. If we were to do some print debugging, if we were to add a print statement inside our EnemyAttack script at Attack(), we’ll notice that we never call Attack()! As you might recall, we call Attack() by setting an event in our animation! Our Bandit was using the same knight attack animation so we never had to run into the problem, however, our Zombie uses a new animation! Let’s fix this: Select our Zombie game object In the Animation Tab (Window > Animation), select our ZombieAttack clip. Experiment around with what’s a good frame to add our event, but I chose frame 21 After creating our event, set the function to run to be Attack() When we’re done, this is what we’ll have: With this added in, the Zombie can now damage us! Step 3: Create a prefab Now that we finished our Zombie enemy, the last thing we need to do is to add it to our Prefabs folder for our SpawnManager in the future! We can just call our new prefab Zombie. Conclusion There we go! Now we have 2 more enemies that we can spawn in the game! Here’s what we have now: With all our new enemies, the next thing to work on is setting our SpawnManager to create new waves our enemy. We’ll set that up next time! Until then! Source: Day 28 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  6. Day 27 of 100 Days of VR: Adding New Bandit Enemy

    Thanks for the very awesome advice as always Scouting Ninja! I knew Mesh Colliders were bad, but I've never thought of making something similar to it by using individual colliders. Genius! I'll definitely remember this for the future and if performance becomes an issue.
  7. Welcome to Day 27! Right now, we have a relatively complete game and in my mental roadmap, we only have a couple more things to do before we start exploring how to use VR and then implementing that back into this game. There will be a lot of fun times ahead! Our goal is to create a new enemy. Specifically, for practice, I want to create: A lower health, but faster unit A higher health, but slower unit With these new enemies, we’ll have a more diverse and interesting experience in our game! Today, we’re going to focus on creating our speedy unit. Step 1: Acquire new assets The first thing we need to do is go to our favorite place in the world! The Unity Asset Store! We’re going to look an asset to be used as our enemy. When looking for new assets, the most important thing for us is to find something that’s mecanimready. If you’re not aware of the glory that is the mecanim system, you can visit Day 11 where we first used it. The basic summary is that all Unity models can have rigged body structure that can share animations from other models with similar body structures without doing any extra work on our end. Step 1.1: Get bandit asset Browsing around the asset store for an enemy to use and I found a nice bandit to use for the speedy enemy! It’s called Basic Bandit: Download and import the bandit into our game. It’ll be in Assets > BasicBandit. Step 2: Setup BasicBandit Now that we have our assets the next thing we need to do is to setup up our BasicBandit asset to use our existing animation and scripts! This work will be the same as what we have done with the knight model, except we don’t have to code anything this time! Step 2.0: Set Wave 1 SpawnManager to Spawn -1 Enemies Not necessary, but for the ease of testing our new enemy. If we set Wave 1 of our SpawnManager, to spawn -1 enemies, we won’t create any new enemies and the wave won’t progress, allowing us to experiment around with our enemies. Another option is to comment out the instantiation code in SpawnManagercode to not create any new enemies. Step 2.1: Attach the Knight Animatior Controller The great thing about Unity’s mecanim system is that we can re-use existing animator controllers for models that share the same animation. In this case, because our knight and bandit are both humanoids, we can share our Knight Animator Controller with our bandit. Now let’s do it! In Assets > BasicBandit, drag and drop Basic_BanditPrefab into our game. Select our bandit in the hierarchy and in the Animator component, under Controller, select our Knight Animator Controller to fill in the slot. You’ll have something like this now: Now with this, our bandit will have the same animation rules as our knight (running, attacking, death, and idle) Step 2.2: Add the Nav Mesh Agent and the 3 scripts: EnemyHealth, EnemyAttack, and EnemyMovement At this point, we want to re-create everything that’s attached to our Knight and move it to our bandit. We want to add the following things to our BasicBandit by clicking Add Component in the inspector: Nav Mesh Agent component to be the navigation AI to make the bandit chase after the player EnemyHealth: to receive the damage when the player shoots the enemy EnemyAttack: to detect when the enemy injures the player EnemyMovement: to control the movement logic for the enemy Step 2.3: Setting up the Scripts and Nav Mesh Agent Now that we have attached our script, we need to set them up. Nav Mesh Agent For our Nav Mesh Agent component, we want to adjust the speed of how fast it moves. Let’s set the Speed to be 3.25. Our Knight’s speed is 3. EnemyHealth For our Enemy Health we want to set: Health: 5 Hit Sfx Clips > Size: 4 Set the 4 new Audio Clips to be Male_Hurt_01 — Male_Hurt_04 Hit Sound Delta: 5 EnemyMovement For the EnemyMovement scripts, we want to set: Walking Clips > Size: 4 Set the 4 new Audio Clips to be Footstep01 — Footstep04 Walking Delay:3 EnemyAttack We don’t have everything for our EnemyAttack script, but let’s set what we can first. Attack Sfx Clips > Size: 3 Set the 3 new Audio Clips to be Punch_01 — Punch_03 Leave the Fist Colliders alone for now Now if we were to play the game, our Basic_Bandit will start chasing after us, but he won’t attack us yet, we haven’t set up our triggers and colliders yet! Step 2.4: Adding 3 Colliders: Trigger Collider, Mesh Collider, and Fist Colliders The final thing that we need to do is to add colliders to our bandit that will help us detect when the player is close, when the enemy damages our player, and when they get hit. To do a re-cap the purpose of what we need to do, we need: A Capsule Collider to be used as a trigger to tell our bandit to enter attack mode if he’s near the player. 2 Box Collider that will be attached to the bandit’s hand to tell when we damage the player. A Mesh Collider to be used for detecting when the player shoots the bandit. Let’s add them in. Adding the Capsule Collider Select Basic_Bandit in the hierarchy and then add the Capsule Collider component. We’ll make it a trigger, set the Y to be 1, and then expand Radius to be 1.5. When you’re done, we should have something like this: Note: the radius is the outer sphere that you see. To do a re-cap, whenever our player enters the circle, in our EnemyAttack script, we’ll hit OnTriggerEnter() which is used to tell the bandit that it’s in range to be able to hit the player. Adding the 2 Box Colliders Next up, we need to add 2 Box Colliders to our Bandit’s fists. Select Basic_Bandit > Armature > Hips > Spine > Chest > Shoulder.L > UpperArm > LowerArm > Hand.L We want to add a Box Collider to Hand.L In our Transform, we’ll keep position the same, however, let’s change the Size of our Box Collider: Size (X, Y, Z): 0.1, 0.3, 0.1 Finally, just attach the Fist Collider Script to Hand.L. When we’re done, we’ll have something like this: Now do the exact same thing to the other hand in Hand.R. Adding the Mesh Collider Next up is the Mesh Collider component. As we learned in Day 20, adding our collider into the parent object will not work, we should put the collider in the location of the mesh itself. This would serve us 2 purposes: We can shoot the enemy The enemy can push us around In the bandit’s case, we have 2 pieces: the body and the clothes. What you see above is the Mesh Collider in each of the pieces of the mesh of the bandit. Let’s get started! In BasicBandit_Body, add a Mesh Collider component, under Mesh add the mesh BasicBandit_Clothes. With this, you’ll have the 1st image above. Here’s what it should look like: Next: In BasicBandit_Clothes, add a Mesh Collider component, under Mesh add the mesh BasicBandit_Clothes. With this, you’ll have the 2nd image above. And here’s what it should look like: At this point, we’re almost there. We have the colliders to detect when our bandits get hit, however, if we were to try and shoot our enemy, we wouldn’t “hit” him yet. Step 2.4: Set Bandit’s Layer to be Shootable As you might recall when we were shooting enemies with Raycasting in PlayerShootingController, we did an optimization to only hit objects that have a layer mask set to Shootable. Select our bandit in the hierarchy and set our Layer to be Shootable. Now with these colliders set, we can shoot the bandit and they’ll take damage and be knocked back. Step 2.4: Attach our Fist Colliders to EnemyAttack Earlier on, we attached our First Collider script to the bandit’s hands. It’s time to attach both of our hands to our EnemyAttack Script so we can detect if either of our the bandit’s fists makes contact with the player in Attack(). In EnemyAttack set: Left Fist: L Right Fist: R Step 2.5: Create a prefab of our Bandit Now that we have everything setup, we’re going to make a prefab of our finished bandit model. Drag our finished copy of Basic_Bandit in our hierarchy into Assets > Prefabs. Let’s also rename our prefab to be just called Bandit. Conclusion Now with all these changes added in, we will have a fully functional Bandit enemy! I do want to mention that the game won’t function perfectly, for example, if we were to get killed by the Bandit, he’ll continue to attack us. The reason is that our Bandit isn’t in the Enemy Container game object, which means he won’t be disabled. In fact, our current Bandit isn’t parented by anything at all! That’s why he doesn’t stop after the player character dies. We can fix that by removing the bandit and adding him as one of the spawns for our SpawnManager, however before we do that, I want to create one more enemy to use. Tomorrow we’re going to create our last and final enemy. Until then! Source: Day 27 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  8. 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! 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: 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_03into 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: 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(); } } } } 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: 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; } // 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: 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 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! 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: 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! Source: Day 26 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  9. Thanks for the kind for words! Things like this is what keeps me going day after day! There were many times where I think to myself that I should just stop posting and just learn, but then I keep thinking to myself about how amazing it would feel when I reach my 100 day goal!
  10. Welcome back to day 25! We’re officially ¼ of our way to the goal! Today is going to be relatively short, we’re going to: finish our Enemy Spawning system by adding a victory state Let’s get started! Step 1: Creating the Victory Panel The first thing we need to do is create another panel that tells the user that they won the game. Instead of going through the work of creating another UI and animation for our Victory Panel, we’re going to take the easy approach and make a duplicate of our existing GameOver game object and re-purpose it. Currently, we can use the same script and animator, because they both do the same thing. Duplicate GameOver To do that, select GameOver in our hierarchy and then hit Ctrl + D to make a duplicate. Rename the duplicate to Victory. They can both just stay in the canvas. Changing Victory Text Now that we have our Victory panel, we need to change our text so that the players will know that they won. Select Victory > Text (not Button > Text) and change the words from “Game Over” to “You win!” Step 2: Writing the code to use our new Victory Panel Now that we have a fully functioning Victory Panel, it’s time to use it! We’re going to have to change 2 scripts to make this work: GameManager SpawnManager Updating the GameManager Code The first thing that we’re going to do is setup GameManager to use our Victory panel. Here’s our code: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; public Animator VictoryAnimator; private GameObject _player; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); } public void GameOver() { GameOverAnimator.SetBool("IsGameOver", true); DisableGame(); } 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 The only new code was that we added the VictoryAnimator, like GameOverAnimator, this is the animator for our victory panel. Walking through the code Here’s how our code works: I created a new public Victory() that our SpawnManager will call to trigger our Victory Panel animation. To keep our code clean, I moved the code that was previously in GameOver() into DisableGame() that way we don’t have to write the same code twice in both Victory() and GameOver() Updating the SpawnManager Code Now that our GameManager can play our victory animation, we need to call it when we win the game from our SpawnManager. Here’s the code for this: 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; void Start () { _gameManager = GetComponentInParent<GameManager>(); _currentWave = -1; // avoid off by 1 _totalWaves = Waves.Length - 1; // adjust, because we're using 0 index 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. Instantiate(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation); 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(); } } } New Variables In our code, we created a new private _gameManager which will be how we access our GameManager. Walking through the code The code is straightforward: In Start() we get an instance of our GameManager script by looking for SpawnManager’s parent, which in this case is the GameManager game object. Then we just take the script from there. We use _gameManager in StartNextWave(), when we have completed all the waves in the game. Step 3: Setting up our script components in Unity We have the script ready, the final thing for us to do is for us to add our new Victory game object to our script. When selecting our GameManager in our Game Manager script drag our Victory game object into the Victory Animator slot. Unity will automatically pull the animator from the game object for our script. Conclusion Now that we have set our victory animation, if we were to play the game and win, we’ll have something happen! Specifically, this! This will be the end of our Spawning System (for now). I’m hoping to introduce at least 2 more enemies into the game, so we’ll have to come back, but for this simple FPS, our spawning system is complete. Tomorrow, my plan is to go and fix minor parts of the game that we glanced over and then move on to adding more units! See you then! Source: Day 25 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  11. Today in Day 24, we’re going to resume working on our EnemySpawner. Yesterday, we laid the groundwork that allows us to spawn enemies, however, we haven’t implemented finishing a wave yet. As a result, we only spawn our 5 enemies and then nothing happens after we defeat them. Today, we’re going to make some changes so that we can move on to the next wave and then win the game. On a different note, after reading some other articles, I decided that I like their formatting, so I’m going to organize my articles the same way. Step 1: Create a SpawnManager Tag for our SpawnManager The first thing that we need to do is to set a Tag for our SpawnManager. We need to do this, because later in EnemyHealth, when our enemy dies, we want to be able to reference our SpawnManager. Select our SpawnManager game object and then under tag, select Add Tag… and create a new Tag called SpawnManager. Then go back to our game object and attach the tag like above. Step 2: Change EnemyHealth.cs to use our SpawnManager For our EnemyHealth script, we need access to our SpawnManager script so we can call EnemyDefeated(), which we implemented yesterday. Here’s our change: using UnityEngine; public class EnemyHealth : MonoBehaviour { public float Health = 100; public AudioClip[] HitSfxClips; public float HitSoundDelay = 0.5f; private SpawnManager _spawnManager; private Animator _animator; private AudioSource _audioSource; private float _hitTime; void Start() { _spawnManager = GameObject.FindGameObjectWithTag("SpawnManager").GetComponent<SpawnManager>(); _animator = GetComponent<Animator>(); _hitTime = 0f; SetupSound(); } void Update() { _hitTime += Time.deltaTime; } public void TakeDamage(float damage) { if (Health <= 0) { return; } Health -= damage; if (_hitTime > HitSoundDelay) { PlayRandomHit(); _hitTime = 0; } if (Health <= 0) { Death(); } } private void SetupSound() { _audioSource = gameObject.AddComponent<AudioSource>(); _audioSource.volume = 0.2f; } private void PlayRandomHit() { int index = Random.Range(0, HitSfxClips.Length); _audioSource.clip = HitSfxClips[index]; _audioSource.Play(); } private void Death() { _animator.SetTrigger("Death"); _spawnManager.EnemyDefeated(); } } Here are the changes we made: We get access to our SpawnManager: _spawnManager, which we’ll notify when an enemy die In Start() we get _spawnManager by finding our SpawnManager game object from its tag and then getting the SpawnManager script attached to it In Death() when the knight is defeated, we’ll call EnemyDefeated(), which will decrement our enemy count and call the next wave if all enemies are defeated. Now when we play the game, after we defeat our first 5 enemies, our next 10 will show up. Look at the fate of all those who opposed us! Conclusion Today has been a relatively busy day, so today’s progress has been relatively short. However, we’re almost done with our Spawning System. Tomorrow, we’re going to create a player victory state and then fully complete our player spawning feature to our simple FPS! I’ll see you all tomorrow! Source: Day 24 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  12. Here we are back to another day of Unity development! Today on day 23 we’re going to learn how to spawn enemy waves. Currently, the game isn’t challenging, we only have one enemy! Today, we’re going to fix this by spawning waves of enemies to defeat, making the game a lot tougher to survive! It’s going to be a big change requiring multiple days, so let’s get right to it! Creating an Enemy Wave Spawning Point If we were to recall from the Unity Survival Shooter tutorial, to spawn enemies, we need to create a SpawnManager class that creates new instances of the enemy. In our Spawn Manager, the primary thing we need to give it, among many other things that we’ll want to add, are the: location of where we would spawn the enemies enemies that we want to spawn However, as a challenge, as opposed to the Survival shooter, where the game would continue for it, we’re going to have a limited amount of enemy spawn so we can win. There was a lot of work involved in the wave system and I borrowed a lot of ideas from Unity’s Enemy Spawner example. Creating the initial game objects The first thing we want to do is create a new Empty Game Object that we’ll call EnemyManager. We’re going to make this a child of our GameManager. Next, we’ll create a new script for our new game object that we’ll call EnemyManager. We’re going to do a couple of things with our manager: Keep track of what wave we’re in Keep track of how many enemies we defeated in a wave Keep track of how many enemies per wave By keeping track of the number of enemies and waves, we can tell when to move to the next wave and if we win. Here’s our initial code for EnemyManager.cs: 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 int _totalEnemiesInCurrentWave; private int _enemiesInWaveLeft; private int _spawnedEnemies; private int _currentWave; private int _totalWaves; void Start () { _currentWave = -1; // avoid off by 1 _totalWaves = Waves.Length - 1; // adjust, because we're using 0 index StartNextWave(); } void StartNextWave() { _currentWave++; // win if (_currentWave > _totalWaves) { 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. Instantiate(enemy, SpawnPoints[spawnPointIndex].position, SpawnPoints[spawnPointIndex].rotation); 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(); } } } Now this is a lot to take in, which is why I added comments, however, here’s the run through of the code. About the Wave Class Before we talk about our variables, I want to introduce the Wave class. Wave is a container for us to hold the data for each wave that we’re going to face. If you remember from the Space Shooter tutorial, we did something similar. We created a new class to hold information and we made it Serializable so that Unity knows how to show it in the editor. Originally, I was going to just pass each of its content to our SpawnManager, but that’s prone to us causing mix-ups of how many enemies to spawn per wave and which enemies. About the variables For our public variable we have: Waves — An array of the Wave class that we created for an easy way for us to access data for each wave SpawnPoints — An array of locations that we’re going to instantiate our enemies TimeBetweenEnemies — The delay we wait before we spawn our next enemy For our private variables to keep track of the enemies, we have: _totalEnemiesInCurrentWave — Self explanatory _enemiesInWaveLeft — The number of enemies that are still alive in the wave _spawnedEnemies — Self explanatory We also keep track of what wave we’re in: _currentWave — Self explanatory _totalWaves — Self explanatory The Code Flow Now that we know the variable we’re using, we can walk through the rest of the code. In Start() we initialize all our variables to 0. Note that we set _currentWave to be -1 and our _totalWaves is the length of our array — 1. For those who aren’t familiar, all of this is, because we’re working in a 0-based indexing for arrays (meaning everything starts from 0 instead of 1). From Start() we also call StartNextWave() this code handles what happens when we clear our wave. We increment our _currentWave and assuming we haven’t finished the game, we’ll setup the enemies we need to encounter for the next wave and call SpawnEnemies() SpawnEnemies() is a coroutine that we use to create our enemies. The function will spawn the enemy for the wave based from one of the random spawn points we set. The code will wait 2 seconds before we spawn the next enemy so we don’t flood the players with all the enemies in a wave at once. Finally, we have public EnemyDefeated() which means this function is called from something else. In this case, our enemy will call this when they’re killed. We will then decrement our enemy counter and if we have defeated all the enemies in a wave, we’ll call StartNextWave() That’s the basic summary of the code. That wasn’t so bad, now was it? RIGHT?! Setting up our SpawnManager script Now that we have our script up and running, the last thing we need to do is to setup our Wave and create our Spawn Points. Setting up the Wave For now, let’s just make 2 waves for our SpawnManager that will create our knight enemy. There are 2 things we need to provide for our Wave class: The knight game object How many enemies to spawn for the wave Let’s first set the knight game object. We could just drag and drop our knight from our hierarchy into the spot and everything would be fine. However, the correct way to do this, is to first create a prefab of our knight. We can think of a prefab is a template of a game object that we can drag and share to different scenes and games. In our case, we can also instantiate them like what we’re doing here. The main benefit is that, if you were to change prefab, anytime we need to change something, we can just change the prefab and anything that instantiates it from code will also get the changes. The first thing we’re going to do is: Create a folder in our Assets folder called Prefabs Create our prefab, by dragging our Knight game object into the PrefabsNote, in this example our Knight is already a prefab, but that’s okay, we’ve already changed some things about it, so let’s make another prefab from it. On a side note: we can delete the Knight game object from our hierarchy. He’s served us well, but we won’t be needing his services anymore. Now that we have everything we need, we can get started in Creating our waves. Back in our SpawnManger script, expand Waves and set Size to be 2 so we can create new waves. Here’s what it’ll look like: Setting up the Spawn Point Now that we have the wave system setup, we need to create our spawn points. Exactly like the survival shooter, we need to add some game object that we’ll use as the location that we’re going to spawn the enemies. Let’s get to it! Create 3 Empty game objects: I call them SpawnPoint1 — SpawnPoint3. I made them the child of the SpawnManager, however, it doesn’t really matter where we put them. Right now, we can’t see the position of our SpawnPoints in the game, however we can add a Label to them to see where the objects are. We just want to set the Spawn points to be in 3 of the houses. Here are my Position values, yours might be different: SpawnPoint1: 98, 0, 118 SpawinPoint2: 72, 0, 89 SpawnPoint3: 106, 0, 80 Finally, go back to our SpawnManager and add our SpawnPoints in. Expand Spawn Points and then for the Size change it to 3, and then drag our SpawnPoints into the empty game spots. With all of this in place, if we play the game, we should now have multiple enemies coming at us. Conclusion We’re not quite done with our Spawning System, however we laid the ground works for spawning enemies. We still need to implement the logic for generating new enemies and finally creating our victory condition. We’ll start implementing those tomorrow on Day 24! With that being said, have a good night! Source: Day 23 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  13. Hi and welcome back to day 22 of the 100 days of VR! Yesterday, we created our game over panel to show to the player when they lose, however in the current state of the game, the game over panel is being displayed at the start of the game. Today, we’re going to: Create a new state controller for our animation so that we don’t instantly play our animation Play the animation when the player is defeated and all states related to that Restart the game when the player selects to play again There’s a lot ahead of us today, so let’s get started! Creating an Animation State Controller Right now, our animation just plays whenever the game starts. The reason for that is, because of our animator controller. Go to our GameOver.controller in the Animation folder and double click it to open it up in our Animator tab. Right now, Game Over is the default state we transition into from Entry. As a result, we always play the Game Over clip that’s attached to it be default when we start our game. To fix this, we’re going to do a couple of things. We’re going to: create a new default state,called Start that is our default transition from Entry that transitions into our Game Over state create a new boolean parameter IsGameOver set a transition from Start to Game Over select the transition arrow from Start to Game Over and create a new condition: IsGameOver = true After you’re done, you should have something like this: Now if we’re to play our game again, nothing will show up! Playing the Game Over animation Now that we have the correct game states set up, it’s time to use it. All we need to do is play the animation when our player’s health drops to 0. Here are the changes we’re going to do: Create a new GameManager script that will take care of the logic of the ending Change our PlayerHealth script to use the GameManager to tell it when the game is over. The first thing we’re going to do is create an empty game object that we’ll call GameManager. Then create a new script called GameManager and attach it to our new game object. Here’s what our GameManager script will look like: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; public void GameOver() { GameOverAnimator.SetBool("IsGameOver", true); } } The code right now is straightforward: We get the GameOverAnimator from our Game Over object and we take the Animator from it. We create a public GameOver() that someone will call that will set our IsGameOver parameter to be true so that we’ll play the animation to show our game over panel. When you’re done with the script, make sure to set our GameOver UI GameObject to our GameOverAnimator slot in our GameManagercomponent. Quick note on pushing vs pulling for updates For those of you who saw Unity’s tutorial, I’m not actually a big fan of what Unity does with having all of these checks in the Update(), where we’re always checking for something to happen. Personally, I much prefer to take a more push functionality where we only do something, if something else notifies us of it happening. A perfect example of a push approach is our public GameOver(). Currently we have some other script call the function to indicated game over. An example of pulling is that we have a script that checks for changes every frame. For example. We could have given GameManager a reference the the player Game Object and in Update() it could constantly check to see if the player is dead. Both way works, I just prefer the way where we only do something when we’re told to do it and not check every frame. Using our GameManager After creating our GameManger, we need to use it. In this particular case, we’re going to use it in our PlayerHealth script. We’re going to: Add our GameManager object Call GameOver() when the player’s health is 0 or below Here’s our code: using UnityEngine; using UnityEngine.UI; public class PlayerHealth : MonoBehaviour { public Slider HealthBar; public float Health = 100; private float _currentHealth; private GameManager _gameManager; void Start () { _currentHealth = Health; _gameManager = Object.FindObjectOfType<GameManager>(); } public void TakeDamage(float damage) { _currentHealth -= damage; HealthBar.value = _currentHealth; if (_currentHealth <= 0) { _gameManager.GameOver(); } } } The code here is also straightforward: We create a private field for our GameManager class In Start() we instantiate our GameManager In TakeDamage(), after our player takes damage and health falls to 0, we would call GameOver() which will show the Game Over panel. Restarting the Game Now that we have our game over panel showing up, it’s time for us to disable some functionality so that we can click on the restart button! There’s a couple of things we need to: in GameManager, when GameOver() is called disable the player’s movements and re-enable our cursor to select a button create a GameOverUIManager that will deal with the logic of clicking a button change our GameOver game object’s Canvas Group to ignore parent elements and check the Blocks Raycasts option so we can click on the button Updating GameManager to Disable the Player in Game Over When the game is over, we want to: disable all the scripts that are involved with the controls of the player. It would be weird that at game over we can still move around and shoot. re-enable our Cursor which we locked in the center of the game Here are the changes we did to GameManager: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; private GameObject _player; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); } public void GameOver() { GameOverAnimator.SetBool("IsGameOver", true); _player.GetComponent<PlayerController>().enabled = false; _player.GetComponentInChildren<MouseCameraContoller>().enabled = false; _player.GetComponentInChildren<PlayerShootingController>().enabled = false; Cursor.lockState = CursorLockMode.None; } } As you can see, we: Created a reference to our player character in Start() In GameOver() we disabled the player movement script, our mouse script, and our shooting script. We also re-enabled our lock With this, if you were to play the game right now and let our player die, we regain our mouse and we won’t be able to control our player anymore. Creating the Code for our UI Button Click Next, we need some code to take care of what happens when we click on our button. To do that we need to create a onClick event listener for our button. For those of use that aren’t familiar with the concept of event listeners, you can think of it as code that will only execute when you take a certain action. In this specific instance, our Button has a onClick event listener so it’ll wait for whenever the user clicks on the button. When someone does click the button, they’ll execute any function that was passed to it. In this case, our restart code. Let’s get started. First, create a new script for our GameOver game object in the hierarchy. We’re going to call it GameOverUIManager. We’re going to use it to manage our play the game when the user clicks play again. Here’s what it looks like: using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; public class GameOverUIManager : MonoBehaviour { private Button _button; void Start () { _button = GetComponentInChildren<Button>(); _button.onClick.AddListener(ClickPlayAgain); } public void ClickPlayAgain() { SceneManager.LoadScene("Main"); } } Now let’s walk through the code: We have a private _button that is the button from our Game Over Panel. In Start() we instantiate our button and we set the OnClick Listener to our button to run ClickPlayAgain() whenever the user clicks the button In ClickPlayAgain(), we use the SceneManager to restart our scene so we can play again. As we might recall, everything in Unity runs in a scene, SceneManager is what we use to help us transition from one scene to the next. Note: using LoadLevel is depreciated for SceneManager Now with all of this, we’re almost done! If we were to try and play the game now and let the knight beat us, you’ll realize a problem. We can’t click on the Restart button! Let’s fix it. Allow Clicking on the Button UI The main reason why we can’t touch our button UI is because of our Group Canvas component attached to HUD. We disabled everything so that our UI will just be something the user sees. We could enable Interactable and Block Raycasts so that our Graphics Raycaster script will be able to detect the button being clicked, like explained here. However, if we were to do that, the problem is that everything else in the game would become interactable, including our health bar! We don’t want that, so instead we’re going to make a separate Canvas Groupcomponent just for GameOver. We need to do 2 things: Add a new Group Canvas component and check all settings so that we can interact with our game over panel and ignore the rules set by the parents. Add a Graphic Raycaster, which is used to detect if the user clicks on anything in the canvas. Once we have these setup, play the game again and you’ll see that we can restart the game! Conclusion We did a lot of work today! We: Created a new animation state for our GameOver animations Enabled our animation when our player is defeated Allowed the user to restart the game when they lose There are some more things that we can add to our Game Over scene to make things more complete, however, tomorrow, we’ll jump into a new topic, specifically enemy spawning! Until then! Source: Day 22 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  14. Welcome back to day 21. In the past couple of days, we worked on creating a health system for our player and fixing some problems with our enemy that relied on it. Now we can kill enemies and enemies can kill us, it’s time for us to make this a more complete game! Today the first thing to do is create a game over state when the player’s health reaches 0. To do that we’re going to use Unity’s UI system. Here’s our game plan today. We’re going to: Create a Panel to hold our content Add in a retry button and a game over text Use an animation for our panel to appear when we lose Use our new Game Over UI into our code Let’s get to it! Creating the Game Over UI The first thing we’re going to do is create the game over UI that we’ll show to the player when their health goes down to 0. The first thing that we must do is create a new Panel UI element to our existing UI canvas HUD. We’ll call the panel UI GameOver. Then as a child to our GameOver game object, we’re going to create a new Text and Button UI element. Let’s make some changes, first we’re going to change the GameOver screen, the first thing we’re going to do is change the Rect Transform. Set the Rect Transform to be the center of the screen with Shift + Ctrl to center our panel. Width: 300 Height: 150 Next, let’s change our Text and here are the settings we want to make to our Text: Font Size: 28 Font Style: Bold Rich Text: Selected Text: Game Over Color: Alpha set to 255 For the Button game object, inside the Rect Transform: Set the Rect Transform to be the center bottom of the screen with Shift + Ctrl to center our Button Width: 160 Height: 30 Pos Y: 20 Then inside the Text game object in the Button, we’re going to change the setting for Text. Font Style: Bold Font Size: 18 Rich Text: checked When you’re done, we’ll have something like this: So now that we have our Game Over pane, the next thing we need to do is to create an animation to get this to show up. Creating the Game Over Animation Right now, we just have a panel standing in front of the screen, if we were to play the game, it’d just be in front of us. Just like in the Survival Shooter tutorial, let’s create an animation for our panel to show when the game is over. To do that, we’re going to use Unity’s Animation feature to create a new animation for our Game Over game object. If the Animation tab isn’t available, go to Windows > Animation to get the window to show up. Select the GameOver game object from the panel and then in the Animationtab, click Create: and you’ll be ask to create a new .anim file. Move it to our Animation folder (or anywhere you want to store it) and name it Game Over.anim. This will automatically create both the GameOver animation clip and the GameOver controller that we use to attach to our game object. In fact, Unity will already automatically attach the controller to our game object. Now that we have the animation selected, the next thing we need to decide what we want our animation to look like. Here’s what I imagine: Our panel would be invisible and then it slowly appears (from alpha 0 to 1), as that happens, it’ll start to drop from the top of the screen to the center (let’s say from Y position 100 to 0). Let’s make it happen! Setting up our Game Object The first thing we need to do is add a Group Canvas component to our GameOver game object. The reason why we need this is that we need to be able to make everything transparent in our animation the parent and the child. Just by changing the parent’s alpha will only change its transparency and not its child’s. Luckily Unity has thought of that and that’s why among many reasons Group Canvas offers an alpha property. Creating our Animation Clip Now that we have all the setup that we need, let’s get started creating our animation. In the Animation tab with the GameOver game object selected, we’re going to click Add Property and add 2 properties: Canvas Group > Alpha — to set our alpha Rect Transform > Anchored Position — to set our Y position Now that we have all the properties we needed, we’re going to make changes to the value in the animator at different frames and Unity will fill in the values from the start to the end. By default, for all of our properties, we’ll have a key (the grey diamonds) at the start of our animation at frame 0 and we have another key at frame 60. Each key we can set a value and Unity will automatically lerp the values from one key to the next. The important part is that we move the white line to the specific key whose value we want to change. Changes only apply to the location of where our white line is. For Canvas Group, at the key at frame: 0, set alpha to be 0 60, we can just leave the value at 1 For Rect Transform, at the key at frame: 0, set Anchored Position.y to be 100 60, we can just leave the value at 0 When we’re done, we should have something like this: Finishing Touches with our new animation Before we finish for today, there are 2 finishing touches that should be done. The first is that we shouldn’t show our Game Over game object in the scene. To fix this, we go back to our Canvas Group component inside Game Paneland then we set our alpha to be 0. Next, if you were to play our scene, our animation would constantly play on repeat. We can fix this by selection our GameOver.anim inside our Animation folder and unselect Loop Time. This will be our short fix, tomorrow, we’re going to implement a state controller for our animation so we can really control how our animations. Conclusion That’s it for today! Tomorrow, we’ll go on and attach our animation to when the player’s health drops below 0 and other things like restarting the game is over. Until then, have a good night! Source: Day 21 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  15. Welcome back to another exciting day of Unity development at day 20! Can you believe it? We’re 1/5 of the way to 100 days! I’m hoping that the momentum will keep me going forward until I ship something! Now looking back at day 19, we created a health system that we can now use, however we encountered a problem. For some reason, our collider only hits the enemies once even if we go through the attack animation multiple times. Today we’ll be solving that problem. So, let’s get to it! Fixing Our Mesh Collider The problem lays with the Mesh Collider that we created for our knight on Day 12. Remember this? That standing mesh collider, is literally what we’re colliding with every time the Knight attacks us. The problem is that Mesh Colliders can’t follow animations of the model, as a result when the knight first touches us, the mesh might touch us, but afterwards it we, the player, don’t get pushed back by the collider, the mesh will never touch us again. Here’s a picture to demonstrate the problem: While the knight model itself might be touching us, our Mesh Collider is static and doesn’t move. This isn’t good! We want to damage our player when the knight’s fist contacts the player! Not this static mesh collider! Some changes are going to be needed to fix this problem. We still need our Mesh collider. This prevents us from walking through our enemy. Right now, we have a problem where we can walk over the knight, but we’ll fix this now. We’re going to add a Box Collider to the hands of our Knight. This way, we’ll know for sure when our player gets punched. Fixing the Mesh Collider for the Knight Right now, we can’t move our mesh, because it’s in the parent body. According to this post about incorrect mesh positioning, we will make a new empty object and add the mesh collider there. This works for us, because we only need the mesh for collision detection and for our raycasting for shooting. Let’s do it! Originally, we put the Mesh Collider in our knightprefab game object. Let’s remove that now. Select knightprefab, right click, and select Create Empty, to make a new Game Object. Let’s rename this object to Collider. Select Collider and add a new Mesh Collider component to it. For the Mesh, select body to get our Knight model back. You might have to move Collider around yourself to get the collider to match our knight, however, at the end you should have something like this: Now with this, we can’t just walk over the knight. Adding new Colliders for our Fists! Now that we have solved the Mesh Collider problem, we’re going to fix our original problem with dealing damage when the enemy attacks us. To do this, we’re going to add box colliders, just to the fists of our knights. Specifically, in knightprefab -> hips -> spine -> chest -> L_shoulder/R_shoulder -> all the way down to L_Wrist/R_Wrist. For each box collider, I made the scale of the box to be 0.25 to fit the size of the hand. Now that we have this, we need to attach a script to each of our hand models that can detect the collision. Scripts from parent objects can’t detect collision for it’s children. It’s also important that our original script: EnemyAttack, stays where it is, because we need access to the animator component that’s located in the parent. To solve our problem, we’re going to move the collision part of our code from EnemyAttack to a new script called FistCollider. FistCollider will deal with the collision of the knight fists and we’ll get the results in our EnemyAttack script. Here’s our FistCollider script: using UnityEngine; public class FistCollider : MonoBehaviour { private GameObject _player; private bool _collidedWithPlayer; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); _collidedWithPlayer = false; } void OnCollisionEnter(Collision other) { if (other.gameObject == _player) { _collidedWithPlayer = true; } print("enter collided with _player"); } void OnCollisionExit(Collision other) { if (other.gameObject == _player) { _collidedWithPlayer = false; } print("exit collided with _player"); } public bool IsCollidingWithPlayer() { return _collidedWithPlayer; } } Here’s the flow of the code: Like our EnemyAttack script, we want access to our player game object and we want to check if we collided with the enemy. In Start() we make sure to instantiate both of our variables. In OnCollisionEnter() and OnCollisionExit(), if whatever we collided with is the player than we would set our boolean flag. Once again this is exactly the same as In IsCollidingWithPlayer() we would give our bool _collidedWithPlayerto whoever calls it. Note that this function is public so other script can have access to this. We’re going to use it later. Let’s attach the script to both L_Wrist and R_Wrist. Now that we moved part of our collision code from EnemyAttack to FistCollider, let’s use FistCollider in EnemyAttack. Here’s the code: using UnityEngine; public class EnemyAttack : MonoBehaviour { public FistCollider LeftFist; public FistCollider RightFist; private Animator _animator; private GameObject _player; void Awake() { _player = GameObject.FindGameObjectWithTag("Player"); _animator = GetComponent<Animator>(); } 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()) { _player.GetComponent<PlayerHealth>().TakeDamage(10); } } } Here’s the changes in our code: First, we create new public variables: LeftArm and RightArm that are the FistCollider script that we just created. We cleaned up a lot of the code, specifically the Collision part and anything that had to do with it. In Attack() we use our new FistCollider to make collisions with the player and then when the attack event from our animation gets fired, we check if the player gets hit. If they do (meaning the knights fist collided with the player), the player will take damage. With our script done, the final thing that we need to do is to make sure that we attach the FirstCollider object to our player. We can directly attach our game object into the spot and the script will be automatically selected. When you’re done, we should have something like this: Conclusion Phew that was a lot of work! In these past 2 days, we created the health system and we re-visited some of the existing problems with the enemy knight’s mesh collider such as walking over them and not accurately colliding with the player. We also separated out the code with attack collision work while still making our original collision script work as normal. Now that we finally have health for our player, tomorrow, the next thing we’ll do is start working on an end game state for when the player’s health reaches 0. Until then, have a nice evening, I’m going to sleep! Source: Day 20 Visit the 100 Days of Unity VR Development main page. Visit our Homepage