We’re so close to finishing our First Person Shooter game! Today, it’s day 41 and today we’re going to put in the final finishing pieces of our game together.
Today, our game plan is to:
- Put our Game Over and Victory panels back into the game
- Fix our Animation
- Connect our UI with the rest of our script
After today, we will have everything our original simple FPS shooter has in VR!
Let’s get started!
Step 1: Getting Our End State Panels to Show Up
The first thing we need to do is that we need to get our victory and game over panels to show up.
Before we had the panel drop down on our screen, however, because we’re in VR now, that wouldn’t be good for the users.
Instead, what we want to do is have the panel show up in front of our user, but as we turn around, the UI wouldn’t follow us.
I’m going to keep the panel the same as it was, we just need to change the scale so that the panel would fit on our screen.
I’ve experimented with what scale I want the panels to be by manually moving our Game Over panel (which conveniently also had a Canvas
component) to in front of the player and playing around with the scale until we found something good.
Here’s what I did (optional):
- In Game Over under our HUD, I changed the Canvas to be World Space
- I manually made the rotation to the same as our
Player
game object - Then I played with the scale and positioning a bit to get something that looks good. Don’t worry too much about positioning, we’ll write some code later that’ll make this appear perfectly every time. My ending scale value was (0.01, 0.01, 0.01)
Here’s what we see:
So great, now we have our Scale, it’s time to use our panel!
Step 1.1: Creating the Prefabs
At this point on, unless stated otherwise, anything we do with the Game Over panel, we do the same thing with the Victory panel.
So now, we have a general sense of the size we want our panels to be, Scale (0.01, 0.01, 0.01), it’s time for us to use it.
The best solution for us to create this is for us to instantiate our panels.
If you recall, if we were to call Unity’s instantiate function, we can create a new Game Object with our own pre-determined directions… like in front of our player! To do that, we need a prefab!
- Select
GameOver
in HUD
- Change the
Scale
to be (0.01, 0.01, 0.01)
- Drag
GameOver
into our Prefabs
- Delete
GameOver
from HUD
After we have done this with both GameOver
and Victory
, we can delete HUD
. We don’t need it anymore.
Next, we need somewhere to Instantiate our prefabs. Now where in our knows whether we won or lost… Right! In our Game Manager script attached to our Game Manager game object!
Step 1.2: Writing the Code to Show Our Panels
Luckily, our Game Manager script already takes in our game panels as objects, which means that we can re-purpose our existing script to accomplish what we want to do.
If we recall what we did with our original GameManager
script, we take our Panel
s that we made invisible by changing the alpha of it to be 0
.
However, because of our new limitations, we can’t just hide these panels in our overlay anymore, they have to be in World Space now.
To do this, we’re going to take in a prefab of our panels and then Instantiate them in front of our player when we need to display them.
Luckily for us, when we wrote our GameManager
code, I want to say we organized the code well. We call a function to show both our game over and victory panel. We just have to re-purpose that function a bit to meet our needs.
Here are our changes to GameManager
:
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;
private Camera _camera;
void Start()
{
_player = GameObject.FindGameObjectWithTag("Player");
_spawnManager = GetComponentInChildren<SpawnManager>();
_scoreManager = GetComponent<ScoreManager>();
_saveManager = GetComponent<SaveManager>();
_camera = Camera.main;
}
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)
{
Vector3 frontOfPlayer = _camera.transform.position + _camera.transform.forward * 3;
Quaternion playerRotation = _camera.transform.rotation;
GameObject newPanel = Instantiate(panel, frontOfPlayer, playerRotation);
newPanel.GetComponent<Animator>().SetBool("IsGameOver", true);
newPanel.GetComponent<GameOverUIManager>().SetHighScoreText
(ScoreManager.GetScoreFormatting(_saveManager.LoadHighScore()), didWin);
}
private void DisableGame()
{
PlayerShootingController shootingController =
_player.GetComponentInChildren<PlayerShootingController>();
shootingController.GameOver();
shootingController.enabled = false;
_scoreManager.GameOver();
}
}
New Variable Used
In our code, we introduced a new variable, our Main Camera: _camera
, the purpose of this variable is for us to get the direction the player is facing so we can show our panels in front of the player.
Walking Through the Code
We didn’t really have to change much of the code.
- In
Start()
, we initialized some of our _camera
variable. - In
ShowPanel()
, we don’t just play the animation of our panel to have it show up. Now, we want to create a new instance of it that is directly in front of the player. We get this information from our camera and we move the panel in front of our player. It’s important to note is that our rotation has to be exactly the same as the players otherwise, the panel will be facing the wrong direction. - Besides, that in
DisableGame()
, we did some cleanup work on the classes that we got rid of. The really great thing is how easy it is to do what we want with so little code change!
Step 1.3: Adding the Prefab to the GameManager Script
However, before we’re done with this, we need to add panel prefabs to the GameManager
script so we know which panel we should show.
To do this:
- Go to the
GameManager
game object in the hierarchy. - In the
GameManager
script, we have 2 open slots: Game Over Panel and Victory Panel, what we want to do now, is that our GameOver
and Victory
panel to the appropriate slot.
We should have something like this when we’re done:
Now, we play our game and when we win or lose, we experience a new problem: the panels don’t show up!
Step 1.4: Getting Our Animation to Appear In Front of the Player
If we look at our hierarchy, we can see that our panel does get created, but when we see where it is:
We find that it’s in some far-off location.
Why does this happen? After some investigation, it turns out that this happened, because of our animation.
Specifically, it’s because of our animation that changed our Z anchor value. This became apparent when looking at the Position of our Panel. Only the Z position is 0 while everything else has a value.
What did we change when we were working on a flat UI again? That’s right, our Z position in our animation!
For simplicity, let’s just get rid of our position transformation.
What I found out is that if we want to change our animation clips, we have to first drag a game
object that uses it to our hierarchy.
- In Assets > Prefab, drag our
GameOver
prefab into our hierarchy - With our
GameOver
game object selected, go to the Animation
Tab (Window > Animation) - Delete our Z-anchor position animation
When we’re done, we should just have something like this:
And the best part is because both our game over and victory panels use this same Game Over animation clip, both panels would receive this change.
- Remove our
GameOver
game object from our hierarchy
Now play the game and if we win or lose, we’ll have something like this:
So that’s great, however, our high score is missing! If we look at our error log, it’ll tell us where the problem is. It’s inside SetHighScoreText()
that is part of our GameOverUIManager
script that we attached to each of our panels.
After looking at the problem, it appears that we’re crashing when we’re trying to print our text in SetHighScoreText()
.
For some reason, our objects in Start()
aren’t being instantiated.
To test this theory, I added a print
statement into my Start()
to see what happens. It turns out that the ordering of our function calls is:
SetHighScoreText()
Start()
I looked into this problem of instantiated prefabs not running start function and found out that Start()
is called by the first Update()
.
For us, after we create our panel game object, we’re immediately calling our function. We don’t even have time to call Start()
.
There were a couple of ways we could have solved this:
- Instantiate our
Text
when we call SetHighScoreText()
- Change
Start()
to Awake()
From looking at our StackOverflow answer, the correct way of solving this problem is to not instantiate our game objects in Start()
, rather we should do it inside Awake()
which gets called immediately after our game object gets created.
Here’s our code now for GameOverUIManager
:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameOverUIManager : MonoBehaviour
{
private Button _button;
private Text _text;
void Awake () {
_button = GetComponentInChildren<Button>();
_button.onClick.AddListener(ClickPlayAgain);
_text = GetComponentInChildren<Text>();
print("text in awake: " + _text.text);
}
public void ClickPlayAgain()
{
SceneManager.LoadScene("Main");
}
public void SetHighScoreText(string score, bool didWin)
{
print("before text: " + _text.text);
if (didWin)
{
_text.text = "You Win! \n" +
"High Score: " + score;
}
else
{
_text.text = "Game Over! \n" +
"High Score: " + score;
}
print("after text: " + _text.text);
}
}
Walking Through the Code
The only change we made here is that we changed our initializing code from Start()
to Awake()
.
By making this change, we actually initiate our code and behold what we get now when we play the game:
Step 1.5: Interacting With Our Panels
Luckily for us, interacting with our UI is VR is not any different compared to what we’re doing now.
As of now, our game is already complete and we can shoot the GameOver
/Victory
panel to restart the game and the code would work flawlessly.
However, to re-iterate on what needs to be done:
- We need to go to the prefabs of our panels in Assets > Prefabs
- We need a Graphics Raycaster on our canvas so we can detect when the user is interacting with our UI
- Finally, we have 2 options, in our code, create an
onClick
command to call a specific function everytime the click action has been done. The other option is to do the exact same thing, except in the Button
script component
With all of this done and plugged in (it already should be), we now have a full and complete game!
Conclusion
Phew, it’s been a long journey, but we finally did it! 41 days and we now have a fully functional game in VR!
Today, we integrated everything that was needed for the UI for our game. Specifically, we made it so that we can display the GameOver
/Victory
UI in front of the player wherever they are and then interact with the panel when they’re done.
I think to start from Day 42, I’ll be looking to see what it takes to implement an actual arm device to move our weapon in the screen as opposed to using the cursor in the center as a crosshair.
It’s starting to get more interesting, I’ll see all tomorrow!
Day 40 | 100 Days of VR | Day 42
Home
CodeProject
The post Day 41 of 100 Days of VR: Creating a VR First Person Shooter IV – Game Over Panels appeared first on Coding Chronicles.