Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game / Unity

Day 38 of 100 Days of VR: Creating a VR First Person Shooter I

5.00/5 (3 votes)
15 Nov 2017CPOL6 min read 5.5K  
Creating a VR first person shooter I

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:

  1. We can’t move
  2. We only have one input which is clicking

Some actual technical problems:

  1. The enemies are all black color
  2. We don’t have any of our UIs 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:

  1. Discussing how to change our game design to accommodate our new limitations
  2. 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 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

As mentioned before, in the Google Cardboard, we have 3 limitations:

  1. We can’t move our characters position
  2. We only have tapping as an input to interact with the game
  3. 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:

  1. Because we can’t move, we should place our character in a more centered location for the enemies to reach us
  2. Because we can no longer run away, we should make the enemies weaker so that we don’t get swarmed
  3. 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).

Image 1

  1. Select Player in our hierarchy.
  2. 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:

  1. Zombie: 4 HP
  2. Knight: 2 HP
  3. Bandit: 1 HP

Here’s how we change our health:

  1. In Assets > Prefabs select our prefabs, in this case, let’s choose Zombie.
  2. In the Inspector, select the Enemy Health (Script) component and change <code>Health to be 4.

Image 2

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:

C#
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.

C#
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:

  1. 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.
  2. 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:

  1. We’ll shoot a raycast and if it hits the enemy, they’ll be pushed back
  2. 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 deal 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:

C#
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 helps 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

The post Day 38 of 100 Days of VR: Creating a VR First Person Shooter I appeared first on Coding Chronicles.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)