• Advertisement

Josh Chang

Member
  • Content count

    53
  • Joined

  • Last visited

Community Reputation

10 Neutral

About Josh Chang

  • Rank
    Member

Personal Information

  1. Here we are at another milestone in the 100 days of VR challenge! Day 40! Who would have known that I would have made it to this point? We’ve come a long way, we learned a bit about Unity, made a simple game, and now here we are working in VR! Yesterday we finished fixing most of the technical problems involved with porting our game to VR. Well, turns out, I lied, there are a couple more things I’d like to fix today along with working a bit on the UI. For some reason, our camera is lower than we set it Enemies are literally running into us when trying to hit us Get our UI to show up again Step 1: Changing Our Camera Starting Position When we play the game on Unity, we have an interesting problem with our camera position being changed. We set our camera position to be 1.5 at Y: However, what’s interesting is that when we play the game on the Android platform, our camera position gets set to 0: After debugging around, I found the cause. Our GvrEditorEmulator prefab forces our Main Camera to be set to the position: 0, 0, 0. While technically, we don’t need the prefab, it is convenient, so we’ll keep it in. Instead, I’ve found a different solution. While our camera is forced to be a set position, we can child our camera to another game object and then change the position of the parent game object. Coincidentally, we’re already doing that with our Player game object. All we have to do is raise our Player Y position up by the amount of the camera. Select Player, change the Y position from 1 before to 1.5 (Optional) Go to the Main Camera and change the Y position to 0 Now when we play the game, we’ll have a much better height when playing: Step 2: Stopping Enemies from Going Inside the Player Next problem, the enemies are literally running into us. See: There could be many things that are causing the problem, but I decided to look at the Nav Mesh Agent attach to each of the enemy prefabs, because there are options that control how close the enemy would get to their target: After playing around with the settings I got the enemies to stop right in front of us: Stopping Distance: 1.25 Auto Braking: Disable Radius: 1.25 Here are our new settings: With these new settings in, here’s a bit of our gameplay now: Isn’t it nice that they’re beating us from the outside and not the inside? No? Oh well… Step 3: Getting the UI to Show Up Again Now that we have all the technical problems resolved (promise this time!), it’s time for us to go back and look at how we can get the UI to show up for our game. This is going to be an annoying problem to solve because the problem only occurs on our Android device and not anywhere else. Which means there will be A… lot… of… building… Luckily for you, I’ve gone through the torture of re-building multiple of time so the rest of us don’t have to! It turns out that getting the UI to show up isn’t too bad! It turns out that if we have VR mode enabled in Unity, any UI we have on our Android device will NOT be available unless the canvas the UI is set with is set to World Space. Here’s what it looks like on my phone when I disabled the VR options in Player Settings and just have normal Android app: According to Unity’s quick guide for VR UI, and for extra credit, this article about UI expectations for the player, UI in VR must be run in World Space. The biggest key takeaway I got from these 2 articles is that you should NEVER have the UI on your player’s screen, like how we’ve been doing it. This could cause motion sickness. Specifically, we want to use Diegetic UI, where the UI is attached to a game object in the game world as opposed to an overlay on top of our screen. Instead of having it float around or statically placed would be the better way to go. Step 3.1: Getting the UI to show up Anyways, to fix our problem and have our UI show up when VR is enabled, we must set our Canvas to be displayed in World Space. Select HUD in our hierarchy. In the Canvas Component, change Render Mode from Screen Space – Overlay to World Space There are 3 options available to use, here’s the canvas documentation to explain what they are, but for a quick summary of the available render modes: Screen Space – Overlay: The UI is rendered on top of the scene Screen Space – Camera: The UI has put a certain distance from the Camera. It’s very similar to the Overlay, except certain changes to the camera could also cause changes to the UI. An example would be Perspective Camera would render the UI differently from an Orthogonal Camera World Space: The UI Canvas will act like a game object that just hangs somewhere in the game world for us to see. Also, this is the only option we can use for our UI Here’s what our game looks like now with the World Space: Now we need to make some adjustments with our UI. Step 3.2: Figuring out where to put the UI The biggest question now at this point is, where should we put our UI elements? While there isn’t a clear answer, I think our best option might be to attach the UI to be our weapon. On the Google Cardboard, this would be the equivalent of having it right in our face, but if we were to switch to use the Daydream Viewer, we would be able to move it independently of our gaze. With the decision being made, let’s go and see how we can attach our health and time UI to our gun! Step 3.3: Putting Our UI Into World Space We already have an existing HUD canvas game object in our game. We’re going to repurpose that for our game, however, because we have more than just our UI on the canvas (the Victory and Game Over panels), I’m going to duplicate our HUD On HUD in our game hierarchy, hit Ctrl + D to duplicate it. Rename the duplicated HUD (1) to be called GunUICanvas Delete the Victory, Game Over, and Ammo UI child objects Make the GunUICanvas a child of MachineGun_01 When we’re done, here’s what your hierarchy should look like. Next up, we’re going to change the settings of our GunUICanvas so that it would be right in front of our gun on the right side: Select GunUICanvas In the Canvas component, the Render Mode should already be World Space, if not, change it In the Rect Transform component, I’ve played around with the settings and changed our position to be (-0.15, 0.22, -0.14), our Width to be 480, and our Height to be 80. Set Scale to be (0.001, 0.001, 0.001), we want our UI to be small enough to fit in our screen (Optional) Remove the Screen Manager Script Here’s what we should have: Next, I’m going to change the Anchor Presets for our Score UI game object to be in the bottom middle of our screen. Select Score In the Rect Transform component, open our Anchor Presets and hit Alt + Shift and select bottom center preset Now with the width changes of our Canvas and making our Score at the bottom, we should have something like this for our game. If we play the game in our VR device, I don’t think there will be any discomfort. The UI is positioned on the gun and since we’re focused on the cursor in the center, it’s not in our focus. Step 3.4: Connecting the UI Elements to the Rest of our Scripts Now that we have our GunUICanvas looking nice, the last thing that we need to do is re-connect all the UI elements to our scripts that use them, so our UI can get updated as we play. We need to update our: Time Text Health Slider Do you remember which scripts used these UI? No? Don’t worry I do! In GameManager, in the Score Manager script, drag our Score UI into the Score slot In Player, in the Player Health script, drag our Health Bar slider into the Health Bar slot Once we add our new UI components, our code will update the UI correctly! Conclusion And that completes Day 40! Today we looked at fixing our last technical problems with the camera height and getting the enemies to attack us at a more appropriate distance. However, the main topic of today was adding our UI back in. It turns out that with Unity VR enabled, the only Canvas we can use is World Space. Once we have made that change we can see our UI again. With our new UI, we attached it to our gun and I think we made a good solution to provide relevant UI information to the player all without causing nausea. Tomorrow, we’re going to continue working with the UI. Specifically, we’re going to add the game end states back into the game, so we can keep playing it without starting a new app instance. Day 39 | 100 Days of VR | Day 41 Home
  2. Welcome back to day 39! Yesterday we started to look at fixing problems that involved the limitation of Mobile VR (and a lot of raycasting), today we’re going to make some more changes. Specifically, the goal today is: Change our Event Trigger logic to deal with what happens if we’re holding down on the screen Fix a problem with our player being pushed around Fix why our Knight turns black Change our enemy to be slower to make the game easier Let’s get to it! Step 1: Changing Our Event Trigger Logic for Continuous Fire Right now, we’re trying to solve the problem where we only damage the enemies when we tap on them. If we were to hold on, then we would continue to shoot, but the enemies won’t take damage Yesterday we discovered that there was 2 ways we could have implemented our game. Use our existing code where we shoot a raycast and depending on what we hit, run some function. Use the Event Trigger system along with Google’s changes. I’ve played around quite a bit with the Event Trigger system and made a solution, but it’s not the best, in fact, I might have preferred just keeping what we have, but that’s okay, we’re just learning! There are 2 problems that we must solve: What happens when we’re holding down the screen on an enemy What happens when we’re holding down and then we move to point at another enemy. After playing around for a while, the PointerClick solution we have will no longer work. Instead, I’ve started playing with the PointerEnter and PointerExit events. I’m going to add the changes to EnemyHealth: using System; using UnityEngine; using Random = UnityEngine.Random; public class EnemyHealth : MonoBehaviour { public float Health = 100; public AudioClip[] HitSfxClips; public float HitSoundDelay = 0.1f; private SpawnManager _spawnManager; private Animator _animator; private AudioSource _audioSource; private float _hitTime; private Boolean _isEnter; void Start() { _spawnManager = GameObject.FindGameObjectWithTag("SpawnManager").GetComponent<SpawnManager>(); _animator = GetComponent<Animator>(); _hitTime = 0f; _isEnter = false; SetupSound(); } void Update() { _hitTime += Time.deltaTime; if (Input.GetButton("Fire1") && _isEnter) { TakeDamage(1); } } private void TakeDamage(float damage) { if (Health <= 0) { return; } if (_hitTime > HitSoundDelay) { Health -= damage; 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; } } public void HealthEnter() { _isEnter = true; print("enter"); } public void HealthExit() { _isEnter = false; print("exit"); } } Walking Through the Code In our EnemyHealth, we create 2 new functions: HealthEnter() HealthExit() These functions are going to be called from the PointerEnter and PointerExit from our Event Trigger that we set up. In these functions, we set new variable we introduced called _isEnter so we know when they’re being selected. Inside Update() we check to see if we’re currently hovering over the enemies and if we’re pressing down on the screen. If we are, we would call our already existing Shoot function. I’m not a fan of this method because it requires us to constantly call Update() in all of our enemy health scripts as opposed to just inside our PlayerShootingController script, but for just playing around, this is okay. I also changed the hit sound effect to be able to play every 0.1 seconds just like our shooting delay. Step 1.1: Updating our Event Trigger Now that we have our shooting script in, the next and final thing we need to do is to create the Event Triggers to use them. Get rid of the PointerClick event that we’ve previously set up. Instead, we’re going to create 2 new types of Event Triggers: PointerExit and PointerEnter. Here’s what we’re going to do: Attach HealthEnter() to PointerEnter Attach HealthExit() to PointerExit Now we can play our game like we intended to do from the very beginning. Make sure to make these changes to the Bandit and Zombie too! Step 2: Preventing Our Player from Falling Currently, if we were to play the game, when an enemy gets closed to our player character, we would fall. We should have addressed this in the past when we set constraints inside our RigidBody component, but it appears that we have not. Let’s go back and fix this In the hierarchy, select Player Under the RigidBody component, we’re going to set our constraints. Specifically, we want to freeze our position and rotation so that the enemies won’t push our character around. Having the game move us could cause nausea for our players. Step 3: Fixing our Knight’s Color If we were to play our game on our mobile device, we’ll notice one big problem. Our knights are all black. If we pay attention to our log, we’ll see this: It seems that we have some problems with the shaders that the asset is using. Unfortunately, I don’t know enough about this problem to resolve this. We have 2 choices: Ignore the problem and just have all black knights Change the materials that use these to use the standard shader. In our case, we’re going to explore the 2nd option. In Assets/Knight/models/Materials we have 3 materials that we’re using: clothColor, knight1Color, weaponsColor, and all of them uses one of those 2 shaders above. Let’s select them and changed them to Standard. Now if we were to play the game on Unity, here’s what the knights would look like: It lost the coloring we originally had for it, but at least we keep the details of the models. Step 4: Making the Game Easier Currently, in our game, we would be getting swarmed with enemies. That would have been fine if we can move around, unfortunately, we can’t do that anymore, thus as a result, we need to make some adjustments We’re going to do 2 things: Change the rate of how long it takes for an enemy to spawn Slow down the rate our enemies move Step 4.1: Changing Spawn Rate on the Spawn Manager Currently, we spawn the next enemy every 2 seconds. Let’s change that to 5. Select the SpawnManager game object (child of GameManager) Set Time Between Enemies to be from 2 to 5 Step 4.2: Changing Enemy Walking Speed As we might recall, to control the enemy speed, we must look at the Nav Mesh Agent component in our enemy prefabs. In order of speed, our speed order is Bandit, Knight, Zombie, with the Bandit being the fastest and Zombie being the slowest. I’m going to change the speed a bit. Bandit to 2 Knight to 1.5 Zombie to 1 Here’s an example: Conclusion Now we’re done! We have taken care of a lot of the technical problems that we encountered. Tomorrow, we’re going to continue to finish the rest of the game by figuring out how we would add UI into a virtual reality environment. Until then, I’ll see you all later in day 40! Day 38 | 100 Days of VR | Day 40 Home
  3. Day 38 of 100 Days of VR: Creating a VR First Person Shooter I

    That's a really interesting idea. I've never thought of using Leap motion controller. I've heard a lot about them, but I've never had the chance to play around with one. It'll definitely be on my bucket list of things to do!
  4. Welcome to Day 38! Today, we’re going to talk about the limitations of mobile VR and make some changes in our game to fix things. We’ve already started to fix some things, specifically adding event triggers to our enemies, but there’s still many more things to solve! Here’s a quick list of things I want to tackle from what we encountered 2 days ago: From a technical limitation: We can’t move We only have one input which is clicking Some actual technical problems: The enemies are all black color We don’t have any of our UI’s anymore We’re going to address these problems over the next couple of days. Today, we’re going to focus on the technical limitations of Mobile VR, today’s priorities are: Discussing how to change our game design to accommodate our new limitations Implementing our new designs Edit, Important Note: After playing around with the Cardboard in Unity today and looking at this article about Google Cardboard’s inputs. It seems that we don’t have to use Google VR SDK. Unity already has most of the internal integration necessary to make a VR app Everything we had already works, the reason why there I initially thought there was a problem is, because of how we did raycasting. Specifically, our raycasting code targeted where our mouse/finger was touching, not the middle of the screen! More on this later. Step 1: Changing the Game to Fit our Mobile Limitations Like mentioned before, in the Google Cardboard, we have 3 limitations: We can’t move our characters position We only have tapping as an input to interact with the game Our cursor will always be in the middle of the screen Even for the Daydream Viewer, we will have the first 2 limitations. However, with the new Daydream Standalone device coming out, we’ll have World Space, finally allowing us to track the player’s movements without requiring external devices like what the Vive does! Anyways, back on topic. Considering these 3 limitations, here are my thoughts of what needs to be changed in our game: Because we can’t move, we should place our character in a more centered location for the enemies to reach us Because we can no longer run away, we should make the enemies weaker so that we don’t get swarmed Because we only have one input, we can shoot, but we can’t reload, we should get rid of the reload system Essentially, we’re going to create a shooter with our player in the center with enemies coming from all around us. Step 2: Implementing Our New Designs Now that we have everything we want to do planned, let’s get started in the actual implementation! Step 2.1: Placing the Character in the Middle Let’s place the character in the middle of where our spawn points are set. After playing around with it, I think the best spot would be at Position: (100, 1, 95) Select Player in our hierarchy. In the Transform component, set our Position to be X: 100, Y: 1, Z: 95 Step 2.2: Making the Enemies Weaker Next up, let’s make the enemies weaker. In the Enemy Health script component attached to our Knight, Bandit, and Zombie prefab, let’s change their health value. In order of our health, the order of size from largest to smallest is: Zombie > Knight > Bandit. Let’s set the health to be: Zombie: 4 HP Knight: 2 HP Bandit: 1 HP Here’s how we change our health: In Assets > Prefabs select our prefabs, in this case, let’s choose Zombie. In the Inspector, select the Enemy Health (Script) component and change Health to be 4 Do the same change with the other 2 prefabs. Step 2.3: Remove our ammo system Now it’s time to back to our Player Shooting Controller (Script) Component that we disabled yesterday. I want to keep the animation and sound effects that we had when shooting our gun, however, I’m going to get rid of the ammo and the need to reload. Here are my 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 + 10; //_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.GetButton("Fire1") && _timer >= ShootingDelay /*&& !_isReloading && _currentAmmo > 0*/) { Shoot(); if (!_isShooting) { TriggerShootingAnimation(); } } else if (!Input.GetButton("Fire1") /*|| _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"); //print("trigger shoot animation"); } private void StopShooting() { _audioSource.Stop(); _particle.Stop(); } public void Shoot() { //print("shoot called"); _timer = 0; Ray ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));//_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>(); if (enemyMovement != null) { enemyMovement.KnockBack(); } /*if (health != null) { health.TakeDamage(1); }*/ } /*else { _lineRenderer.SetPosition(1, ray.GetPoint(Range)); }*/ } // 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(); print("game over called"); } } I’ve kept what I commented out, here’s the clean version of our script. using UnityEngine; using System.Collections; public class PlayerShootingController : MonoBehaviour { public float Range = 100; public float ShootingDelay = 0.1f; public AudioClip ShotSfxClips; public Transform GunEndPoint; private Camera _camera; private ParticleSystem _particle; private LayerMask _shootableMask; private float _timer; private AudioSource _audioSource; private Animator _animator; private bool _isShooting; void Start () { _camera = Camera.main; _particle = GetComponentInChildren<ParticleSystem>(); Cursor.lockState = CursorLockMode.Locked; _shootableMask = LayerMask.GetMask("Shootable"); _timer = 0; SetupSound(); _animator = GetComponent<Animator>(); _isShooting = false; } void Update () { _timer += Time.deltaTime; if (Input.GetButton("Fire1") && _timer >= ShootingDelay) { Shoot(); if (!_isShooting) { TriggerShootingAnimation(); } } else if (!Input.GetButton("Fire1")) { StopShooting(); if (_isShooting) { TriggerShootingAnimation(); } } } private void TriggerShootingAnimation() { _isShooting = !_isShooting; _animator.SetTrigger("Shoot"); } private void StopShooting() { _audioSource.Stop(); _particle.Stop(); } public void Shoot() { _timer = 0; Ray ray = _camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); RaycastHit hit = new RaycastHit(); _audioSource.Play(); _particle.Play(); if (Physics.Raycast(ray, out hit, Range, _shootableMask)) { print("hit " + hit.collider.gameObject); EnemyMovement enemyMovement = hit.collider.GetComponent<EnemyMovement>(); if (enemyMovement != null) { enemyMovement.KnockBack(); } } } private void SetupSound() { _audioSource = gameObject.AddComponent<AudioSource>(); _audioSource.volume = 0.2f; _audioSource.clip = ShotSfxClips; } public void GameOver() { _animator.SetTrigger("GameOver"); StopShooting(); print("game over called"); } } Looking through the Changes We removed a lot of the code that was part of the reloading system. We basically removed any mentions of our ammo and reloading, however, I kept the changes involved with the shooting animation, shooting sound effects, and shooting rate. There were only 2 changes that were made: I changed the input we use to shoot from GetMouseButton to GetButton(“Fire1”), I believe this is the same thing, but I’m making the change anyways. Either option returns true when we’re touching the screen on our mobile device. I also changed our Ray from our raycasting system. Before casted a ray from where our mouse was located at, which before we fixed at the center. However, after we got rid of the code that fixed cursor to the middle, we needed a new way to target the middle. Instead of firing the raycast from our mouse, we now fire the raycast from the middle of our camera, which will fix our problem with our mobile device. Go ahead and play the game now. We should be able to have a playable game now. There are 2 things that will happen when we shoot: We’ll shoot a raycast and if it hits the enemy, they’ll be pushed back The enemies trigger event will detect that we clicked down on the enemy, so they’ll take some damage At this point, we have a problem: if we were to hold down the screen, we’ll push the enemy back, but they’ll only be hit once! That’s because we only have that deals with an OnClick event, but not if the user is currently selecting them. We’re going to fix this problem tomorrow, but I’ve done a lot of investigation work with raycasts now and want to take a break! Step 2.4: Changing the ScreenManager script One more thing we need to do before we leave. The Unity compiler would complain about a missing reference with our ScreenManager, specifically with the MaxAmmo variable that we got rid of. Let’s just get rid of it: using UnityEngine; using UnityEngine.UI; public class ScreenManager : MonoBehaviour { public Text AmmoText; void Start() { { PlayerShootingController shootingController = Camera.main.GetComponentInChildren<PlayerShootingController>(); //UpdateAmmoText(shootingController.MaxAmmo, shootingController.MaxAmmo); } } public void UpdateAmmoText(float currentAmmo, float maxAmmo) { AmmoText.text = currentAmmo + "/" + maxAmmo; } } And we’re good to go! Technically speaking, we won’t be using this script anymore either. Conclusion And another day’s worth of work has ended! There’s a lot of things I learned about VR, such as: we don’t need ANYTHING that the Google VR SDK provides! Unity as a game engine already provides us with everything we need to make a VR experience. Google’s SDK kit is more of a utility kit that help make implementation easier. The TLDR I learned today is that we don’t have to be fixed on using Unity’s Raycasting script, we don’t need it. We can continue to use what we already have. However, for the sake of learning, I’m going to continue down re-implementing our simple FPS with the Google Cardboard assets! We’ll continue tomorrow on Day 39! See you then! Day 37 | 100 Days of VR | Day 39 Home
  5. Writers Note: Sorry for the lack of updates, I'm not dead yet! I went on a short vacation and got horribly sick. I'm hoping to continue as I have been going before. Welcome to Day 37! Today, things are finally going to get serious in working with VR! Currently, there are a lot of problems with the app from when we launch it. Some are just limitations and others are actual problems. However, before we start to go in and fix everything that we encountered yesterday, today we’re going to add the Google VR SDK into our game. Today we’re going to: Set up wireless debugging so we can both: debug and receive console print statements from our game Remove the old scripts that we’ll replace with VR Add the Google VR SDK into our game Set up a simple Event Trigger Today, we’re going to start working in VR, so let’s not waste time and get to it! Step 1: Setting Up Wireless Debugging/Consoles Before we do anything, one of the most important thing we should do is to set up remote debugging or at the very least, the ability to have our console statements be sent to Unity untethered. Currently, in Development mode, we only get console logs from our Android device if our phone is connected to our computer. This wire would become too limiting if we must do something like spin around in a circle. To fix this, we’re going to set up wireless debugging where our phone can send data remotely to Unity. We’re going to follow Unity’s documentation on attaching a MonoDevelop Debugger to an Android Device. The instructions are straightforward, so I’ll just leave the link to the instruction. In our current state, because we have no way of restarting the game, we must rebuild and run every single time we want to see the console wirelessly. The reason being we lost the ability to restart the game inside the game. However, when we re-add our ability to restart the game, wireless debugging will be more useful. Step 2: Removing Old Scripts that Needs VR Replacements It’s finally time to start working in Unity! Before we do anything, let’s think about the things that the Google VR SDK gave us and what must we get rid of in our current system that conflicts with the VR SDK. The main thing that the Google VR SDK provides is: The ability to move the camera with our head Its own Raycasting system What we need to remove from our game is: The ability to move our character The ability to move our camera The ability to shoot The crosshair UI Luckily for us, this process is going to be fast and easy. First, let’s remove our ability to move: In our game hierarchy, select the Player game object. Select the little cog on the top right-hand corner of the Player Controller (Script) and select Remove Component Next, let’s get rid of the game following our mouse. Select Player > Main Camera Remove our Mouse Camera Controller (Script) Component After that, let’s get rid of the shooting script. We’re going to come back later and re-purpose this script, but that’s going to be for a different day: Select Player > Main Camera > MachineGun_00 Disable the Player Shooting Controller (Script) We’re still going to need this. Finally, let’s get rid of the crosshair. As you recall, when we add the VR SDK, we get a gaze prefab that already adds a cursor in for us. Select HUD > Crosshair and delete it from our hierarchy. When we’re done, we’ll have a completely unplayable game! Yay…. Step 3: Adding the Google VR SDK in Recalling from the Google Cardboard demo, for our game, we’ll need to add: GvrEditorEmulator – to simulate head movement GvrEventSystem – to use Google’s Event System for dealing with raycasting GvrReticlePointer – for our gaze cursor GvrPointerPhysicsRaycaster – The Raycaster that GoogleVR uses to hit other objects The set up for this will also be very straightforward. Drag GvrEditorEmulator in Assets > GoogleVR > Prefabs > GvrEditorEmulator to the hierarchy Drag GvrEventSystem in Assets > GoogleVR > Prefabs > EventSystem to the hierarchy Drag GvrReticlePointer in Assets > GoogleVR > Prefabs > Cardboard to be the child of Main Camera Selectcs from Assets > GooglveVR > Scripts > GvrPointerPhysicsRaycaster.cs and attach it to our Main Camera. When we’re done, we’ll have something like this: Now with these prefabs and scripts in, we can rotate and look around our game by holding Alt. We can also shoot our raycasts with our VR Raycaster, however right now we don’t have an Event Trigger set up in our enemies that will detect them getting hit. Let’s do that! Step 4: Setting Up an Event Trigger Before we end today, I want to make a simple event trigger that allows us to be able to defeat an enemy. Luckily for us, we already have the function available to us! Specifically, inside our Enemy Health script, we have a code that we call to damage an enemy. Let’s set this up. We want to get something like this: For now, we’re only going to change our Knight enemy. Here’s what we’re going to do: Select our Knight prefab in Assets > Prefab > Knight Add an Event Trigger Component to our prefab. Click Add New Event Type to select what type of event we want to listen for Select PointerClick Now click + to add the object we want to access the scripts of. Drag our Knight Prefab into the empty Object slot Then we need to select the function to call: EnemyHealth > TakeDamage(float) Set the float value we pass in as 1 When we play our game now, when our gazer focuses on an enemy and we click, we’ll shoot him! There are a lot of things that we’re missing like the push back, but we can start focusing on the rest of that tomorrow! Now let’s do that to the rest of our prefabs: Bandit and Zombie! Conclusion There we have it! Our first dive into doing some work with VR. It turns out right now, there’s a lot less code that needs to be written, instead, a lot of it is just putting prefabs and scripts to the correct location so our game would work. Either way, now we have a game that is playable. Tomorrow, we’re going to discuss what changes that we should do to make a better VR experience. Or at the very least, as good as it was before we try to VR-ify it! Phew, it’s been a long day, I’ll see you all tomorrow on day 38! Day 36 | 100 Days of VR | Day 38 Home
  6. Welcome back to Day 36! Yesterday we set up our mobile device to be able to play VR device and there’s nothing quite like that experience, especially if you’re the one that “made it”. If you’ve made it this far in the journey, but you haven’t tried using the Cardboard or other VR devices, I highly recommend trying it once! Now… with my pitch out of the way, let’s talk about what we’re going to do today! We finally started working in VR, today, we’re going to try and convert our simple FPS into a VR experience. This will be a multi-day progress. Today, I want to: Do some Asset clean ups for our app so we don’t have to spend forever building Set up our Google VR tools Play our game Getting the Project GitHub Repository Before we get started. I realize that not everyone (or anyone) followed me step by step to get to Day 36. In our Simple FPS game, we have reached a complete prototype so I’ve decided to make a Github Repository of our game before we start adding the VR components in. Specifically, after doing some manual cleanup work in Step 1. Now anyone can start following along to build our VR application. You can find my Simple VR First Person Shooter GitHub repository here. Step 1: (FAILED/SKIP) Clean Up Un-Used Assets If you haven’t tried switching to the Android platform in our simple FPS game, you might notice, that it… takes… FOREVER. I’ve tried building the app (and I waited 10+ minutes). Thank goodness for the Unity previewer, I would not know what to do if I had to build my app on my phone every single time! Luckily, I found that Google/Unity does have a solution for this. Google has Instant Preview. Unfortunately, which loads the game into our phone while also playing on the editor. The bad news is that this only appears to be for the Daydream Viewer so I’m going to hold off for now. However, let’s see what we can do to optimize this! When I say optimize, I really mean get rid of our un-used assets! Looking at what we have, we have 1 GB worth of files! That’s not good! IMPORTANT NOTE Unfortunately, this didn’t exactly work. I tried to export all our dependencies and then import it into a new project and there were some problems. It turns out, things like Layers and Tags do not get preserved so if we wanted everything to work, we had to manually add everything back in. Instead, I used the files I exported into a new project as a reference and then manually removed assets from a copy of our project (that’s what I get for not having source control!) Also from testing with a before and after build time, I believe that un-used assets DO NOT improve our build and runtime, so the only useful thing that came out of Step 1 was that I: Cleared some clutter so we can find files more easily now Made the project smaller so people can download it from Github faster, so not a complete loss! Step 1.1: Exporting our Assets Let’s get rid of our old un-used files! How? Well, a quick search on how to get rid of unused assets in Unity. All we need to do is: Select our scenes, which in this case is just Main Right-click and click: “Select Dependencies” Export our assets by going to Assets > Export Package… and save your package somewhere. Warning: This will not export Editor scripts and plugins, which in our current situation, is not a problem. Now at this point, we have 2 choices: Delete everything and re-import the assets that we exported or… Create a new project and import our exported project I’m going to do the 2nd choice and make a new project. Step 1.2: Importing our Exported Assets We’re going to create a new project and import everything we just exported. To do that: Select File > New Project… Call your file whatever you want, but I’m going to call mine First Person Shooter VR And now we’ll have a fresh new Unity project: Now we need to re-import everything we exported. Go to Assets > Import Package > Custom Package and find the .unitypackage that we just created Afterwards, just choose to re-import everything Step 2: Set Up Our VR Tools and Settings The next thing we need to do after we have our new project is that we need to import the Google VR SDK and configure our Unity. I’m going to be lazy and just refer us to Day 35: Setting Up Google Cardboard In Unity. Just do the exact same thing in downloading the SDK and setting up our Unity configuration. Note: In the repo, I already included Google VR SDK 1.100.1 and the necessary changes for Player Settings. I assume the PlayerSettings are project-based and not computer-based, but if it’s not, follow the instructions in Day 35. Step 3: Playing Our Game At this point, we should be almost ready to play our game! At this point, we have: imported the Google VR SDK Switched to the Android platform configured our PlayerSettings to the appropriate settings to run a Google Cardboard app The last thing we need to do that’s specific to our Game Project is that we try to build in Build Settings… we run into a problem to resolve incompatibilities between Color Space and the current settings. To fix this, we just need to change our Color Space from Linear to Gamma. To do that: Go to File > Build Settings > Player Settings > Other Settings > Color Spaces Change Linear to Gamma With this setting in, we’re ready to build our game. To do that, I recommend running the development build to build our app. Contrary to what the name sounds, development build DOES NOT make our build faster, instead it allows us to have access to useful debugging settings gives us access to the Profiler and Script Debugging. Now once you’re ready, let’s build our game! Make sure your phone is connected to your computer. Go to File > Build Settings… Enable Development Build Click Build And Run You can save the APK anywhere. Now enjoy the long wait~! Conclusion That’s it for today! Today we cleaned up a bit of our project and then set up the game so that we can run our app directly from our phone. The build is long and horrendous, which is unfortunate. There are a couple of solutions available, but I’m going to look at them some other day. We can also play the game directly from Unity. If we were to play the game right now, we’ll encounter problems. From a technical limitation: We can’t move We can’t reload anymore To actual technical problems: The enemies are all black color We don’t have any of our UI’s anymore I’ll have to investigate these things and solve them one at a time, but I can finally say, we’re here! We’re finally working in VR! That’s what we’re going to try and tackle the next couple of days. It’s going to be a fun one! Day 35 | 100 Days of VR | Day 37 Home
  7. Yesterday we looked at how we can work with VR and went through the basic demo and understood how everything worked. Today, we’re going to look at how we can install our game directly into the phone. To do everything today, we need to have: A phone that supports Android Level 19 (Kit Kat) A USB to Micro-USB (or Type-C for newer devices) cable (Optional) Google Cardboard Today we’re going to: Install the Android SDK so we can build and run our app Install a USB driver for our computer to detect our phone Set up our phone to be in developer mode Build and Run the demo app into our phone With all that being said, let’s get started! Today we’ll be following Unity’s Android SDK setup guide Step 1: Install the Necessary Android Software Since we’re building our VR app for Android applications, we need the required software to compile, build, and run our app on our phone. Download and install the latest Java SDK to run Android Studio Download and Install Android Studio You might have to restart your computer first for your computer to recognize the new Java SDK that you installed. When we’re done downloading and installing Android Studio (which will take a LONG time), we want to open the SDK Manager. In our new project, we can find our SDK Manager under Configure. Now we’ll get this: Under SDK Platform, select the platform we want to support, in this, case it’s Android 4.4 for Cardboard and Android 7.0 for DayDream, however, I believe if you install the newest version that’ll work for both. Under SDK Tools, install: Android SDK Platform-Tools Android SDK Tools Google USB Driver if you have a Nexus device With all of this, we should now have everything we need to be able to build our game into our Android device. Step 2: Install a USB Driver to Detect our Phone The next part (and probably the part I hate the most) is installing a USB driver that allows our computer to detect our phone. Go to Google’s documentation on where to find the appropriate OEM USB driver for your phone and install it. With any luck, your computer should be able to successfully recognize your phone when you plug it into your computer. If not, then I refer you to Google this problem as there are too many possibilities of what could have gone wrong. Step 3: Change Your Phone to Developer Mode Now our computer can connect to our mobile device, the final thing we need to do is have our phone be in developer mode so Unity (or Android) can create the app and install it on our phone. The instructions to enable Developer Mode varies depending on what your phone is. A quick Google search should give you what you need to enable it. However, the most common approach these days is to: Go to Settings > About phone > Build Number Click build number 7 times to enable Developer Mode Now under Settings, you should find Developer options. Go into Settings > Developer options and turn on USB Debugging Hopefully, with this step completed, we can finally move on to our configurations in Unity! Step 4: Configuring Unity to Build and Run our Android App Now that our phone is ready, it’s time to finally build our game into Unity. Make sure that your phone is connected to your computer In Unity go to File > Build & Run to create an APK file (our app) that will install it on our computer That’s it. Now in the perfect world, that’s it, we’re done. Enjoy our VR game! Unfortunately, there are always problems that we would encounter: Your API is at the wrong level. You’re missing a Bundle Identifier Failed to compile resources with the following parameters: major version 52 is newer than 51, the highest major version supported by this compiler. The 1st and 2nd problem can be resolved easily. The first problem is because we need to make sure that we create a minimum version of Android devices that have the software we need to run our VR application. In Player Settings under Other Settings… in Minimum API Level select API Level 19 for Google Cardboard support and API Level 24 for Google Daydream. If you choose API Level 24, just make sure that your phone can run Daydream! For the second problem, every Android app has a unique identifier that Google uses to identify the app. The error that we’re getting is that Unity is telling us that we’re using the default one and we should change it. In Player Settings under Other Settings… in Package Name change the string to be something else. Just make sure you follow the convention of <companyname>.<appname>. In our case, it doesn’t matter what it is, we can put anything we want. Now for the third and final problem. This one more interesting. Most likely your error is something like this: Failed to compile resources with the following parameters: -bootclasspath "C:/Users/JoshDesktop/AppData/Local/Android/android-sdk\platforms\android-24\android.jar" -d "C:\Users\JoshDesktop\git\Cardboard\Temp\StagingArea\bin\classes" -source 1.6 -target 1.6 -encoding UTF-8 "com\google\android\exoplayer\R.java" "com\google\gvr\exoplayersupport\R.java" "com\google\gvr\keyboardsupport\R.java" "com\google\gvr\permissionsupport\R.java" "com\google\vr\cardboard\R.java" "com\google\vr\keyboard\R.java" "com\Josh\Chang\R.java" "com\unity3d\unitygvr\R.java" warning: C:\Users\JoshDesktop\AppData\Local\Android\android-sdk\platforms\android-24\android.jar(java/lang/Object.class): major version 52 is newer than 51, the highest major version supported by this compiler. It is recommended that the compiler be upgraded. warning: C:\Users\JoshDesktop\AppData\Local\Android\android-sdk\platforms\android-24\android.jar(java/lang/AutoCloseable.class): major version 52 is newer than 51, the highest major version supported by this compiler. What all of this is saying is that our Java is out of date and we need to have at least Java SDK 8.52. In my case, I previously had 8.51 installed and when I installed version 8.52, Unity didn’t pick up on the changes. To fix this: Go to Edit > Preferences > External Tools under Android, select JDK and choose the path to your newest JDK file. For me, on my window machine, it was located at C:\Program Files\Java\jdk1.8.0_152 With all of this done, hopefully, you should be able to successfully build and run the GvrDemo on your phone + Google Cardboard if you have one. Conclusion Hopefully, this was a useful guide to getting your Android device set up to play the scene. Leave a comment if you run into problems and I’ll try to help and update this article with any new information. On a different note, it’s truly amazing playing with VR on our own mobile device. Just playing the VR game from Unity was interesting, but words can’t describe how much more realistic and interesting it becomes until you strap your phone onto your face! I think at this point, we have a good understanding of the basics and what is and isn’t possible with the Google Cardboard now. Tomorrow we’re going to look and see how we can incorporate the VR SDK into our simple FPS game to see how our game fairs in VR! Day 34 | 100 Days of VR | Day 36 Home
  8. Hi, Thanks for the compliment, unfortunately, my current plan is to do mobile VR because it is more accessible to me at the moment. However I do have hopes to either get the Vive/Rift in the future and play around with them although no promises on when that will be.
  9. Now that we have made a conscious decision to work in VR, today I finally had the chance to play around with VR in Unity. Today we’re going to explore setting up and using Google’s VR SDK. You might think that setting up VR would be an extremely complex process, but after going through the process, I can say that starting out is simpler than you would think! Here’s our objective for today: Setting up support for Google Cardboard on Unity Going through Unity’s Tutorial Let’s get started! Step 1: Setting up Google Cardboard on Unity For today, I’ll be going through Google’s documentation for setting up VR on Unity. The nice thing about the Google VR SDK is that we can re-use most of the prefabs and scripts that are used with Google Cardboard and use them with Google Daydream. That’s 2 different platforms for the price of one. Today I’ll be following Google’s official documentation on getting started on Unity. Step 1.1: Install Unity At this point, I’m going to assume that we all have an older version of Unity (5.6+). To support being able to run our VR App, we’re going to need to Android Build Support, if you don’t have that installed already, re-download Unity and during the installation process, choose to include Android Build Support. Step 1.2: Adding the Google VR SDK After we have Unity set up correctly with Android Build Support, we need to get Google’s VR assets. Download Google’s VR SDK here. We’re looking to download the .unitypackage. After we have the package downloaded, it’s time to add it to a Unity project. For our case, we’re going to create a new project to play around with. In Unity create a New Project (File > New Project…) Once inside our new project, import everything from the package that we downloaded. In Unity we can import by going to Assets > Import Package > Custom Package. Step 1.3: Configuring Unity to Run VR Now we have imported everything we need, the last thing to do is to change some of our settings in Unity so we can run our game. Change Our Build Setting to Build and Run Android Applications The first thing we need to do is get Unity to run our game project on an Android platform. Open the Build Settings by going to File > Build Settings. Select Android and hit Switch Platform Wait for the game to finish re-packaging our assets for our new platform Change Our Player Settings to Support VR The next thing we need to do is to change our Player Settings so that we can support the specific VR SDK that we want. In our case, it’s going to be Google Cardboard. In Build Settings, next to Switch Platform, we have Player Settings, select it. In Player Settings, enable Virtual Reality Supported and then add Cardboard to our Virtual Reality SDKs Finally, in Minimum API Level, select API level 19 for the minimum Android version the device the players must have. Google Cardboard requires a minimum of level 19 and the Google Daydream Viewer requires a minimum of level 24. Once we have everything installed, we can finally get started on taking a first look at working with VR! Step 2: Looking Through the Unity Tutorial Now that everything is configured, we can officially start looking through Google’s SDK Basics. I went through the SDK basics while also going through the GVRDemo scene. In our new project go to Assets > GoogleVR > Demos > Scenes and open GVRDemo Google provides prefabs and scripts that will take care of the VR features for you. These are all located in Assets > GooglveVR > Prefab and Scripts. Here’s a breakdown of what they and the script attached to them do: GvrEditorEmulator prefab– Allows us to control our camera like how we might control it with our headset. Hold on to the alt button to rotate your view around the camera. GvrControllerMain prefab – Gives us access to the Daydream controller which we can implement actions with Google’s controller API to interact with the game GvrEventSystem prefab – Enables us to use Google’s input pointer system. Specifically, how our gaze/controller interacts and selects objects. GvrPointerGraphicRacyater script – This script is like a normal Graphic Raycaster that we would attach on to a UI canvas so that we can interact with our UI using our input devices (gaze or controller) GvrPointerPhysicsRaycaster script – This script shoots out a raycast directly in the middle of our screen to select something when we decide to click. We should attach this to our main camera. We must also attach Unity’s event system on each object we want to interact with when we select them. GvrControllerPointer prefab – This is the Daydream’s controller. It gives us an arm asset to imitate our controller. This prefab must the sibling of our Main Camera object where we attached our GvrPointerPhysicsRaycaster GvrReticlePointer prefab – This is the Google Cardboard’s gaze controller. It creates a dot in the middle of our screen which we use to select objects that are in the game. For this prefab to work we must make it a child of the Main Camera game object. There are quite a couple of other prefabs and scripts, but on the high level, these are the basics we’ll need to make a VR game. Let’s see this in action with the GvrDemo scene! Step 2.1: Looking at the demo scene When we open up GvrDemo, here’s what we see: I suggest that you explore around the scene and see the objects in our hierarchy, but on the high-level summary, here’s what we have in our hierarchy that’s relevant to just the Google Cardboard (because it has Daydream assets too) GvrEditorEmulator for us to emulate head movement in VR GvrEventSystem for Unity to detect our VR inputs when we select an object Inside Player > Main Camera, we have our GvrPointerPhysicsRaycaster script which allows us to use Google’s raycasting system for 3D objects Inside the Floor Canvas game object, we have the GvrPointerGraphicRacyate for us to interact with the UI. Finally, inside Player > Main Camera > GvrReticlePointer, we have our gaze cursor for Google Cardboard that we use to interact with the game world. The main point of this game is to click on the cube that appears in the game. When we click on the cube, it’ll be randomly moved somewhere else in the game. The interesting part of all of this is how we can trigger the code with our Gaze. Let’s look at the Cube and Unity’s Event Trigger system. The Event Trigger System is a way for Unity to recognize any action taken on the game object that the Event Trigger is registered onto. An action is something like: OnPointerClick OnPointerEnter OnPointerExit In our example, OnPointerClick will be triggered whenever we click on an object that has the Event Trigger attached to it. Here’s the teleport script: // Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using UnityEngine; using System.Collections; [RequireComponent(typeof(Collider))] public class Teleport : MonoBehaviour { private Vector3 startingPosition; public Material inactiveMaterial; public Material gazedAtMaterial; void Start() { startingPosition = transform.localPosition; SetGazedAt(false); } public void SetGazedAt(bool gazedAt) { if (inactiveMaterial != null && gazedAtMaterial != null) { GetComponent<Renderer>().material = gazedAt ? gazedAtMaterial : inactiveMaterial; return; } GetComponent<Renderer>().material.color = gazedAt ? Color.green : Color.red; } public void Reset() { transform.localPosition = startingPosition; } public void Recenter() { #if !UNITY_EDITOR GvrCardboardHelpers.Recenter(); #else GvrEditorEmulator emulator = FindObjectOfType<GvrEditorEmulator>(); if (emulator == null) { return; } emulator.Recenter(); #endif // !UNITY_EDITOR } public void TeleportRandomly() { Vector3 direction = Random.onUnitSphere; direction.y = Mathf.Clamp(direction.y, 0.5f, 1f); float distance = 2 * Random.value + 1.5f; transform.localPosition = direction * distance; } } We can ignore what the code does, but the important thing that I want to bring attention to are the public functions that are available: SetGazedAt() Reset() Recenter() TeleportRandomly() Where are these called? Well, if you look back at our Event Trigger that’s created in Cube, we set 3 event types: Pointer Enter Pointer Exit Pointer Click Then whenever any of these events occur, we call our public function. In this example, when we look at our cube, we’ll trigger the Pointer Enter event and call the SetGazedAt() function with the variable gazedAt to be true. When we look away, we trigger the Pointer Exit event and call the SetGazedAt() function with gazedAt to be false. Finally, if we were to click on the cube, we would trigger the Pointer Click event and call TeleportRandomly() to move our cube to a new location. Conclusion It’s surprising how un-complex this whole process is so far! I’m sure there are a lot more things to consider once we dive deeper into Unity, however for today, I think the progress we have made is sufficient. Tomorrow, we’re going to look at how we can get the demo app to run on a phone that supports a Google Cardboard (which I assume at this point is 99% of you guys here) Day 33 | 100 Days of VR | Day 35 Home
  10. Side Note: I've been feeling sick recently and progress have been slow, but I'm feeling better and ready to get back to it! Welcome back to day 33! Yesterday, we looked at 3 ways we can save and load data in Unity: with PlayerPrefs, Data Serialization, and saving our data to a server. Today we’re going to use what we learned the previous day to save our score in our simple FPS. Here’s the goal for today: Implement our SaveManager to help us save score Update our UI to show our high score when the game is over So, let’s get started! Step 1: Saving our Data Of the methods we’ve talked about, I’m going to use the PlayerPrefs to help us save our data. While we can technically use our PlayerPrefs anywhere we want between our scripts, it’s better for us to create a manager where we will centralize everything all our Saving/Loading work so that when we need to make changes, we don’t have to comb through all our Script to fix things. Step 1.1: Creating our SaveManager The first step to saving our score is to create our ScoreManager script and attach it to our GameManager game object. Select our GameManager game object in our hierarchy. In the Inspector, click Add Component and create a new ScoreManager In our SaveManager, we want to be able to save and load our high score. Here’s what we’ll have: using UnityEngine; public class SaveManager : MonoBehaviour { private string _highScoreKey = "highscore"; public void SaveHighScore(float score) { PlayerPrefs.SetFloat(_highScoreKey, score); } public float LoadHighScore() { if (PlayerPrefs.HasKey(_highScoreKey)) { return PlayerPrefs.GetFloat(_highScoreKey); } return 99999999999; } } Variables Used For our SaveManager, we only create a string _highScoreKey that we use to store the text that we want to use for our score. We never want to manually type our key in as that might lead to us mistyping and many hours spent debugging over a single spelling mistake. Walking Through the Code Our SaveManager script is only used to help us access our PlayerPrefs in one centralized location. The beauty of this system is that if one day we decide that we don’t want to use PlayerPrefs and use DataSerialization instead, we can just change SaveManager, but everything else that’s using it can stay the same. Here’s the code flow: In SaveHighScore() we save the score that we’re given to our high score. In LoadHighScore() we return the high score that we saved. It’s important to note that if we don’t have a value in our Prefab, we would return 0, however, in our case, a lower score is better, instead we return a very high score. Step 1.2: Modifying our ScoreManager to Expose Our Score Previously, we had our ScoreManager change the text of the score in our page, however, if we want to be able to show our high score at the end of the game. To do that, we need to use the Victory and GameOver panels that we made in the past. Luckily for us, in our GameManager, we already have some code that uses them. Now, for our GameManager to access our time (and save it with our SaveManager), we need to expose the score for other scripts to access them. Here are our changes to ScoreManager: using System; using UnityEngine; using UnityEngine.UI; public class ScoreManager : MonoBehaviour { public Text Score; private string _time; private bool _gameOver; private float _score; void Start () { _time = ""; _gameOver = false; _score = 9999999999; } void Update() { if (!_gameOver) { UpdateTime(); } } private void UpdateTime() { _score = Time.time; _time = ScoreManager.GetScoreFormatting(Time.time); Score.text = _time; } public void GameOver() { _gameOver = true; } public float GetScore() { return _score; } // we can call this function anywhere we want, we don't need to have an instance of this class public static string GetScoreFormatting(float time) { int minutes = Mathf.FloorToInt(time / 60); int seconds = Mathf.FloorToInt(time % 60); float miliseconds = time * 100; miliseconds = miliseconds % 100; return string.Format("{0:0}:{1:00}:{2:00}", minutes, seconds, miliseconds); } } New Variables Used We create a new float _score that we’ll use to keep track of the time that passed for the player. Walking Through the Code Most of the code is the same. We just wrote some new functions, here’s what we did: In Start() we set _score to have a starting value In UpdateTime() we update _score to be the current time in the game. I also moved the code that gave us the time in a nice: minutes:seconds:milliseconds format to a static function called GetScoreFormatting() where we used to set our _time. Notice how I use GetScoreFormatting()? GetScoreFormatting() can be used just like this anywhere else in our game now, even if we don’t have an instance to ScoreManager. GetScoreFormatting() is just a copy and paste of what we originally had in UpdateTime(). Finally, we create a public function GetScore() to get the score the player earned in GameManager when they win That’s it! Now let’s combine everything we’ve worked on to create our high score system. Step 1.3: Use Everything in Our GameManager Now that we have everything we want, let’s use it in our GameManager script! Now we’re going to write some code that allows us to save our when the player wins. Here’s what we’ll have: using UnityEngine; public class GameManager : MonoBehaviour { public Animator GameOverAnimator; public Animator VictoryAnimator; private GameObject _player; private SpawnManager _spawnManager; private ScoreManager _scoreManager; private SaveManager _saveManager; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); _spawnManager = GetComponentInChildren<SpawnManager>(); _scoreManager = GetComponent<ScoreManager>(); _saveManager = GetComponent<SaveManager>(); } public void GameOver() { GameOverAnimator.SetBool("IsGameOver", true); DisableGame(); _spawnManager.DisableAllEnemies(); } public void Victory() { VictoryAnimator.SetBool("IsGameOver", true); DisableGame(); if (_scoreManager.GetScore() < _saveManager.LoadHighScore()) { _saveManager.SaveHighScore(_scoreManager.GetScore()); } } 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 The first thing we did was we got ourselves an instance of SaveManager: _saveManager. Now with our SaveManager in GameManager, we can save and load scores. Walking Through the Changes The code that we’re adding uses our SaveManager to save our score. In Start() we instantiate our SaveManager which is also attached to the GameManager game object. When Victory() is called, we check their current score and then compare it with our high score from our SaveManager if our time is lower than the high score, than we set our score as the new high score With our changes to the GameManager, we now have a working high score system. Now the problem? We have no way of seeing if any of this works! Worry not, that’s going to be the next step of our work! Step 2: Update the Game Over Panels to Show Our Score Now that we have the code to change our high score, we’re going to work on displaying our high score in our UI. To do this, we’re going to make a couple of changes to our script, we’re going to: Change our GameOverUIManager to change the Text that we show. Change our GameManager to get our GameOverUIManager from our game over panels and then set the high score to show when the game is over. Step 2.1: Making Changes to GameOverUIManager If you recall, our GameOverUIManager was created to help us detect when the player clicks on the start over a button in our panel. We’re going to make some changes to change the text in our Panel to also say what our high score is. Let’s get to it! Here are the changes that were made: using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; public class GameOverUIManager : MonoBehaviour { private Button _button; private Text _text; void Start () { _button = GetComponentInChildren<Button>(); _button.onClick.AddListener(ClickPlayAgain); _text = GetComponentInChildren<Text>(); } public void ClickPlayAgain() { SceneManager.LoadScene("Main"); } public void SetHighScoreText(string score, bool didWin) { print(_text.text); if (didWin) { _text.text = "You Win! \n" + "High Score: " + score; } else { _text.text = "Game Over! \n" + "High Score: " + score; } print(_text.text); } } New Variables Used The only new variable that we used is a Text UI that we call _text. Specifically, this is the UI element that we use to tell the player that they won (or lost) the game. Walking Through the Changes The only changes we did was: Instantiate our Text UI in Start(). In the case of our panel, the Text was a child the panel that GameOverUIManager was attached to, so we have to look for the component in our child. Once we have an instance of our text, I created a new SetHighScoreText() that, depending on if we won or loss, change our text to show the high score. One important thing I want to mention. Do you notice the “\n”? \n is an escape character for new line. Which means that in our UI, we’ll see something like: Game Over High Score: 1:21:12 Step 2.2: Calling our GameOverUIManager from GameManager Next up, we want to be able to set the score from our GameOverUIManager from our GameManager. We must make some pretty big changes to our Game Panels that we use. Before we just grabbed the animator component, now, we need that and the GameOverUIManager. Besides that, we just need to call our GameOverUIManager script and set the text with our high score. Here’s what we’ve done: using UnityEngine; using UnityEngine.UI; public class GameManager : MonoBehaviour { public GameObject GameOverPanel; public GameObject VictoryPanel; private GameObject _player; private SpawnManager _spawnManager; private ScoreManager _scoreManager; private SaveManager _saveManager; void Start() { _player = GameObject.FindGameObjectWithTag("Player"); _spawnManager = GetComponentInChildren<SpawnManager>(); _scoreManager = GetComponent<ScoreManager>(); _saveManager = GetComponent<SaveManager>(); } public void GameOver() { DisableGame(); _spawnManager.DisableAllEnemies(); ShowPanel(GameOverPanel, false); } public void Victory() { DisableGame(); if (_scoreManager.GetScore() < _saveManager.LoadHighScore()) { _saveManager.SaveHighScore(_scoreManager.GetScore()); } ShowPanel(VictoryPanel, true); } private void ShowPanel(GameObject panel, bool didWin) { panel.GetComponent<Animator>().SetBool("IsGameOver", true); panel.GetComponent<GameOverUIManager>().SetHighScoreText(ScoreManager.GetScoreFormatting(_saveManager.LoadHighScore()), didWin); } 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 The biggest change is with the GameOverPanel and VictoryPanel. Before these were Animators that we used to show our panel, now we have the game objects themselves because we need to access more than just the animator. New Functions Created We created a new function: ShowPanel(), which takes in the GamePanel that we’re changing the text to and whether or not we won the game. From this information, we play our animator and get the GameOverUIManager and call SetHighScoreText() to change the text. Walking Through the Code Here’s how our new code gets used: Whenever the game is over, either the player lost or won, we would call GameOver() and Victory(). From there, we would disable our game and then call our new function ShowPanel() Depending on whether we won or not, we would pass in the correct Panel and state we’re into ShowPanel() Finally, in ShowPanel(), we would play the animation to show our Panel and call setHighScoreText() from our GameOverUIManager to change the text to display our high score. Step 2.3: Attaching our Panels back into GameManager Now with everything in place, we need to add our GameOverPanel and VictoryPanel, because when we changed them, we got rid of any reference to our previous models. Here’s what to do: Select our GameManager game object from the hierarchy. Look for the GameManager script component, drag our Panels (Victory and GameOver) from HUD in our game hierarchy, and put them in the appropriate slots. With that done, we should have something like this: Step 2.4: Fixing our Text UI Display Now with all of this implemented, we can finally play our game! After winning (or losing) in our game, we’ll get our BRAND-NEW panel: New… right? Wrong! It looks the same as what we had! What happened? It turns out, the text is still there, however, we didn’t have enough space in our Text UI to show the remaining text! This problem can be solved easily. We just need to increase the size of our Text UI in our GameOver and Victory Panel in our hierarchy. The first thing we need to do is reveal our Panels that we hid. In our Panels, find the CanvasGroup component, we previously set our alpha to be 0 to hide our panel. Let’s change that back to 1 so we can see our panel again. Just don’t forget to change it back to 0. In the same Panel, select its child game object, Text. We want to try playing around with the Width and Height field in our Rect Transform component. I ended up with: Width: 300 Height: 80 I also added some new Text for a demo of what to expect. Here’s what we have now: Make sure that you make this change to both our GameOver Panel and our Victory Panel in the hierarchy. Now if we were to play the game, here’s what we’ll have when we win: And when we lose: Don’t ask how I won in 2 seconds. I modified our scripts a bit, okay? Conclusion With all of this, we’re done with day 33! Which means we’re officially 1/3 of the way through the 100 days of VR challenge! Not only that, now that we have this high score system, I’m going to officially call our simple FPS finished! Tomorrow, I’m finally going to start looking more into how to do VR development! Until then, I’ll see you all on day 34! Day 32 | 100 Days of VR | Day 34 Side topic: Choosing a phone and platform to develop VR in Home
  11. Hello and welcome back to day 32! Today is going to be an exciting day. We’re going to work on something that we’ve never seen before in any of the tutorials: Saving and Loading data in Unity! Specifically, we’re going to save and load our high score from our game in Unity. From this great Unity video on how to Save and Load data and my own research, I found 3 ways for us to save data: Using PlayerPrefs Using Data Serialization Using a Server Now with our goals set, let’s get started! Step 1: Saving with PlayerPrefs In the first step, we’re going to look at some of the possible ways we can save and load data. From this great Unity video on how to Save and Load data and my own research, I found 3 ways for us to save data: Note: for step 1, none of the code will be used in our actual game. In fact, we’re going to make a separate script just for this. Step 1.1: Using PlayerPrefs The first method is PlayerPrefs. PlayerPrefs is a class that we call that allows us to save and load simple data (think numbers and string) across multiple game sessions. We can think of PlayerPrefs as a dictionary/table object that takes in a key/value pairing. For those who haven’t worked with something like this before, what this means is that PlayerPrefs is an object that we can store specific values to a string and when we want to get that value back, we can give it the string. We’ll see how this works: In our GameManager game object, click Add Component and create a new TestSaveManager In the code, we’re just going to store a value in and then print it out to our console. Here it is: using UnityEngine; public class FakeSaveManager : MonoBehaviour { void Start () { if (PlayerPrefs.HasKey("FakeScore")) { int savedScore = PlayerPrefs.GetInt("FakeScore"); print("We have a score from a different session " + savedScore); } else { PlayerPrefs.SetInt("FakeScore", 1337); print("We don't have a score yet, so we're adding one in"); } } } New Variables Used None Walking Through the Code PlayerPrefs is a static object that we can just access. Here’s what we did: In Start() the first thing we did was check if we stored a value to our key string: FakeScore. From here 2 things will happen. First Run Through The first time we run the code, we haven’t stored anything yet, so we’ll go to our else statement and set an Int with the value 1337 to our key FakeScore. We also print in our console notifying us that it’s the first time we did that. Now if we run the game for the first time, here’s what we’ll see: Notice that it says we don’t have a score yet? Second Run Through Now after we ran the code for the first time and we added our key/value pair to our PlayerPrefs. If we were to run the code again, we’ll go to the other code path. In the if statement, we’ll get the value we stored inside our key “FakeScore”, which is 1337. We’ll print out 1337. Play our game again and here’s what we get: See how we have a score now? That means our score persisted from our previous game. Neat! Step 1.2: Why use PlayerPrefs Looking at what we’ve done, when do we want to use PlayerPrefs? The benefit of PlayerPrefs is: Fast and easy to use with no setup required to store primitive variables The con of PlayerPrefs: If the data we’re storing is important, the user can easily access the data and change it Why is it insecure you might ask? Well, it turns out that any data that we’re saving is saved in open space on your computer. In a Windows machine, you can find these values un-encrypted in our registry. Here it is: Do you see the 2nd item from the top of the list FakeScore and the value is 1337. Any computer savvy hacker who has knowledge of how Unity works can easily go in and make changes to these values to increase our score. To summarize: Only use PlayerPrefs to store information that doesn’t really affect the game, such as player settings: brightness, audio sound, special effects, etc. Do not use it to store things like high scores or sensitive information like your credit card information. Step 2: Saving with Data Serialization After looking at PlayerPrefs we have another method of storing data and that is using data serialization with the help of BinaryFormatter and FileStream class. How this method works, is that we’re: Taking data (either an object we made serializable, a primitive value, or a JSON) Convert it to numbers Storing it somewhere in our device We’ll discuss the details further down. Step 2.1: Using Data Serialization I’ve made some changes to our FakeSaveManager: using System.IO; using System.Runtime.Serialization.Formatters.Binary; using UnityEngine; public class FakeSaveManager : MonoBehaviour { void Start () { /*if (PlayerPrefs.HasKey("FakeScore")) { int savedScore = PlayerPrefs.GetInt("FakeScore"); print("We have a score from a different session " + savedScore); } else { PlayerPrefs.SetInt("FakeScore", 1337); print("We don't have a score yet, so we're adding one in"); }*/ SaveWithSerializer(); LoadWithSerializer(); } private void SaveWithSerializer() { BinaryFormatter formatter = new BinaryFormatter(); print("Save location: " + Application.persistentDataPath); FileStream fileStream = File.Create(Application.persistentDataPath + "/save.dat"); formatter.Serialize(fileStream, 1337); fileStream.Close(); } private void LoadWithSerializer() { BinaryFormatter formatter = new BinaryFormatter(); FileStream fileStream= File.Open(Application.persistentDataPath + "/save.dat", FileMode.Open); print("serializer got back: " + formatter.Deserialize(fileStream)); fileStream.Close(); } } New Variables Used None Walking Through the Code The first thing you might notice is that I commented out the PlayerPrefab code. We don’t need that anymore, we’re just going to focus on the serialization part. Before we walk through the code, let’s talk about these individual classes that we’re using. FileStream We can think of FileStream is a class that allows us to interact with files. In this case, use File.Create() and File.Open() to give us access to these files that we want to access. Notice how we use something called Application.persistentDataPath when we create and open a file? persistentDataPath is a file location in our computer/phone that we can use to store our game files. Each path is different depending on the OS that the game is running on. On a windows machine, here’s where the file would be saved to: Notice how we called our file “save.dat”, that’s just what we call it, the file can be any format we want and it’ll still function the same way. After we set up our FileStream, we will give it to our BinarySerializer so that it can use it to store our data. It’s also important to remember that when we’re done using a FileStream, we should always call Close() to prevent a memory leak. BinaryFormatter We can think of the BinaryFormatter class as a useful utility that takes in a FileStream and reads/write data into the file. In our specific example, I put the float 1337 inside, but we can put in classes that we create ourselves. However, if we were to create our own object, we must make sure we tell Unity to serialize it (by putting [Serialize] on top of the class name). The two important function we have to know is Serialize() and Deserialize(). Serialize is how we change our data into binary and then store them in our File Deserialize is how we read the data back from binary. If we were to open the file that we saved data into, here’s what we have: Now that we understand everything used, let’s walk through the code: In Start(), we call SaveWithSerializer() which uses a BinaryFormatter and a FileStream to help us store our data, which in this case is just a float value. We create a new FileStream with the path to where we want to save our data to. We use Application.persistentDataPath to give us a location to store our file that changes depending on what OS is running our game. This is Unity making sure we put our save files in the right location so we can access it back later no matter what platform we’re using. After we have our FileStream we would use the BinaryFormatter to Serialize our data to the path we specified. After that, make sure to close our FileStream to prevent memory leaks! After we save, we call LoadWithSerializer() we do a similar operation where we create a BinaryFormatter and a FileStream to the location of the file we previously saved. With these two objects, we Deserialize, which means we unencrypt our data and put it back into a usable form for us. At this point, we have our data back. Step 2.2: Why Use Data Serialization? At this point, you might guess why we might want to use Data Serialization: Our data is encrypted so it’s harder for the players to find it and modify the file. In our example, we only store one variable, but we can actually save a lot more than just that However, just like the PlayerPrefab, the con is: We know the location of the file, and just because it’s in binary, it doesn’t mean it can’t be reverse engineered! When should we use Data Serialization? The answer: almost everything! Data serialization can be used as a database for our equipment, magic spells, and more! We can also use it to store information about our player character. My only caveat is that if the game you’re creating is multiplayer where you don’t want players to have an unfair advantage, then you DO NOT want to save data about the player locally. We can store information that the game references where we don’t modify, such as stats of an equipment locally, however information like player inventory is best stored somewhere else. Where is that somewhere else? Good question, that’s the topic of our next method. Step 3: Saving Data to A Server The final and last way for us to store data for our game are to save the data to an external web server which we control and access. From the external server, we would keep track of the state of the player, for example: What level they are How much money they are What items they have in their inventory All information that is changeable by the player should be saved externally in a server, where we can do verifications to make sure the player is what they say they are. Step 3.1: Using a Server I’m not going to create a server, but rather discuss how we would do it if we wanted to. The first thing we must do is host a web server online somewhere. Once the server is up, from our game, ie the client, we would make HTTP network request to our server passing around whatever data our server requires. A general flow of what we would expect is that: Whenever we’re ready to save our data, we would send the data to our server Whenever we want to load our data, we would make a request to our server for our data. When sending data over the network, we can’t just send our whole class over, our web server have no idea what anything is. To solve this problem, we’re going to have to convert our objects into a JSON object and then send it across the network. I won’t go too deep into the details, but a JSON object is basically a string representation of our object that we can easily convert back and forth. We would send this string to our server where we would parse it, validate the data, and then save it in the database. When we’re loading information, the server would send a JSON object back to the game where we would also parse and change the JSON back into something we can use. Step 3.2 Why use a server? The benefit of using this approach to store our data is: Cheating is impossible. Assuming there are no vulnerabilities and we have good data checks, it’ll be very hard for the player to be able to modify their own data and have it saved on our server. Similar to Data Serialization, we can save more complex data than just primitive variables. The cons are: We need a server, which costs money. There’s more overhead work needed. We need to create our own database, know how to set up a server, and deal with storing multiple players instead of one. The big question here is: when should we use a server? The answer: In any game that has multiplayer that we don’t want players to get an unfair advantage against each other. If not, there should be no problem to storing everything locally. Conclusion Today, we explored the options that are available to us to save data in a game. The 3 ways we explored are: PlayerPrefs Data Serialization Sever Most likely, in a real game, we would use all 3, but the best way for me to summarize when to use each is: Use PlayerPrefs to store player setting information that doesn’t affect the game itself. Use Data Serialization to store database information that we will only reference, but not change Use a Server when we have a multiplayer game where we want to ensure no one can cheat by sending all player data to a server where we can do validation on the authenticity of the data. For a single player game, however, we can save the data in a serialized file or if it’s simple enough, even in our PlayerPrefs. Now that we spent Day 32 looking at saving data, it’s finally time for us to start using it on Day 33! I’ll see you all next time to start putting everything we learned into action! Source: Day 32 Visit the 100 Days of Unity VR Development main page. Visit our Homepage
  12. 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
  13. 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
  14. 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!
  15. 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
  • Advertisement