The core gameplay of your space shooter is done, but it’s not a complete game yet. In order to be playable outside of Unity, you’ll need to add menus and other controls that let the player navigate around the game as an application. Finally, we’ll polish up the game by replacing the temporary art with higher-fidelity 3D models and materials.
Right now, the gameplay is entirely constrained to the editor’s Play button. When you start the game, you’re in the action immediately, and if your space station is destroyed, you have to stop the game and start it again.
To provide the player with a little more context for their game, we need to add menus. In particular, we need to add an especially important button: “New Game.” If the space station is destroyed, we need a way to let the player start again.
Adding the menu structure to a game goes a long way toward making the game feel complete. We’ll be adding four components as part of the menus:
This screen presents the game’s title, and shows the New Game button.
This screen shows the text “Paused,” and contains a button to unpause.
This screen shows Game Over and the New Game buttons.
This screen contains the joystick, indicators, Fire button, and everything else that the player actually sees while playing the game.
Each of these UI groups will be exclusive—only one will appear at a time. The game will start with the Main Menu, and when you click on the New Game button, the menu will disappear and be replaced with the In-Game UI (in addition to the actual game action being started).
Unity’s UI system lets you test your menu using your computer’s mouse or touchpad. However, you should still test how the menu feels on a real touchscreen as you develop it, such as through the Unity Remote app (see “Unity Remote”.)
The first step in this process is to bring the In-Game UI components together into a single object, so that it can be managed all at once.
Create the In-Game UI container. Select the Canvas object, and make a new empty child object. Name this object “In-Game UI”.
Configure the container. Make In-Game UI’s anchors to stretch horizontally and vertically, and set the left, top, bottom, and right margins to zero. This makes it fill the entire canvas.
Next, we’ll bring all of the existing UI elements together into the container.
Group the game’s UI. Select every child of the canvas except the In-Game UI container, and move it into In-Game UI.
We’ll now start building the other menus. Before we begin, it’s helpful to turn off the In-Game UI, so that it doesn’t distract from the other UI content you’re about to build.
Disable the In-Game UI. Select the In-Game UI object, and disable it by clicking the checkbox at the top-left of the Inspector. When you’re done, it should look like Figure 13-1.
The content of the Main Menu is very simple—it’s a text label that shows the game’s title (“Rockfall”), and a button that creates a new game.
Much like the In-Game UI, the Main Menu will be composed of an empty container object, with all of the UI components that belong to the menu added as a child.
Create the Main Menu container. Create a new empty game object, and make it a child of the Canvas. Name it “Main Menu”.
Make it fill the entire canvas by setting it to stretch horizontally and vertically. Set all of the margin values to zero.
Create the title label. Create a new Text object by opening the GameObject menu, and choosing UI → Text. Make it a child of the Main Menu, and name it “Title”.
Set the anchor of this new Text object to Center Top. Set the Pos X value to 0, and the Pos Y to -100. Set the height to 120, and the width to 1024.
Next, you’ll need to set up the text itself. Set the text’s color to the hex color #FFE99A (slightly yellow-y), the text’s alignment to center, and the text itself to “Rockfall”. Additionally, turn on the Best Fit setting. This will make the text automatically size itself to fit the Text object’s boundaries. Finally, drag the At Night font into the Font Slot.
Create the Button. Create a new Button object, and name it “New Game”. Make it a child of the Main Menu.
Set the anchors of the button to be center top, and set the X and Y position values to [0, -300]. Set its height to 330, and its height to 80.
Set the Source Image of the button to the Button
sprite, and set its Image Type to Sliced.
Select the Text child object, and change its text value to “New Game”. Set the Font as CRYSTAL-Regular, the Font Size to 28, and the Color as 3DFFD0FF
.
When you’re done, the menu should look like Figure 13-2.
The Paused screen shows the text “Paused”, along with a button to unpause the game. To build it, follow the same steps as you did for the Main Menu, but with the following changes:
The container object should be called “Paused”.
The text of the Title object should be “Paused”.
The button object should be called “Unpause Button”.
The text of the button should be “Unpause”.
When you’re done, the Pause menu should look like Figure 13-3.
Disable the Paused container before building the final menu: the Game Over screen.
The Game Over screen shows the text “Game Over”, along with a button to start the game again. The Game Over screen will appear when the space station is destroyed, which will end the game.
Again, follow the same steps as for the Main Menu and Paused screens, but with the following changes:
The container object should be called “Game Over”.
The text of the Title object should be “Game Over”.
The button object should be called “New Game Button”.
The text of the button should be “New Game”.
When you’re done, the Game Over screen should look like Figure 13-4.
All three of these new menus are pretty much identical, and you might be wondering why you’ve done the same work three times. The reason is that you’ll want to customize them later, and breaking them apart now will save you work later.
There’s one last UI component that we need to add to the game, and that’s a way to Pause the game.
The Pause button will appear at the top-right of the In-Game UI, and will signal to the game that the user wants to stop the action for a moment.
To build the Pause button, you first need to create a new Button object, and make it a child of the In-Game UI container object. Name it “Pause Button”.
Set the anchors of the Pause button to the top-right, and set the X and Y position values to [-50, -30]. Set the width to 80, and the height to 70.
Set the Source Image of the Image component to the Button
sprite.
Set the text of the Text child object to “Pause”. Set its font to CRYSTAL-Regular, and its size to 28. Set its color to #3DFFD0FF.
Congratulations! Your UI is all done. However, none of the buttons that you’ve set up work correctly. In order for it all to work, you’ll need to add a Game Manager to coordinate everything.
The Game Manager, much like the Input Manager and the Indicator Manager, is a singleton object. The Game Manager has two main jobs:
Managing the state of the game and the menus, and
Spawning the ship and station
When the game starts, the game will be in an unstarted state. The ship and station won’t be in the scene, and the asteroid spawner won’t be creating an asteroids. Additionally, the Game Manager will display the Main Menu container object, and hide every other menu.
When the user taps the New Game button, the In-Game UI will be displayed, the ship and station created, and the asteroid spawner will be told to start creating asteroids. Additionally, the Game Manager will set up some important elements of the game: the Camera Follow
script will be told to follow the new Ship object, and the Asteroid Spawner will be told to aim its asteroids at the Space Station.
Finally, the Game Manager will handle the Game Over state. You might recall from earlier that the DamageTaking
script has a checkbox called “Game Over On Destroyed”. We’ll be setting that up to instruct the Game Manager to end the game whenever the object that the script is attached to is destroyed, if the checkbox is on. Ending the game is simply a matter of turning off the asteroid spawner, and destroying the current ship (and the station, if it happens to still be around).
Before we get started building the Game Manager, we need to be able to create multiple copies of the Ship and the Station. This requires turning both of these objects into prefabs, and also defining the positions at which they’ll both appear.
Turn the Ship and Space Station into prefabs. Drag and drop the Ship into the Project pane to create the prefab, and then remove it from the scene. Repeat this process for the Space Station.
We’ll now create two marker objects, which will serve as indicators for where the Ship and Space Station should be created when a new game starts. These indicators won’t be visible to the player, but we’ll make them visible to you inside the editor.
Create the Ship position marker. Create a new empty game object, and name it “Ship Start Point”.
Click the icon at the top-left of the Inspector and choose the red label (see Figure 13-5). The object will now appear in the scene view, despite being invisible to the player.
Position the marker where you want the ship to appear.
Create the Space Station position marker. Repeat these steps, this time creating an object called “Station Start Point”. Position it where you want the space station to appear.
With this done, we’re now able to create and set up the Game Manager.
The Game Manager largely serves as a central point for storing critical information about the game, such as references to the current Ship and Space Station, as well as changing the state of important game objects when either a button is clicked or when a DamageTaking
script reports that the game should end.
To set up the Game Manager, create a new empty game object called Game Manager, add a new C# script to it called GameManager.cs, and add the following code to the file:
public
class
GameManager
:
Singleton
<
GameManager
>
{
// The prefab to use for the ship, the place it starts from,
// and the current ship object
public
Gameobject
shipPrefab
;
public
Transform
shipStartPosition
;
public
GameObject
currentShip
{
get
;
private
set
;}
// The prefab to use for the space station, the place
// it starts from, and the current ship object
public
GameObject
spaceStationPrefab
;
public
Transform
spaceStationStartPosition
;
public
GameObject
currentSpaceStation
{
get
;
private
set
;}
// The follow script on the main camera
public
SmoothFollow
cameraFollow
;
// The containers for the various bits of UI
public
GameObject
inGameUI
;
public
GameObject
pausedUI
;
public
GameObject
gameOverUI
;
public
GameObject
mainMenuUI
;
// Is the game currently playing?
public
bool
gameIsPlaying
{
get
;
private
set
;}
// The game's Asteroid Spawner
public
AsteroidSpawner
asteroidSpawner
;
// Keeps track of whether the game is paused or not.
public
bool
paused
;
// Show the main menu when the game starts
void
Start
()
{
ShowMainMenu
();
}
// Shows a UI container, and hides all others.
void
ShowUI
(
GameObject
newUI
)
{
// Create a list of all UI containers.
GameObject
[]
allUI
=
{
inGameUI
,
pausedUI
,
gameOverUI
,
mainMenuUI
};
// Hide them all.
foreach
(
GameObject
UIToHide
in
allUI
)
{
UIToHide
.
SetActive
(
false
);
}
// And then show the provided UI container.
newUI
.
SetActive
(
true
);
}
public
void
ShowMainMenu
()
{
ShowUI
(
mainMenuUI
);
// We aren't playing yet when the game starts
gameIsPlaying
=
false
;
// Don't spawn asteroids either
asteroidSpawner
.
spawnAsteroids
=
false
;
}
// Called by the New Game button being tapped
public
void
StartGame
()
{
// Show the In-Game UI
ShowUI
(
inGameUI
);
// We're now playing
gameIsPlaying
=
true
;
// If we happen to have a ship, destroy it
if
(
currentShip
!=
null
)
{
Destroy
(
currentShip
);
}
// Likewise for the station
if
(
currentSpaceStation
!=
null
)
{
Destroy
(
currentSpaceStation
);
}
// Create a new ship, and place it
// at the start position
currentShip
=
Instantiate
(
shipPrefab
);
currentShip
.
transform
.
position
=
shipStartPosition
.
position
;
currentShip
.
transform
.
rotation
=
shipStartPosition
.
rotation
;
// And likewise for the station
currentSpaceStation
=
Instantiate
(
spaceStationPrefab
);
currentSpaceStation
.
transform
.
position
=
spaceStationStartPosition
.
position
;
currentSpaceStation
.
transform
.
rotation
=
spaceStationStartPosition
.
rotation
;
// Make the follow script track the new ship
cameraFollow
.
target
=
currentShip
.
transform
;
// Start spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
true
;
// And aim the spawner at the new space station
asteroidSpawner
.
target
=
currentSpaceStation
.
transform
;
}
// Called by objects that end the game
// when they're destroyed
public
void
GameOver
()
{
// Show the Game Over UI
ShowUI
(
gameOverUI
);
// We're no longer playing
gameIsPlaying
=
false
;
// Destroy the ship and the station
if
(
currentShip
!=
null
)
Destroy
(
currentShip
);
if
(
currentSpaceStation
!=
null
)
Destroy
(
currentSpaceStation
);
// Stop spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
false
;
// And remove all lingering asteroids from the game
asteroidSpawner
.
DestroyAllAsteroids
();
}
// Called when the Pause or Resume buttons are tapped
public
void
SetPaused
(
bool
paused
)
{
// Switch between the in-game and paused UI
inGameUI
.
SetActive
(!
paused
);
pausedUI
.
SetActive
(
paused
);
// If we're paused..
if
(
paused
)
{
// Stop time
Time
.
timeScale
=
0.0f
;
}
else
{
// Resume time
Time
.
timeScale
=
1.0f
;
}
}
}
The Game Manager script is bulky, but simple. It has two main functions: managing the appearance of the menus and In-Game UI and creating and destroying the space station and ship when the game begins and ends.
Let’s walk through what it does, one step at a time.
The Start
method is called when the Game Manager first appears in the scene—that is, at the start of the game. The only thing that it does is cause the main menu to appear, calling ShowMainMenu
.
// Show the main menu when the game starts
void
Start
()
{
ShowMainMenu
();
}
In order to show any UI, we use a method called ShowUI
that handles the presentation of the desired UI object and the dismissal of all other UI objects. It does this by hiding all UI objects, and then un-hiding the desired UI element:
// Shows a UI container, and hides all others.
void
ShowUI
(
GameObject
newUI
)
{
// Create a list of all UI containers.
GameObject
[]
allUI
=
{
inGameUI
,
pausedUI
,
gameOverUI
,
mainMenuUI
};
// Hide them all.
foreach
(
GameObject
UIToHide
in
allUI
)
{
UIToHide
.
SetActive
(
false
);
}
// And then show the provided UI container.
newUI
.
SetActive
(
true
);
}
With this implemented, ShowMainMenu
can be implemented. All it does is show the main menu UI (via ShowUI
), and indicates to the game that gameplay isn’t currently happening, and that the asteroid spawner should not be spawning asteroids:
public
void
ShowMainMenu
()
{
ShowUI
(
mainMenuUI
);
// We aren't playing yet when the game starts
gameIsPlaying
=
false
;
// Don't spawn asteroids either
asteroidSpawner
.
spawnAsteroids
=
false
;
}
The StartGame
method, which is called when the New Game button is tapped, shows the In-Game UI (which hides the other UI as a result), and sets up the scene for gameplay by removing any existing ship or station, and creating new ones. It also makes the camera start tracking the newly created ship, and tells the asteroid spawner to start throwing asteroids at the newly created station:
// Called by the New Game button being tapped
public
void
StartGame
()
{
// Show the In-Game UI
ShowUI
(
inGameUI
);
// We're now playing
gameIsPlaying
=
true
;
// If we happen to have a ship, destroy it
if
(
currentShip
!=
null
)
{
Destroy
(
currentShip
);
}
// Likewise for the station
if
(
currentSpaceStation
!=
null
)
{
Destroy
(
currentSpaceStation
);
}
// Create a new ship, and place it
// at the start position
currentShip
=
Instantiate
(
shipPrefab
);
currentShip
.
transform
.
position
=
shipStartPosition
.
position
;
currentShip
.
transform
.
rotation
=
shipStartPosition
.
rotation
;
// And likewise for the station
currentSpaceStation
=
Instantiate
(
spaceStationPrefab
);
currentSpaceStation
.
transform
.
position
=
spaceStationStartPosition
.
position
;
currentSpaceStation
.
transform
.
rotation
=
spaceStationStartPosition
.
rotation
;
// Make the follow script track the new ship
cameraFollow
.
target
=
currentShip
.
transform
;
// Start spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
true
;
// And aim the spawner at the new space station
asteroidSpawner
.
target
=
currentSpaceStation
.
transform
;
}
The GameOver
method will be called by certain objects that, when they’re destroyed, end the game. It shows the Game Over UI, stops gameplay, and destroys the current ship and station. Additionally, asteroid spawning is stopped, and all remaining asteroids are removed. Essentially, we’re returning to the initial starting conditions of the game:
// Called by objects that end the game when they're destroyed
public
void
GameOver
()
{
// Show the Game Over UI
ShowUI
(
gameOverUI
);
// We're no longer playing
gameIsPlaying
=
false
;
// Destroy the ship and the station
if
(
currentShip
!=
null
)
Destroy
(
currentShip
);
if
(
currentSpaceStation
!=
null
)
Destroy
(
currentSpaceStation
);
// Stop spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
false
;
// And remove all lingering asteroids from the game
asteroidSpawner
.
DestroyAllAsteroids
();
}
The SetPaused
method is called when either the Pause or Resume buttons are tapped. All it does is manage the display of the pause UI, and stop or resume the flow of time.
// Called when the Pause or Resume buttons are tapped
public
void
SetPaused
(
bool
paused
)
{
// Switch between the in-game and paused UI
inGameUI
.
SetActive
(!
paused
);
pausedUI
.
SetActive
(
paused
);
// If we're paused..
if
(
paused
)
{
// Stop time
Time
.
timeScale
=
0.0f
;
}
else
{
// Resume time
Time
.
timeScale
=
1.0f
;
}
}
With the code written, we can now set up the Game Manager in the scene. Configuring the Game Manager is entirely a matter of connecting objects in the scene to variables in the script:
Ship Prefab should be the ship prefab you just made.
Ship start position should be the ship start position in the scene.
Station prefab should be the station prefab you just made.
Station start position should be the station start position in the scene.
Camera Follow should be the Main Camera in the scene.
In-Game UI, Main Menu UI, Paused UI, and Game Over UI should be their equivalent UIs in the scene.
Asteroid Spawner should be the Asteroid Spawner object in the scene.
Leave Warning UI for now; that’s for the next section.
When you’re done, the Inspector for the Game Manager should look like Figure 13-6.
Now that the Game Manager is set up, we need to connect the various buttons that are in the Game UI to the Game Manager.
Connect the Pause button.
Select the Pause button in the In-Game UI, and click the + button at the bottom of the Clicked event. Drag the Game Manager into the slot that appears, and change the function to GameManager → SetPaused. A checkbox will appear; turn it on. This has the effect of calling the SetPaused
method on the Game Manager, and passing in the boolean value true
.
Connect the Unpause button.
Select the Unpause button in the Paused menu. Perform the same set of steps as for the Pause button, but with one change: turn the checkbox off. This will make the button call SetPaused
with the boolean value false
.
Connect the New Game buttons. Select the New Game button in the Main Menu, and click the + button at the bottom of the Clicked event. Drag the Game Manager into the slot, and change the function to GameManager → StartGame.
Next, repeat these steps for the New Game button in the Game Over screen.
The buttons will now be set up! Before we’re done, there are a few more minor things we need to set up to get the full gameplay experience.
First, we need to make it so that destroying the Space Station ends the game. The Space Station already has the DamageTaking
script on it; we need to make this script call the GameOver
function on the Game Manager.
Add the call to GameOver
in DamageTaking.cs.
Open the file and add the following code:
public
class
DamageTaking
:
MonoBehaviour
{
// The number of hit points this object has
public
int
hitPoints
=
10
;
// If we're destroyed, create one of these at
// our current position
public
GameObject
destructionPrefab
;
// Should we end the game if this object is destroyed?
public
bool
gameOverOnDestroyed
=
false
;
// Called by other objects (like Asteroids and Shots)
// to take damage
public
void
TakeDamage
(
int
amount
)
{
// Report that we got hit
Debug
.
Log
(
gameObject
.
name
+
" damaged!"
);
// Deduct the amount from our hit points
hitPoints
-=
amount
;
// Are we dead?
if
(
hitPoints
<=
0
)
{
// Log it
Debug
.
Log
(
gameObject
.
name
+
" destroyed!"
);
// Remove ourselves from the game
Destroy
(
gameObject
);
// Do we have a destruction prefab to use?
if
(
destructionPrefab
!=
null
)
{
// Create it at our current position
// and with our rotation.
Instantiate
(
destructionPrefab
,
transform
.
position
,
transform
.
rotation
);
}
>
// If we should end the game now, call the
>
// GameManager's GameOver method.
>
if
(
gameOverOnDestroyed
==
true
)
{
>
GameManager
.
instance
.
GameOver
();
>
}
}
}
}
This code makes the object check to see if the gameOverOnDestroyed
variable is set to true
; if it is, the Game Manager’s GameOver
method is called, ending the game.
We also need to make the asteroids inflict damage when they collide. To do this, we’ll add a DamageOnCollide
script to them.
To make the asteroids apply damage, select the Asteroid prefab, and add a DamageOnCollide
component.
Next, the asteroids should display their distance to the space station. This will help the player decide which asteroid is the most important one to go over. We’ll do this by modifying the Asteroid
script to query the Game Manager for the current space station, which is then given to the showDistanceTo
variable of the asteroid’s indicator.
To make the asteroid show the distance label, open Asteroid.cs, and add the following code to the Start
function:
public
class
Asteroid
:
MonoBehaviour
{
// The speed at which the asteroid moves.
public
float
speed
=
10.0f
;
void
Start
()
{
// Set the velocity of the rigidbody
GetComponent
<
Rigidbody
>().
velocity
=
transform
.
forward
*
speed
;
// Create a red indicator for this asteroid
var
indicator
=
IndicatorManager
.
instance
.
AddIndicator
(
gameObject
,
Color
.
red
);
>
// Track the distance from this object to
>
// the current space station that's
>
// managed by the GameManager
>
indicator
.
showDistanceTo
=
>
GameManager
.
instance
.
currentSpaceStation
>
.
transform
;
}
}
This code sets up the indicator to show the distance from the asteroid to the current space station, which helps the player to prioritize the asteroids that are closest to the station.
You’re done!
Play the game. You can now fly around and shoot asteroids, and asteroids will destroy the space station if too many hit it; you can also destroy the space station by shooting at it, and if the station is destroyed, game over!
There’s one last core piece of gameplay that we need to add: we want to warn the player if they’re getting too far away from the space station. If the player goes too far, we’ll show a red warning border around the edges of the screen; if turn around, it’s game over.
First, we’ll set up the UI for the warning:
Add the Warning sprite. Select the Warning texture. Change the texture’s type to Sprite/UI.
What we need to do is slice the sprite, so that it can be stretched over the entire screen without distorting the shape of the corners.
Slice the sprite. Click the Sprite Editor button, and the sprite will appear in a new window. In the panel at the lower-right of the window, set the border to 127 for all sides. This will make the corners not get stretched (see Figure 13-7).
Click the Apply button.
Next, we’ll create the Warning UI. This will simply be an image displayed on the UI, which will be set to stretch over the entire screen.
To set up the warning UI, create a new empty game object, and name it “Warning UI”. Make it a child of the Canvas object.
Set the anchors to stretch horizontally and vertically, and set the margins to zero. This will make it fill the entire canvas.
Add an Image component to it. Make the Source Image of this Image component be the Warning sprite that you just created, and set the Image Type to sliced. The image will be stretched over the entire screen.
With that done, it’s time to code it up.
The boundaries are invisible to the player, which means that they’ll be invisible while editing the game. If you want to visualize the volume in which the player can fly around, you’ll need to use the Gizmos feature again, just like you did for the Asteroid Spawner.
There are two concentric spheres that we care about, which we’ll call the warning sphere and the destroy sphere. Both of these spheres will be centered on the same point, but they’ll have different radii: the warning sphere’s radius will be less than that of the destroy sphere.
If the ship’s position is within the warning sphere, then all is good, and no warning will be visible.
If the ship is outside the warning sphere, then the warning will appear on screen, which will signal to the player that they need to turn around and head back.
If the ship is outside the destroy sphere, the game ends.
The actual checking of the ship’s position will be handled by the Game Manager, which will use the data stored inside the Boundary object (which you’re about to create) to determine whether the ship is outside either of the two spheres.
Let’s get started by creating the Boundary object, and adding the code that visualizes the two spheres:
Create the Boundary object. Create a new empty object, with the name “Boundary”.
Add a new C# script to the object called Boundary.cs, and add the following code to it:
public
class
Boundary
:
MonoBehaviour
{
// Show the warning UI when the player is this far from the
// center
public
float
warningRadius
=
400.0f
;
// End the game when the player is this far from the center
public
float
destroyRadius
=
450.0f
;
public
void
OnDrawGizmosSelected
()
{
// Show a yellow sphere with the warning radius
Gizmos
.
color
=
Color
.
yellow
;
Gizmos
.
DrawWireSphere
(
transform
.
position
,
warningRadius
);
// And show a red sphere with the destroy radius
Gizmos
.
color
=
Color
.
red
;
Gizmos
.
DrawWireSphere
(
transform
.
position
,
destroyRadius
);
}
}
When you return to the game editor, you’ll see two wireframe spheres. The yellow sphere shows the warning radius, and the red sphere shows the destroy radius (as seen in Figure 13-8).
The Boundary
script doesn’t actually do any logic of its own in-game. Instead, the GameManager
uses its data to determine if the player has flown beyond the boundary radii.
Now that the boundary object has been created, we just need to set up the Game Manager to use it.
Add the boundary fields to the GameManager
script, and update GameManager
to use them.
Add the following code to GameManager.cs:
public
class
GameManager
:
Singleton
<
GameManager
>
{
// The prefab to use for the ship, the place it starts from,
// and the current ship object
public
GameObject
shipPrefab
;
public
Transform
shipStartPosition
;
public
GameObject
currentShip
{
get
;
private
set
;}
// The prefab to use for the space station, the place it
// starts from, and the current ship object
public
GameObject
spaceStationPrefab
;
public
Transform
spaceStationStartPosition
;
public
GameObject
currentSpaceStation
{
get
;
private
set
;}
// The follow script on the main camera
public
SmoothFollow
cameraFollow
;
>
// The game's boundary
>
public
Boundary
boundary
;
// The containers for the various bits of UI
public
GameObject
inGameUI
;
public
GameObject
pausedUI
;
public
GameObject
gameOverUI
;
public
GameObject
mainMenuUI
;
>
// The warning UI that appears when we approach
>
// the boundary
>
public
GameObject
warningUI
;
// Is the game currently playing?
public
bool
gameIsPlaying
{
get
;
private
set
;}
// The game's Asteroid Spawner
public
AsteroidSpawner
asteroidSpawner
;
// Keeps track of whether the game is paused or not.
public
bool
paused
;
// Show the main menu when the game starts
void
Start
()
{
ShowMainMenu
();
}
// Shows a UI container, and hides all others.
void
ShowUI
(
GameObject
newUI
)
{
// Create a list of all UI containers.
GameObject
[]
allUI
=
{
inGameUI
,
pausedUI
,
gameOverUI
,
mainMenuUI
};
// Hide them all.
foreach
(
GameObject
UIToHide
in
allUI
)
{
UIToHide
.
SetActive
(
false
);
}
// And then show the provided UI container.
newUI
.
SetActive
(
true
);
}
public
void
ShowMainMenu
()
{
ShowUI
(
mainMenuUI
);
// We aren't playing yet when the game starts
gameIsPlaying
=
false
;
// Don't spawn asteroids either
asteroidSpawner
.
spawnAsteroids
=
false
;
}
// Called by the New Game button being tapped
public
void
StartGame
()
{
// Show the In-Game UI
ShowUI
(
inGameUI
);
// We're now playing
gameIsPlaying
=
true
;
// If we happen to have a ship, destroy it
if
(
currentShip
!=
null
)
{
Destroy
(
currentShip
);
}
// Likewise for the station
if
(
currentSpaceStation
!=
null
)
{
Destroy
(
currentSpaceStation
);
}
// Create a new ship, and place it
// at the start position
currentShip
=
Instantiate
(
shipPrefab
);
currentShip
.
transform
.
position
=
shipStartPosition
.
position
;
currentShip
.
transform
.
rotation
=
shipStartPosition
.
rotation
;
// And likewise for the station
currentSpaceStation
=
Instantiate
(
spaceStationPrefab
);
currentSpaceStation
.
transform
.
position
=
spaceStationStartPosition
.
position
;
currentSpaceStation
.
transform
.
rotation
=
spaceStationStartPosition
.
rotation
;
// Make the follow script track the new ship
cameraFollow
.
target
=
currentShip
.
transform
;
// Start spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
true
;
// And aim the spawner at the new space station
asteroidSpawner
.
target
=
currentSpaceStation
.
transform
;
}
// Called by objects that end the
// game when they're destroyed
public
void
GameOver
()
{
// Show the Game Over UI
ShowUI
(
gameOverUI
);
// We're no longer playing
gameIsPlaying
=
false
;
// Destroy the ship and the station
if
(
currentShip
!=
null
)
Destroy
(
currentShip
);
if
(
currentSpaceStation
!=
null
)
Destroy
(
currentSpaceStation
);
>
// Stop showing the warning UI, if it was visible
>
warningUI
.
SetActive
(
false
);
// Stop spawning asteroids
asteroidSpawner
.
spawnAsteroids
=
false
;
// And remove all lingering asteroids from the game
asteroidSpawner
.
DestroyAllAsteroids
();
}
// Called when the Pause or Resume buttons are tapped
public
void
SetPaused
(
bool
paused
)
{
// Switch between the in-game and paused UI
inGameUI
.
SetActive
(!
paused
);
pausedUI
.
SetActive
(
paused
);
// If we're paused..
if
(
paused
)
{
// Stop time
Time
.
timeScale
=
0.0f
;
}
else
{
// Resume time
Time
.
timeScale
=
1.0f
;
}
}
>
public
void
Update
()
{
>
>
// If we don't have a ship, bail out
>
if
(
currentShip
==
null
)
>
return
;
>
>
// If the ship is outside the Boundary's Destroy Radius,
>
// game over. If it's within the Destroy Radius, but
>
// outside the Warning radius, show the Warning UI. If
>
// it's within both, don't show the Warning UI.
>
>
float
distance
=
>
(
currentShip
.
transform
.
position
>
-
boundary
.
transform
.
position
)
>
.
magnitude
;
>
>
if
(
distance
>
boundary
.
destroyRadius
)
{
>
// The ship has gone beyond the destroy radius,
>
// so it's game over
>
GameOver
();
>
}
else
if
(
distance
>
boundary
.
warningRadius
)
{
>
// The ship has gone beyond the warning radius,
>
// so show the warning UI
>
warningUI
.
SetActive
(
true
);
>
}
else
{
>
// It's within the warning threshold, so don't
>
// show the warning UI
>
warningUI
.
SetActive
(
false
);
>
}
>
>
}
}
This new code makes use of the Boundary class that you just created to check to see if the player has gone beyond either the warning radius or the destroy radius. Every frame, the distance from the player to the center of the boundary spheres is checked; if they’re beyond the warning radius, the warning UI is made to appear, and if they’re beyond the destroy radius, the game ends. If the player is within the warning radius, they’re fine, so the warning radius is disabled. This means that if the player flies outside the warning radius and then returns to safety, they’ll see the warning UI appear and then disappear.
Next, you just need to connect up the slots. The Game Manager needs a reference to the Boundary object you created a moment ago, as well as a reference to the Warning UI.
Congratulations! You’ve now finished setting up the core gameplay of a rather sophisticated space shooter. As you followed along in the preceding sections, you set up a space environment, created spaceships, space stations, asteroids, and laser beams; set up their physics; and set up all of the various logical components that connect them together. On top of that, you’ve created the UI that’s necessary for actually playing the game outside of the Unity editor.
The core of the game is done, but there’s still room for some visual improvements. Because the visuals of the game are quite sparse, we don’t have many visual reference points to give the player a sense of traveling at speed. Additionally, we’ll add a little more color to the game by adding trail renderers to the ship and asteroids.
If you’ve ever played a spaceflight game before, like Freelancer or Independence War, you might have noticed how, when the player flies around, small pieces of dust, debris, and other small objects move past the player.
To improve our game, we’ll add small dust motes that provide a sense of depth and perspective as the player moves past them. We’ll achieve this with a particle system that moves with the player, continuously creating dust particles in a sphere surrounding the player. Importantly, these dust particles will not move relative to the player. This means that, as the player flies, dust particles will appear that the player then flies past. This creates a much better impression of speed in the game.
To create the dust particles, follow these steps:
Drag the Ship prefab into the scene. We’ll be making some changes to the prefab.
Create the Dust child object. Create a new empty game object, and name it “Dust”. Make it a child of the Ship game object you just dragged out.
Add a Particle System component to it. Copy the settings in Figure 13-9 to it.
The critical parts of this particle system are the fact that the Simulation Space setting is World
, and the Shape is a Sphere
. By setting the Simulation Space to World
, the particles will not move with their parent object (the Ship). This means that the Ship will fly right past them.
Apply your changes to the prefab. Select the Ship object, and click the Apply button at the top of the Inspector. This will save your changes to the prefab. We’re not quite done with the ship, so don’t delete it just yet.
You can see the particle system in action in Figure 13-10. Note how it creates a feeling of a field of stars against the relatively smooth colors of the skybox.
The ship is a very simple model, but there’s no reason why you can’t dress it up a little with some special effects. We’ll add two line renderers to the ship that create the effect of engines behind them.
Create a new Material for the trail. Do this by opening the Assets menu, and choosing Create → Material. Name the new material “Trail”, and place it in the Objects folder.
Make the Trail material use an Additive shader. Select the Trail material, and change its Shader to Mobile → Particles → Additive. This is a simple shader that simply adds its color to the background. Leave the Particle Texture empty—it won’t be needed.
Add a new child object to the Ship. Name it “Trail 1”. Position it at (-0.38,0,-0.77).
Add a Trail Renderer component. Make it use the settings in Figure 13-11. Note that the Material it’s using is the new Trail material you just created.
The colors used in the trail renderer’s gradient are:
#000B78FF
#061EF9FF
#0080FCFF
#000000FF
#00000000
You’ll notice that the colors darken toward the end. Because the Trail material uses an Additive shader, this has the result of making the trail fade out.
Duplicate the object. Once you’ve set up the first trail, duplicate it by opening the Edit menu and choosing Duplicate. Move this new duplicate object to (0.38,0,-0.77).
The location for this second trail is the same as the first, but with the X component flipped.
Apply the changes you’ve made to the prefab. Select the Ship object, and click the Apply button at the top of the Inspector. Finally, delete the Ship from the scene.
You’re now ready to test it out! When you fly the ship, two blue lines will appear behind it, as seen in Figure 13-12.
We’ll now apply a similar effect to the asteroids. The asteroids in the game are quite dark, and while they have indicators to help the player keep track of where they are, they could do with a little more color. To improve things, we’ll add a trail renderer to them.
Add an Asteroid to the scene. Drag out the Asteroid prefab into the scene, so that you can make changes.
Add a Trail Renderer component to the Graphics child object. Use the settings you see in Figure 13-13.
Apply the changes to the Asteroid prefab, and remove it from the scene.
The asteroids will now have a bright trail behind them. You can see the full game in action in Figure 13-14.
There’s one last part that we need to add: audio! Even though there’s no sound in real space, video games are seriously improved with the addition of sound. There are three sounds that we need to add: the roar of the ship’s engines, the zap of laser blasts, and the boom of asteroids exploding. We’ll add each one, one at a time.
We’ve included some public-domain sound effects in the book’s files, which you’ll find in the Audio folder.
First, we’ll add a looping sound effect to the spaceship.
Add the Ship to the scene. We’re about to make some changes to it.
Add an Audio Source component to the Ship. Audio Sources are how you make sound happen.
Turn on the Loop setting. We want the engine noises to play continuously as the player flies the ship.
Add the rocket sound. Drag the Engine audio clip into the AudioClip slot.
Save the changes to the prefab. That’s it!
Adding looping sounds is incredibly easy, and gives you a huge amount of overall improvement to the game for very little effort on your part.
Don’t delete the Ship from the scene yet—we’ll add some more to it in a moment.
Adding sound effects to the weapons is a little more complex. We want to play a sound effect every time the weapon fires, which means we’ll need to make the code aware of sound effects.
First, we’ll need to add audio sources to the two weapon points:
Add Audio Sources to the weapon fire points. Select both of the weapon fire points. With both of them selected, add an Audio Source.
Add the Laser effect to the audio sources. Once you’ve done that, turn off the Play On Awake setting—we only want to play sound when we fire a shot.
Add code to play the sound effect when shots are fired. Add the following code to ShipWeapons.cs:
public
class
ShipWeapons
:
MonoBehaviour
{
// The prefab to use for each shot
public
GameObject
shotPrefab
;
public
void
Awake
()
{
// When this object starts up, tell the input manager
// to use me as the current weapon object
InputManager
.
instance
.
SetWeapons
(
this
);
}
// Called when the object is removed
public
void
OnDestroy
()
{
// Don't do this if we're not playing
if
(
Application
.
isPlaying
==
true
)
{
InputManager
.
instance
.
RemoveWeapons
(
this
);
}
}
// The list of places where a shot can emerge from
public
Transform
[]
firePoints
;
// The index into firePoints that the next
// shot will fire from
private
int
firePointIndex
;
// Called by InputManager.
public
void
Fire
()
{
// If we have no points to fire from, return
if
(
firePoints
.
Length
==
0
)
return
;
// Work out which point to fire from
var
firePointToUse
=
firePoints
[
firePointIndex
];
// Create the new shot, at the fire point's position
// and with its rotation
Instantiate
(
shotPrefab
,
firePointToUse
.
position
,
firePointToUse
.
rotation
);
>
// If the fire point has an audio source
>
// component, play its sound effect
>
var
audio
>
=
firePointToUse
.
GetComponent
<
AudioSource
>();
>
if
(
audio
)
{
>
audio
.
Play
();
>
}
// Move to the next fire point
firePointIndex
++;
// If we've moved past the last fire point in the list,
// move back to the start of the queue
if
(
firePointIndex
>=
firePoints
.
Length
)
firePointIndex
=
0
;
}
}
This code checks to see if the fire point that the shot is being fired from has an AudioSource
component. If it does have one, it’s made to play the shot sound.
Save your changes to the Ship prefab, and remove it from the scene.
You’re done. Now, you’ll hear a sound effect every time a shot is fired!
There’s one last sound effect to add: an explosion sound effect, for when explosions appear. This one’s easy: we just need to add an audio source to the explosion object, and set it to play on awake. When an explosion appears, it will automatically play the Explosion sound.
Add an Explosion to the scene.
Add an Audio Source component to the explosion. Drag in the Explosion audio clip, and turn on Play On Awake.
Save your changes to the prefab and remove it from the scene.
You’re now all done. Congratulations! Rockfall is now complete, and in your hands. It’s up to you to decide what to do next with it!
Some ideas:
Maybe a rocket that turns to face its target?
The asteroids are pretty simple, and fly straight at the space station, while nothing actually goes after the player.
When an asteroid hits, add a particle system that emits smoke and flames at the point of impact. It’s not realistic, but that hasn’t stopped us adding any of the other features to the game.