In the previous chapter, we discussed the importance of the Official Unity Programmer exam and what benefits it can produce for any developer who is looking to reassure either themselves or others in understanding programming in Unity. We also discussed the building blocks of being a programmer in general and our game's design brief.
As we are programmers working on a game engine, it is likely you will be working for a range of industries. In many of these industries, you will be issued with a technical brief/documentation (well, you should be!) for building an application. With this project, we are making a game, and the game design brief is effectively the blueprint for making this game. In this chapter, we will be applying the majority of our code, game objects, prefabs, and more based on the guidance of the brief and the game framework. We will be reminding ourselves of the brief and game framework during this chapter and will transfer specific information into our code.
With regard to our code, we will be covering the importance of interfaces and scriptable objects to help structure and uniform our code, preventing it from bloating unnecessarily, which we covered in Chapter 1, Setting Up and Structuring Our Project, with SOLID principles. We will also be getting used to the Unity Editor and becoming familiar with game objects, prefabs, and importing three-dimensional models to animate.
In this chapter, we'll be covering the following topics:
IActorTemplate
)ScriptableObject
(SOActorModel
)Player
, PlayerSpawner
, and PlayerBullet
scriptsEnemySpawner
and enemy scriptThe next section will outline the exam objectives covered in this chapter.
Programming core interactions:
Working in the art pipeline:
Programming for scene and environment design:
The project content for this chapter can be found at https://github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_02.
You can download the entire chapter project files at https://github.com/PacktPublishing/-Unity-Certified-Programmer-Exam-Guide-Second-Edition.
All content for this chapter is held in the relevant unitypackage
file, including a Complete
folder that contains all of the work we'll carry out in the chapter, so if at any point you need some reference material or extra guidance, be sure to check it out.
Check out the following video to see the Code in Action: https://bit.ly/3yfWyt5.
Things can get messy quickly in a project if we don't manage our files correctly by placing them in the allocated folders. If you want to structure your folders your own way, or during the book, you decide to stray away from how I'm doing it, that's also fine. Just try and be conscious of your future self or other people working on this project when it comes to finding and organizing files.
Open the project up if you haven't already and create the following folders:
Model
contains 3D models (the player ship, enemies, bullets, and so on).Prefab
holds instances of game objects (these are created within Unity).Scene
stores our first-level scene as well as other levels.Script
contains all of our code.Material
stores our game object materials.Resources
stores assets and objects to load into our game.ScriptableObject
are data containers that are capable of storing large amounts of data.Tip
You should know what a prefab is, as it's one of the main parts of what makes Unity so quick and easy to use. However, if you don't: it's typically your game object with its settings and components stored in an instance. You can store your game objects in your Project window as prefabs by dragging the game object from the Hierarchy window. A blue box icon will be generated following the game object's name, and if you select the prefab in the Project window, its Inspector window details will show all its stored values. If you would like to know more about prefabs, you can check out the documentation at https://docs.unity3d.com/Manual/Prefabs.html.
The following screenshot shows you how to create these folders:
Next, we will create subfolders; we need to do the following:
Prefab
folder, create another two folders, Enemies
and Player
:Resources
is a special folder that Unity recognizes. It will allow us to load assets while the game is running. For more information about the Resources
folder, check the documentation at https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity6.html.
Further Information
It's worth mentioning the StreamingAssets
folder at this point. Even though we aren't using it in this project, it relates well to how similar (and how different) it is to the Resources
folder.
The Resources
folder imports assets and converts them into internal formats that will be compatible with the target platform. So, for example, a PNG file will get converted and compressed when the project is being built.
The StreamingAssets
folder will hold the PNG file, not convert or compress it. For more information on Streaming Assets, check out the following link: https://docs.unity3d.com/Manual/StreamingAssets.html.
Provided in the Technical requirements section is the download link for the GitHub repository of this chapter. Once downloaded, double-click the Chapter2.unitypackage
file, and we will be given a list of assets to import into our Unity project:
Player_ship.fbx
enemy_wave.fbx
The following screenshot shows the Import window of the assets we are about to bring into our project:
Make sure all assets are ticked and click the Import button at the bottom-right corner of the window. We can now move on to organizing our files and folders in the Project window in the next section.
In this section, we are going to create three prefabs: the player, the player's bullet, and the enemy. These prefabs will hold components, settings, and other property values for our game that we can instantiate throughout our game.
Let's start by making our player_ship.fbx
file into a prefab instance by doing the following.
Sometimes, when importing any three-dimensional file, it may contain extra data that we might not need. For example, our player_ship
model comes with its own material and animation properties. We don't require any of these, so let's remove these properties before continuing to import our models fully into our Unity project.
To remove the Animation and Material properties from our player_ship
model, we need to do the following:
Assets/Model
and select the player_ship
file.player_ship
model.Important Information
Throughout the book, whenever we select a three-dimensional model, make sure to run through the same process, as we will not require imported extras such as the ones we just removed. This means I would like you now to repeat the process we have just gone through with the enemy_wave.fbx
model.
Let's continue preparing our player_ship
model for our game:
player_ship
from Assets/Model
into the Hierarchy window.player_ship
in the Hierarchy window and set its name and Transform properties to the following values:player_ship
0
apart from Scale, which is set to 1
on all axesThe following screenshot shows the player_ship
values in the Inspector window:
When creating a prefab, sometimes you might be asked if this is an Original or Variant:
A variant prefab will be a copy of an original prefab but will also carry any changes made from the prefab it originates from. So, for example, if the original prefab was a car with 4 wheels, the variant will have the same. If the original prefab changes it's numbers from 4 to 3, the variant will copy the original prefab.
Note that player_ship
in the Hierarchy window has turned blue, which means it has become a prefab.
player_ship
from the Hierarchy window.We are going to use a similar process to create our enemy_wave
prefab, but we will also need to create its own name tag because there isn't an Enemy tag... yet.
In this section, we are going to create an enemy_wave
prefab along with a custom tag. The tag will be used to identify and categorize all related enemies under one tag.
To create an enemy_wave
prefab and custom name tag, follow these instructions:
enemy_wave.fbx
file from Assets/Model
into the Hierarchy window.enemy_wave
file selected in the Hierarchy window, update the following values in the Inspector window:enemy_wave
.0
apart from Scale, which is set to 1
on all axes:Now, let's create a new tag for the enemy_wave
game object by doing the following:
Enemy
in the pop-up window, as shown in the following screenshot, and then click the Save button:enemy_wave
game object to bring back our Inspector window details.enemy_wave
game object from the Hierarchy window into Assets/Prefab/Enemies
.enemy_wave
from the Hierarchy windowWe now move on to our third prefab creation – the player's bullet. But this time, we won't import a three-dimensional model – we are going to create one in the Unity Editor, and then create a prefab from it in the next section.
Next, we are going to create the visuals for the player's bullet in the Unity Editor. We will make a blue sphere and give it a surrounding light source. Let's start by creating a three-dimensional sphere game object.
In the Hierarchy window, right-click, and from the drop-down list, select 3D Object | Sphere.
With the newly created Sphere
in the Hierarchy window still selected, make the following changes to the Inspector window:
Sphere
to player_bullet
.0
, apart from the Scale values for the bullets, which should be slightly larger, with a Scale value of 2
on all axes.The following screenshot shows all three changes:
Next, we will give the player_bullet
game object a new blue material.
In this section, we will be creating a simple unlit material that will not take up much of the device's performance, thanks to the simplicity of the material. To create a basic material and apply it to the player_bullet
object, do the following:
Assets/Material
folder.Material
folder, make a new folder (the same way we did in the Setting up our Unity project section) and name the folder Player
. That way, any material related to the player can be stored inside.Player
folder and right-click in the Project window (in the open space in the right section of the window) again, and from the drop-down list, select Create | Material.player_bullet
.player_bullet
material, and in the Inspector window, change the material from a Standard shader to Unlit | Color by following the three steps in the following screenshot:The Inspector window will remove the majority of the properties and strip the material back to something simpler and easier to perform on any device.
0
, G: 190
, B: 255
, and A: 255
.We have created and calibrated our player's bullet, so now, we can apply the material to the player_bullet
prefab by doing the following:
player_bullet
prefab in the following location of the Project window: Assets/Prefab/Player
.player_bullet
in the drop-down list until you see the material, and then select it.The following screenshot shows the player_bullet
prefab's Mesh Renderer component updated to our new unlit material:
In Chapter 4, Applying Art, Animation, and Particles, we will return to materials and art in general, which will be of note if you found this interesting. We will also play around with particle systems to create a fleet of stars rushing past the player's ship.
The last component we will add to our player's bullet is a surrounding light to give our bullet an energy glow.
In this section, we will be adding a light component to the player's bullet to hide the impression that all that we are doing is firing spheres. It will also introduce us to Unity's point light, which acts as a glowing ball.
To add and customize a ball of light to the player's bullet, we need to do the following:
Assets/Prefab/Player
folder, select the player_bullet
prefab, and drag it into the Hierarchy window (if it isn't in the Hierarchy window already).The player_bullet
prefab will now have a Light component attached to it. We just need to change three property values to make the light suit the game object more.
player_bullet
file's Light component:50
0
, G: 190
, B: 255
, and A: 255
20
The following screenshot shows the Light component after the values have been updated:
Before moving onto the next section, because we have taken an existing prefab and added a material and a light component, we need to click the Override button to confirm the new changes.
The following screenshot shows the Overrides button at the top-right corner of the Inspector window for our player_bullet
prefab:
player_bullet
from the Hierarchy.In the next section, we will continue to update our three prefabs by applying Unity's own physics system, the Rigidbody component, to help detect collisions.
Because this game involves collisions with game objects, we need to apply collision detection to the player, the player's bullets, and the enemy. Unity offers a range of different shapes to wrap around a game object that functions as an invisible shield; we can set our code to react to contact being made with the shield.
Before we add colliders to the player and enemy game objects (the Sphere game object automatically comes with a collider), we need to add a Unity component called Rigidbody. If a game object is going to collide with at least one other game object, it requires a Rigidbody component, which can affect a game object's mass, gravity, drag, constraints, and more. If you would like to know more about Rigidbody components, check out the documentation at https://docs.unity3d.com/Manual/class-Rigidbody.html.
Rigidbody Joints
Unity has other physics types apart from the collider. Joints also require the Rigidbody system, and they come in different forms, such as Hinge, Spring, and others.
These Joints will simulate at a fixed point; for example, the Hinge Joint would be good at making a door swing back and forth around a door hinge's pivot point.
If you would like to know more about Joints, check the documentation at https://docs.unity3d.com/Manual/Joints.html.
Let's add the Rigidbody component to the player_ship
and player_bullet
prefabs in one go:
player_ship
and player_bullet
files.Rigidbody
.Now, we can add colliders to our player_ship
and enemy_wave
game objects (our player_bullet
already has a SphereCollider). We will be adding a SphereCollider to our game objects because it's the cheapest collider to use, relative to performance costs:
player_ship
prefab from the Project window location of Assets/Prefab/Player
into the Hierarchy window.player_ship
still selected in the Hierarchy window, click the Add Component button in the Inspector window and type Sphere Collider
in the drop-down menu.player_ship
game object.You will notice a green wireframe around the player_ship
in the Scene window (with the player_ship
still selected in the Hierarchy window, hover your mouse in the Scene window and press F on the keyboard to zoom in on the ship if you can't see it). This is the player_ship
collider that will be used to detect hits. It may be too big for the purpose of a hitbox, so let's reduce its size.
player_ship
prefab still selected in the Hierarchy window, alter the Radius of the SphereCollider component to 0.3
in the Inspector window, as shown in the following screenshot:player_ship
prefab selected, check the Is Trigger box, as shown in the previous screenshot. This will make the player_ship
prefab look for another collider without causing any form of potential physics collision.player_ship
prefab in the Hierarchy window and press Delete on our keyboard, as we no longer need it in our Scene.We now need to apply the same methodology to the SphereCollider component of our player_bullet
:
player_bullet
prefab from Assets /Prefab/Player
into the Hierarchy window.player_bullet
changes, and delete the player_bullet
prefab from the Hierarchy window.The last game object we need to update is the enemy_wave
prefab. We have already covered the steps with the player_ship
and player_bullet
prefabs, so it's not ideal to repeat the instructions in full; however, we need to do the following:
enemy_wave
prefab from its location at Assets/Prefab/Enemies
in the Project window into the Hierarchy window..enemy_wave
prefab in the Inspector window.enemy_wave
prefab with the correct proportions, as we did with player_ship
.enemy_wave
prefab doesn't require a Rigidbody component, as it will be colliding with relevant game objects that hold one themselves.enemy_wave
prefab from the Hierarchy window.Use the following screenshot as a reference for the preceding mini-brief, and if you get stuck, use the previous steps that we discussed in this section:
Hopefully, that went well for you. If you get stuck at any point, refer to the https://github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/blob/main/Chapter_02/ProjectFIles/Chapter-02-Complete.unitypackage folder containing all the completed files to check them out and compare.
Before moving on, note that if a game object is pink, such as our enemy_wave
object in the previous screenshot, it simply means that it doesn't have a material attached. In other cases, it can also mean there is something wrong with the shader attached to the material.
We can fix this pink issue by doing the following:
Assets/Prefab/Enemies
.enemy_wave
in the Hierarchy window..enemy_wave_core
.enemy_wave_ring
.The enemy_wave
object will now have a default material applied.If any changes were made to the prefab be sure to click Override, Apply All.
Attributes
If a game object requires a component such as Rigidbody
, we can place, above the class name, what is effectively a reminder to the script that the game object needs it:
[RequireComponent(typeof(Rigidbody))]
If the game object doesn't have the component, the script will create one, and if we try to remove the Rigidbody
component, we will receive a message in the Unity Editor that it is a required component.
This code isn't a requirement as such, more of a good practice with components in general.
If you would like to know more about the RequireComponent
attribute, check the documentation at https://docs.unity3d.com/ScriptReference/RequireComponent.html.
So, now we have our colliders and Rigidbody components applied to our game objects. This gives us the ability to create a reaction when colliders come into contact with each other.
Because we are starting to build up our project, let's quickly discuss saving our scenes, projects, and so on.
It's easy to get stuck into our project, but as a brief reminder, save your work as often as possible. That way, if anything bad happens, you can always revert back.
Because we have created and saved our testLevel
scene from the previous chapter, we can also add this scene to the Build Settings window. The reason for this is so that Unity is aware of what scenes we want to include in our project. It is also a requirement when it comes to packaging up our game as a build for deployment.
To add our scene to Build Settings, do the following:
testLevel
scene.testLevel
scene. When we add more scenes later, each scene will be numbered:Let's now continue with setting up our scene camera in the Unity Editor.
For our side-scrolling shooter game Killer Wave, we need control over a camera to display the aspect ratio and visible depth of the scene, and to make sure we show the correct amount of our game's environment.
Let's get started and decide on the screen ratio of our game. We'll create our own resolution, which will be fairly common across most platforms.
To change the Game window's screen ratio to a custom aspect, do the following:
1080
resolution we have just made:It is good to be aware of the need to make our game's artwork support (or to give it the scope to extend to) as many screen ratios as possible, especially if we ever wanted to make a game for portable devices such as tablets or mobile phones. This is because nearly every major brand of phone and tablet comes in different ratio sizes, and we don't want to start squashing and squeezing our content, as it won't look right. It's also possible that our small mobile games will become successful and could later be ported to a console or PC. If that's the case, we need to make the game screen support these ratios too. The main point to take from all of this is that we are targeting our game to cover all possible common screen ratios. The more platforms (consoles, portable devices, and so on) we can cover with flexible screen ratios, the easier it will be to extend our game out to those devices without requiring extra work. We explain more about screen size ratios in Chapter 8, Adding Custom Fonts and UI, and Chapter 9, Creating a 2D Shop Interface and In-Game HUD, where we discuss UI display settings. Additionally, in Chapter 13, Effects, Testing, Performance, and Alt Controls, we will explain how to display our game screen on a raw image component.
Before we continue any further with our project, it's probably a good time to confirm our understanding of Unity's own UI layout. The following screenshot shows the Unity Editor, where I have outlined and labeled the relevant windows:
Typically, the Unity Editor window is made up of five main windows:
Tip
To drag each window around individually, left-click and drag the name of the tab, and it will then snap into different locations.
My Game window is set to 1080, and because I don't have the luxury of a second screen, I've clicked its name tab (Game) and pulled it down in the bottom-right corner. The window is small, but as you can see at the top of the Game window, the scale is set to 1x, which means I have a full picture; nothing is hidden or cut out of view.
To check that we have the main camera's Transform properties reset to their default settings, make sure that Position, Rotation, and Scale are all set to 0
. We can also reset the Transform option as follows:
Continuing with setting up our main camera, let's get rid of the landscape background in our Scene/Game window by changing its Background setting:
0
, G: 0
, B: 0
, and A: 255
, as shown in the following screenshot:Great, now let's move on to coding these properties for our main camera.
We now have our main camera's behavior set in our Scene. Next, we need to code this into a script so that whenever a scene is loaded, Unity will read the script and understand how the main camera should be set up.
Observing our framework again, let's see where the camera script should be placed:
As you can see in the diagram, there is no reference to the camera, so should we make a script to support this? Arguably, the only reason to make a script based on the camera would be if the camera had a complex purpose filled with multiple properties and functions. The camera in our game, however, is put in place when the game starts. Later on, on the third level, the camera will move from left to right with a simple component script, but it doesn't hold any other complexity. It would, therefore, be more ideal to use the GameManager
, as it only takes up a small role. If the game became bigger and the camera took on more of a role, then this might justify the camera having a class of its own. Others might disagree based on personal preference, but this is the approach we'll take.
Let's make the GameManager
script, as follows:
NewBehaviourScript
. We don't want to call it that, so type (in camel case) GameManager
.What's camel casing?
Camel casing is a way to avoid spacing between words. This is fairly common with programming, as spaces are typically not welcomed for various reasons. Each new word starts with a capital letter, so in this case, the M in GameManager
is the hump of the camel. However, variables typically start with lowercase, as you will see shortly.
We now have our GameManager
script. Notice how Unity is trying to be helpful by changing the icon to a silver cog because what we are doing is a recognized method with Unity:
As we did when placing our three-dimensional models into the Model folder, move the GameManager
into the Script
folder.
Good. Now, before we open our script to code in it, we need to attach it to a game object in our scene so that when the scene runs, the script attached to the game object also runs.
To create our GameManager
game object, we need to do the following:
GameManager
.GameManager
game object still selected, click the Add Component button in the far-right Inspector window.GameManager
from the drop-down menu until you see the GameManager
script and select it.Tip
Whenever we make an empty game object, we must be sure that all of its Transform property values are reset to their default values unless we are specifically changing them.
To reset a game object's Transform value, make sure that the game object we are resetting is selected. Click the metal cog at the top-right corner of the Inspector window, and then select Reset.
Double-click the GameManager
script to open it up in your IDE (Visual Studio or whatever IDE you use), and then proceed as follows:
GameManager
script, we will be faced with the UnityEngine
library being imported into our script to add extra functionality to Unity's own components:using UnityEngine; public class GameManager : MonoBehaviour {
Also in the preceding code, we have the name of our script along with MonoBehaviour
being inherited yet again to add more functionality to our script. MonoBehaviour
is also required if the game object that attaches to this script needs to be used in the Unity Editor.
Let's start adding some of our own code into our GameManager
script.
CameraSetup
, and then run this method in the Start
function:void Start() { CameraSetup(); } void CameraSetup() { }
CameraSetup
method, add a reference to the camera and set the position and angle of the camera to zero apart from its z axis. We'll set Z
to -300
, which will move the camera back and ensure all game objects are in the shot:GameObject gameCamera = GameObject.FindGameObjectWithTag("MainCamera"); //Camera Transform gameCamera.transform.position = new Vector3(0,0,-300); gameCamera.transform.eulerAngles = new Vector3(0,0,0);
CameraSetup
method://Camera Properties gameCamera.GetComponent<Camera>().clearFlags = CameraClearFlags.SolidColor; gameCamera.GetComponent<Camera>().backgroundColor = new Color32(0,0,0,255); }
This does the following:
Now, you should have something like this:
Tip
If you would like to change other settings relating to the camera, you can find out about them at https://docs.unity3d.com/ScriptReference/Camera.html.
Press the Play button in the upper middle of the editor window, or by using the shortcut Ctrl + P (Command + P on the Mac). The following screenshot shows where the Play button is located:
With the scene in play mode, we can now check out the Main Camera game object's properties by doing the following:
Observe the Inspector window in the next screenshot to see the following changes our script has made.
The following screenshot shows the Main Camera component properties being updated in Play mode:
Now, hopefully, our properties should be the same as what we have scripted (with no errors). If not, you will likely have an error message in the Console window. If there is an error, it will likely tell you what line the error is on. You can also double-click the error, and it will take you to the line the error is on.
To double-check everything has worked, change the Position and Rotation of the camera in the editor, and then press the Play button. The properties for the camera should now be set to our script's Position and Rotation properties.
At this point, while the editor is still playing, we can also make a prefab of the camera:
Prefab
folder.You might be thinking, why didn't we just make a prefab of the camera in the first place instead of fiddling with its property settings in code? However, two key things are important here: firstly, we are studying for an exam that is likely to cover such properties; and secondly, you now know how to change these settings dynamically through code.
Tip
Another benefit to scripting Unity's components is that we can sometimes be offered more functionality than what is displayed in the Editor. For example, the Camera
component has a layerCullDistances
property that is only accessible via scripting. This can offer functionality such as skipping the rendering of smaller game objects in the far distance to increase a game's performance.
To read more about layerCullDistances
, check the documentation at https://docs.unity3d.com/ScriptReference/Camera-layerCullDistances.html.
This brings this section to a close. So far, we have covered the following:
GameManager
scriptGameManager
script to our scene as a game objectAs a programmer, the importance of being able to understand and change the settings in the Unity Editor (but also being able to do the same in code) can be expanded to other components that are in the editor. This is what we will do next, with a focus on directional light.
As a default setup, each scene comes with a directional light, and currently, this is all we need to get going; ideally, we want the scene to be well lit.
With the directional light already present in the scene as the default light, select it in the Hierarchy window. In the Inspector window, set the Directional Light's Transform Rotation values to the following: X: 50
, Y: -30
, and Z: 0
.
When we put our player ship into the scene, this will light it up well, as shown in the following screenshot:
Different Lights
Unity provides three different types of real-time lights. As well as the directional light we mentioned, it also provides a point light, which is like a 360° glow that we will cover in Chapter 4, Applying Art, Animation, and Particles. The third type of light is a spotlight or, as Unity refers to it, a spot. The spot can also have masks applied, so it can project images known as cookies.
For more information about the three types of lights, check out https://docs.unity3d.com/Manual/Lighting.html.
We can now make sure these settings stay in place by adding them to the GameManager
script. We can also alter the light's color.
In the GameManager
, we will set the Transform Rotation values and change the color tint from a light yellow to a cold blue:
GameManager
script and enter the following method:void LightSetup() { GameObject dirLight = GameObject.Find("Directional Light"); dirLight.transform.eulerAngles = new Vector3(50,-30,0); dirLight.GetComponent<Light>().color = new Color32(152,204,255,255); }
LightSetup();
in the scope of the Start
function.The LightSetup
method does three things:
EulerAngles
.EulerAngles
eulerAngles
allows us to give Vector3
coordinates instead of Quaternion
values. eulerAngles
makes rotations less complicated to work with. More information about eulerAngles
can be found at https://docs.unity3d.com/ScriptReference/Transform-eulerAngles.html.
That's all we need to do with our light. As with the camera, we can access the light and change its properties via a script.
We have become familiar with our light by changing its settings in the Unity Editor and the GameManager
script. Next, we will set up our interface for the majority of our game objects.
The IActorTemplate
interface is what we are using to prompt damage control, death, and scriptable object assets. The reason for using an interface such as this is that it ties general uses together between classes that inherit it.
A total of six classes will be using the IActorTemplate
interface, which is as follows:
Player
PlayerBullet
PlayerSpawner
Enemy
EnemyBullet
EnemySpawner
The following diagram shows the IActorTemplate
interface with a partial overview of our game framework:
Let's create our interface and explain its content along the way:
Assets/Scripts
folder with the filename IActorTemplate
.public interface IActorTemplate { int SendDamage(); void TakeDamage(int incomingDamage); void Die(); void ActorStats(SOActorModel actorModel); }
The code we just entered looks like we have declared a class, but it acts fundamentally differently. Instead of using the class
keyword, we enter interface
followed by the name of the interface, IActorTemplate
. It's not a requirement to start any interface name with an I
, but it makes the script easily identifiable.
Within the interface
, we make a list of methods that act like contracts to whichever class implements them. For example, the Player
script that we'll create later on in the chapter inherits the IActorTemplate
interface. The Player
script must declare the function names from IActorTemplate
or the Player
script will throw an error.
Inside the scope of the interface
, we declare methods without accessors (it doesn't require private
or public
at the beginning of each method). Methods also don't require any content in them (that is, they are empty bodies).
For more information about interfaces, check out https://learn.unity.com/tutorial/interfaces.
The last method in our interface
is ActorStats
, which takes a SOActorModel
type. SOActorModel
is a scriptable object that we are going to explain and create in the next section.
In this section, we are going to cover scriptable objects and their benefits. Similar to our interface
, scriptable objects cover the same six classes. The reason for this is that our interface
uses the SOActorModel
and, therefore, creates an attachment with the other variables.
It is also good to remind ourselves of the Game Design Document (GDD) and how it is incorporated into the overview of the creation of our game.
Our game has three series of game objects that will hold similar properties: EnemyWave
, EnemyFlee
, and Player
. These properties will include health, speed, score value, and more. The difference between each of these as described in the game design brief is the way they act and also how they are instantiated in our game.
Player
will be instantiated at every level, EnemyWave
will be spawned from EnemySpawner
, and EnemyFlee
will be placed in particular areas of the third level.
All of the aforementioned game objects will relate to the SOActorModel
object.
The following diagram is also a partial view of our game framework, showing the scriptable object and the six classes that inherit it:
Similar to what was mentioned with the interface
script is that the name of the scriptable object name starts with SO
, which isn't a standard way of naming the script, but it's easier to identify as a ScriptableObject
.
The purpose of this scriptable object is to hold general values for each of the game objects it's being given to. For example, all game objects have a name, so within our SOActorModel
is a string
named actorName
. This actorName
will be used to name the type of enemy, spawner, or bullet it is.
Let's create a scriptable object, as follows:
Assets/Scripts
folder with the filename SOActorModel
.using UnityEngine; [CreateAssetMenu(fileName = "Create Actor", menuName = "Create Actor")] public class SOActorModel : ScriptableObject { public string actorName; public AttackType attackType; public enum AttackType { wave, player, flee, bullet } public string description; public int health; public int speed; public int hitPower; public GameObject actor; public GameObject actorsBullets; }
Inside the SOActorModel
, we will be naming most, if not all, of these variables in the Player
script. Similar to how an interface
signs a contract with a class, the SOActorModel
does the same because it's being inherited, but isn't as strict as an interface
by throwing an error if the content from the scriptable object isn't applied.
The following is an overview of the SOActorModel
code we just entered.
We named our scriptable object SOActorModel
as a generic term to try and cover as many game objects as will likely use the scriptable object. This way of working also supports the SOLID principles we covered in the first chapter by encouraging us to try and keep our code concise and efficient.
The main categories we'll cover for this script are as follows:
SOActorModel
script is using UnityEngine
; no other libraries are required.CreateAssetMenu
attribute creates an extra selection from the drop-down list in the Project window in the Unity Editor when we right-click and select Create, as shown in the following screenshot:MonoBehaviour
but ScriptableObject
instead, as it's a requirement when it comes to creating an asset.In the following sections, we are going to create assets from the scriptable object script to give our scripts different values.
With our SOActorModel
ScriptableObject
made, we can now create an asset that will act as a template that can be used not just by programmers but also by designers who want to tweak game properties/settings without needing to know how to code.
To create an Actor Model
asset, do the following:
Player_Default
and store the file in the Assets/Resources
folder.The following screenshot shows the Actor Model
asset's fields, where I have entered my own values:
Let's break down each of the values that have been added to our newly created asset:
Player
).player_ship
prefab here (Assets/Prefab/Player
player_bullet
prefab here (Assets/Prefab /Player/
).We will add this asset to our PlayerSpawner
script once it's built later on in the chapter. Let's move on to the next scriptable object asset.
In this section, we are going to make our enemy asset attach to EnemySpawner
for later on in the chapter. For the sake of keeping our work fresh and complete, let's continue with that before moving on to the EnemySpawner
script.
To make an enemy asset, follow these instructions:
BasicWave Enemy
) and store the file in the Assets/ScriptableObject
location.The following screenshot shows what the BasicWave Enemy
asset is going to look like once we've finished:
Let's briefly go through each of the values for our enemy:
enemy_wave
.Wave
. This explains what type of enemy it is and how it attacks the player.Typically in groups
. As mentioned before, it's more of a guideline than a rule to comment on anything.1
, which means it takes 1 hit to die. -50
, because our enemy is moving from right to left, so we give it a minus figure.1
, which means that if this enemy collides with the player, it will cause 1 hit point of damage.enemy_wave
prefab here (Assets/Prefab/Enemies
).Hopefully, you can see how useful scriptable objects are. Imagine continuing to develop this game with 50
enemies, where all we need to do is create an asset and customize it.
We are going to move on to the final scriptable object asset for this chapter in the next section.
In this section, we are going to create an asset for the player's bullet for when they fire. As with the last two sections, create an asset, name it PlayerBullet
, and store it in the same folder as the other assets.
The following screenshot shows the final results for the PlayerBullet
asset:
Let's briefly go through each variable's values:
player_bullet
.1
.700
.1
sends a hit point of 1.player_bullet
prefab here (Assets/Prefab/Player
).In a later chapter, when we build a shop for our game, we will be able to buy power-ups for our player's ship. One of the power-ups will be similar to the one that we just made, but the Actor Name will be different, and the Hit Power will have a higher number.
Now, we can move on to the next section and create the player's scripts and attach these assets to them.
In the following series of sections, we are going to create three of the scripts that will cover the following: spawning the player, the player's controls, and the player's bullet.
The scripts we will be creating and including are as follows:
PlayerSpawner
: Creates and calibrates the playerPlayer
: Player controls and general functionalityPlayerBullet
: Bullet movement and general functionalityIActorTemplate
: A template of the expected rules assigned to a given object (already made)SOActorModel
: A set of values that can be altered by non-programmers (already made)We will cover all of these scripts thoroughly and break down each of their purposes, as well as how they depend on and communicate with one another. We will start with the PlayerSpawner
, which will create the player's ship and issue its values.
The purpose of the PlayerSpawner
script is to be attached to a game object, resulting in the player appearing at its position in the game. The PlayerSpawner
script will also set the player's values when it is created. For example, if our player had a particular speed value, or if they had received an upgrade from the shop, the PlayerSpawner
script would grab these values and apply them to the Player
script.
The following diagram shows a partial view of the PlayerSpawner
class in the game's framework and its relationship with the other classes around it:
As we can see, the PlayerSpawner
script is connected to four other scripts:
Player
: PlayerSpawner
is connected to Player
because it creates the player.SOActorModel
: This is a ScriptableObject
that gives the PlayerSpawner
its values, which are then passed on to the Player
.IActorTemplate
: This is the interface
that generalizes the script with other common functions.GameManager
: This will send and receive general game information from and to the PlayerSpawner
script.Before we create our PlayerSpawner
script, it would be good housekeeping to create an empty game object to store anything to do with our player, their bullets, and whatever else the player might create in our testLevel
scene.
Make and name the game object by following these steps:
_Player
.That's all that we need to do. Now, let's make a start with the PlayerSpawner
script:
Assets/Scripts
folder with the filename PlayerSpawner
.using UnityEngine;
We only require using UnityEngine
, as it covers all of the objects we need in the script.
public class PlayerSpawner : MonoBehaviour {
It is common in Unity to inherit MonoBehaviour
to give the script more functionality within Unity. Its common purpose is so the script can be attached to a game object.
SOActorModel actorModel; GameObject playerShip;
Inside the PlayerSpawner
class, we add two global variables: the first variable is the actorModel
, which holds a scriptable object asset that will contain values for the player ship, and the second variable will hold our player ship once it's been created from our CreatePlayer
method.
Start
function:void Start() { CreatePlayer(); }
After the global variables, we add a Start
function that will run automatically as soon as the game object holding the PlayerSpawner
script is active at runtime.
Inside the scope of the Start
function is a method that we are going to create called CreatePlayer
.
CreatePlayer
method:void CreatePlayer() { //CREATE PLAYER actorModel = Object.Instantiate(Resources.Load ("Player_Default")) as SOActorModel; playerShip = GameObject.Instantiate(actorModel.actor) as GameObject; playerShip.GetComponent<Player>().ActorStats(actorModel); //SET PLAYER UP } }
I have split the CreatePlayer
method into two commented-out parts (//CREATE PLAYER
and //SET PLAYER UP
) due to its size.
This first part of the CreatePlayer
method will instantiate
the player ship's ScriptableObject
asset and store it in the actorModel
variable. We then instantiate
a game object that refers to our ScriptableObject
that holds the game object called actor
in our game object variable named playerShip
. Finally, we apply our ScriptableObject
asset to the playerShip
method called ActorStats
that exists in the Player
component script (which we will create later on in this chapter).
CreatePlayer
method to add the second half://SET PLAYER UP playerShip.transform.rotation = Quaternion.Euler(0,180,0); playerShip.transform.localScale = new Vector3(60,60,60); playerShip.name = "Player"; playerShip.transform.SetParent(this.transform); playerShip.transform.position = Vector3.zero;
In the second half of the CreatePlayer
method, we add more code at the same point where we have commented //SET PLAYER UP
.
The code from //SET PLAYER UP
onward is dedicated to setting up the player's ship in the correct position at the start of the level.
The code does the following:
60
on all axes.instantiate
any game object, Unity will add (Clone)
to the end of the game object's name. We can rename it Player
.playerShip
game object a child of the _Player
game object in the Hierarchy window so that we can easily find it.That is our PlayerSpawner
script coded. Now, in the next section, we need to create and attach this script to a game object and name it. Make sure to save the script before moving on.
In this section, we will create a game object that will hold our newly created PlayerSpawner
script, and then we will position the PlayerSpawner
game object in the testLevel
scene.
To create and set up our PlayerSpawner
game object, we need to do the following:
PlayerSpawner
.PlayerSpawner
game object onto the _Player
(remember that _Player
is the empty game object in our scene) game object to make the PlayerSpawner
its child.Because our PlayerSpawner
game object doesn't have anything visually applied to it, we can give it an icon.
PlayerSpawner
game object still selected in the Inspector window, click the multi-colored box to the left of its name. A selection of colors will be offered, as shown in the following screenshot:PlayerSpawner
game object will be given a label to show us where it is in the scene. This will now appear in the Scene window.Tip
If you still can't see the icon in the Scene window, make sure 3D icons are turned off. You can check by clicking the Gizmos button in the top right of the Scene window and unchecking the 3D Icons box.
With the PlayerSpawner
game object sitting inside the _Player
game object in the Hierarchy window, we now need to give it the following Transform property values, which will help two things. The first thing is to help set the boundaries of our ship within the game's screen ratio (we will explain more about this in the next chapter); the second is for later on in the book, where we will make the player ship animate in the screen view. For now, we just need to give our PlayerSpawner
game object the following values:
PlayerSpawner
game object still selected, in the Inspector window, give it the following Transform values:PlayerSpawner
until you see the script appear in the drop-down list.PlayerSpawner
script to add this to the PlayerSpawner
game object.We can't move the ship yet, nor can we fire because we haven't coded this in yet. In the following section, we will go through the player's controls, then we will move on to coding our player and its bullet to travel across the screen.
Remember that this is a side-scrolling shooter game, so the controls will be two-dimensional even though our visuals are three-dimensional. Our focus now is to get the Players
controls set up. To do this, we need to access the Input Manager:
The Input Manager will offer a list of all available controls for our game. We will first check what the controls are set to by default. There are a lot of options here, but as mentioned, we only need to browse through the properties that matter to us, namely the following:
To check these three properties, we need to do the following:
-1
), and the right button configures it positively (+1
). Alternative key presses to this effect are A for left and D for right.If we had analog controls such as a joystick or a steering wheel, we would likely need to be concerned about the influence of gravity when the player releases the controls and it returns to its center. Dead refers to the center of the analog controls. Sometimes, controllers can be unbalanced and naturally lean to one side, so by increasing the dead zone, we can eliminate false feedback from the player that could be detected as a movement.
-1
) and the positive button is up (+1
). Alternative buttons are S for down and W for up.mouse 0
(that is, the left mouse button). For now, remove mouse 0
from the alternative button.To find out more about the Input Manager window, click the little blue book at the top-right corner of the Input Manager panel.
Our controls are now set in the Input Manager window, so let's move on to coding the Player
script to take advantage of these controls.
The Player
script will be attached to the player ship game object, from which the player will be able to move and shoot, as well as inflict and receive damage. We will also make the player ship not go outside of the screenplay area. Before we continue, let's remind ourselves where the Player
script lies in our game framework:
The Player
script will be in contact with the following scripts:
PlayerBullet
: The Player
script will create bullets to fire.PlayerSpawner
: The Player
script is created from the PlayerSpawner
.IActorTemplate
: Contains damage control and the properties for Player
.GameManager
: Extra information such as the number of lives, the score, the level, and whatever upgrades the player ship has accumulated will be stored in GameManager
.SOActorModel
: Holds ScriptableObject
properties for Player
.Now that we are familiar with the Player
script's relation to the other scripts, we can start coding it:
Assets/Scripts
folder with the filename Player
.IActorTemplate
interface to the existing default code:using UnityEngine; public class Player : MonoBehaviour, IActorTemplate {
The script will by default import a UnityEngine
library (including some others), the name of the class, and MonoBehaviour
. All of these are essential to make the script work in the Unity Editor.
Player
script, enter the following global variables:int travelSpeed; int health; int hitPower; GameObject actor; GameObject fire; public int Health { get {return health;} set {health = value;} } public GameObject Fire { get {return fire;} set {fire = value;} } GameObject _Player; float width; float height;
We have entered a mixture of integers, floats, and game objects in our global variables; starting from the top, the first six variables will be updated from the player's SOActorModel
script. travelSpeed
is the speed of the player's ship, health
is how many hits the player can take before dying, hitPower
is the damage the ship will cause when colliding into something that can receive damage (the enemy), actor
is the three-dimensional model used to represent the player, and finally, the fire
variable is the three-dimensional model from which the player fires. If that seemed a little rushed, go back to the Introducing our ScriptableObject – SOActorModel section, where we went into more detail about these variables.
The two public properties of Health
and Fire
are there to give access to our two private health
and fire
variables from other classes that require access.
The _Player
variable will be used as a reference to the _Player
game object in the scene.
The last two variables of width
and height
will be used to store the measured results of the world space dimensions of the screen that the game is played in. We will discuss these two more in the next block of code.
Before we begin the following Start
function code block, you may question why we would pick Start
over Awake
when it comes to running a function's code content. Both functions run once at runtime; the only noticeable difference is that Awake
runs when the object is created. Start
is executed when it's enabled, as can be seen in the documentation at https://docs.unity3d.com/Manual/ExecutionOrder.html.
For simplicity in our Unity project, we will vary between which of the two functions to use. This is so we avoid conflicts between several Awake
functions running at the same time. As an example, one script may try to update its Text UI, but the variable updating the text may still be null at runtime because the script with the variable is still waiting for its content to be updated.
There is a way to avoid conflicts between several Awake
functions being called by several scripts at runtime, by going to Unity's Script Execution Order in Edit | Project Settings | Script Execution Order.
If you would like to know more about the Script Execution Order, check the documentation at https://docs.unity3d.com/Manual/class-MonoManager.html.
Player
script, next up, we will type out the Start
function along with its content:void Start() { height = 1/(Camera.main.WorldToViewportPoint (new Vector3(1,1,0)).y - .5f); width = 1/(Camera.main.WorldToViewportPoint(new Vector3(1,1,0)) .x - .5f); _Player = GameObject.Find("_Player"); }
As previously mentioned, the height
and width
variables will store our world space measurements. These are required so that we can clamp the player's ship inside the screen. Both the height and width lines of code use similar methods; the only difference is with the axis we are reading.
The Camera.main
component refers to the camera in our scene, and the function it uses, WorldToViewportPoint
, is to take the results from the game's three-dimensional world space and convert the results into viewport space. If you aren't sure what viewport space is, it's similar to what we know as a screen resolution, except its measurements are in points and not pixels, and these points are measured from 0
to 1
. The following diagram shows the comparison between screen and viewport measurements:
So, with viewports, no matter what the screen's resolution is, the full height and width are 1
and everything between that is a fraction. So, for the height, we feed Vector3
to WorldToViewportPoint
, where Vector3
represents a world space value, followed by -0.5f
, which sets its offset back to 0
. Then, we divide 1
(which is our full-screen size) by the result of our formula. This will give us our current world space height of the screen. We then apply the same principles for the width and use x
instead of y
and store the result.
Finally, the last line of code takes the reference of the _Player
game object in the scene and stores it into our variable.
Player
script, we have our Update
function that is called on every frame. Enter the function along with the following two methods:void Update () { //Movement(); //Attack(); }
The Update
function runs the Movement
method and Attack
method on every frame. We will go into depth about these two methods later on in the chapter, for now we will comment ("//") these two methods out to avoid the script not being able to run.
The next method we are going to put into our Player
script is the ActorStats
method. This method is a requirement, as we declare it in the interface we are inheriting.
Update
function, enter the following piece of code:public void ActorStats(SOActorModel actorModel) { health = actorModel.health; travelSpeed = actorModel.speed; hitPower = actorModel.hitPower; fire = actorModel.actorsBullets; }
The code we have just entered assigns values from the player's SOActorModel
ScriptableObject
asset that we made earlier on in the chapter.
This method doesn't get run in our script but gets accessed by other classes, the reason being that these variables hold values regarding our player and don't need to be anywhere else.
Player
script.Before we test what we have so far, we need to attach our Player
script to our player_ship
in the Project window.
Assets/Prefab
and select the player_ship
prefab.Player
until the script appears and then select it.With our Hierarchy window containing the _Player
, PlayerSpawner
, and the GameManager
game objects, it's time to test out the game. We can see the player ship get created in our Game window by pressing Play in the Editor.
The following screenshot shows our game in Play mode; note the Hierarchy window on the left with the PlayerSpawner
game object as the parent of the Player
game object; also note the Game window with its black background, and in the center, the player's ship is facing right and is located in the center of the screen. Finally, the far-right image shows our Scene window with our PlayerSpawner
icon:
Tip
Before moving on to the next section, create a prefab of the PlayerSpawner
game object by dragging and dropping it into the Project window to Assets/Player
. That way, if you lose the scene for whatever reason along with its Hierarchy content, you can drag and drop your prefab back in. This should be a rule with any common active game object.
Let's move on to the next section where we'll continue to work on the Player
script, but this time, we will look at what happens when our player's game object comes into contact with an enemy.
In this section, we are going to add a function to our Player
script that will check to see what has collided with our player's game object during runtime. Currently, the only thing that can collide with our player is an enemy, but we can still demonstrate the use of Unity's own OnTriggerEnter
function, which handles most of the work for us:
ActorStats
) in the Player
script, we are going to add the following code that detects our enemy colliding with the player's ship:void OnTriggerEnter(Collider other) { if (other.tag == "Enemy") { if (health >= 1) { if (transform.Find("energy +1(Clone)")) { Destroy(transform.Find("energy +1(Clone)"). gameObject); health -= other.GetComponent<IActorTemplate> ().SendDamage(); } else { health -= 1; } } if (health <= 0) { Die(); } } }
Let's explain some of the code we have just entered into the Player
script:
OnTriggerEnter(Collider other)
is a function that Unity recognizes to check what has entered into the player's trigger collider.if
statement to check whether the tag
to the collider is called Enemy
. Note when we create our enemy, we will give them an Enemy
tag
so they are easily identified. If the tag
is equal to Enemy
, we drop it into that if
statement.if
statement checks to see whether our player's health
is equal to or more than 1
. If it is, that means the player can take a hit and continue without dying and also means we can go into its if
statement.if
statement that checks to see whether the collider has a game object named energy +1(Clone)
. The name of this object is the name of the shield the player can purchase in the game shop, which we will add in Chapter 6, Purchasing In-Game Items and Advertisements. If the player has this energy +1(Clone)
object, we can Destroy
it with Unity's premade function. We also deduct the player's extra health from the enemies' SendDamage
function. We will discuss SendDamage
later on in the chapter.if
statement is an else
condition where, in the event that the player doesn't have an energy +1(Clone)
game object, they get their health deducted.health
is at a value of zero or under, we run the Die
method, which we will cover later in the chapter.Tip
Don't forget to keep saving your work as we continue to add more code to the project.
Let's continue with our Player
script and add the functionality so that the player can receive and send damage from and to the enemy respectively.
TakeDamage
) will take an integer called incomingDamage
and use whatever the value is to deduct from our player's health
value.The second method (SendDamage
) will return
an integer of our hitPower
value.
ActorStats
method, now add the following code:public void TakeDamage(int incomingDamage) { health -= incomingDamage; } public int SendDamage() { return hitPower; }
Let's continue with another method for the Player
script and make it possible for the player to control the player ship around the Game window.
In this section, we will code the Movement
method, which will take input from the player's joypad/keyboard and also make use of the height
and width
floats to keep the player's ship within the scree:.
Player
script, make a start with the following method using the following content to check for the player's input:void Movement() { if (Input.GetAxisRaw("Horizontal") > 0) { if (transform.localPosition.x < width + width/0.9f) { transform.localPosition += new Vector3 (Input.GetAxisRaw("Horizontal") *Time.deltaTime*travelSpeed,0,0); } }
Movement
method will consist of detecting movement in four directions being made from the player; we'll start with when the player presses right on the controller/keyboard. We run an if
statement that checks whether the Input Manager has detected any movement from the Horizontal
property. If the GetAxisRaw
detects a value higher than zero, we fall into the if
statement's condition. Note that GetAxisRaw
has no smoothing, so the player's ship will instantly move unless extra code is added.if
statement; this checks whether the player has exceeded past the width
(that is, of the screen's world space that we calculated earlier on in the chapter). We've also added an extra partial width
to avoid the geometry of the player's ship leaving the screen. If the player's position is still under the width
(and its buffer) value, we run the content inside the if
statement.Vector3
struct, which holds the value of the Horizontal
direction, multiplied by time per frame and by the travelSpeed
we set from our ScriptableObject
.Movement
method and add a similar if
statement for moving the player ship to the left:if (Input.GetAxisRaw("Horizontal") < 0) { if (transform.localPosition.x > width + width/6) { transform.localPosition += new Vector3 (Input.GetAxisRaw("Horizontal") *Time.deltaTime*travelSpeed,0,0); } }
As we can see, the code is close to the previous block; the only difference is that our first if
statement checks whether we are moving left; the second if
statement checks whether the player's position is greater than the width and a slightly different buffer.
Apart from that, the if
statement and its content serve the same position, just in the opposite direction.
Movement
method and add the if
statement code for moving the player's ship down:if (Input.GetAxisRaw("Vertical") < 0) { if (transform.localPosition.y > -height/3f) { transform.localPosition += new Vector3 (0,Input.GetAxisRaw("Vertical")*Time.deltaTime*travelSpeed,0); } }
Yet again, we follow the same rule from the previous two if
statements, but this time, instead of Horizontal
, we add the Vertical
string
property. In the second if
statement, we check whether the player's y-axis is higher than a negative height/3
. The reason why we divide by this value is that later on in the book (Chapter 9, Creating a 2D Shop Interface and In-Game HUD), we will be adding graphics at the bottom of the screen that will restrict the player's view.
if
statement in the Movement
method, moving up:if (Input.GetAxisRaw("Vertical") > 0) { if (transform.localPosition.y < height/2.5f) { transform.localPosition += new Vector3 (0,Input.GetAxisRaw("Vertical")*Time.deltaTime*travelSpeed,0); } } }
As before, this if
statement carries a similar role, but this time, it's checking whether the player's position is under the height/2.5f
value. A buffer is applied to stop the three-dimensional geometry from leaving the top of the screen.
Tip
When making a game, sometimes it occurs that when the player moves diagonally, their speed increases. This is because the player is effectively pressing two directions at the same time instead of just one.
To ensure a direction has just the magnitude of 1
, we can use Unity's pre-made Normalize
function.
To find out more about this function, check the documentation at https://docs.unity3d.com/ScriptReference/Vector3.Normalize.html.
We will continue with the Player
script by adding the Die
method.
Adding the Die
method to the Player
script will make it possible for our player to be destroyed. Currently, inside the Die
method is a Unity function called Destroy
; this function will delete whatever game object is within its parameter.
Enter the following method in the Player
script to destroy the player:
public void Die() { Destroy(this.gameObject); }
Let's move on to the last method in the Player
script, which is to attack.
In this section, we will add content to the Attack
method in the Player
script.
The purpose of this Attack
method is to receive input from the player, create a bullet, point the bullet in the correct direction, and make the bullet a child of the Player
game object to keep our Hierarchy window tidy.
Enter the following Attack
method into the Player
script to allow the player to fire bullets:
public void Attack() { if (Input.GetButtonDown("Fire1")) { GameObject bullet = GameObject.Instantiate (fire,transform.position,Quaternion.Euler (new Vector3(0, 0, 0))) as GameObject; bullet.transform.SetParent(_Player.transform); bullet.transform.localScale = new Vector3(7,7,7); } }
Inside the Attack
method, we call an if
statement that checks whether the player has pressed the Fire1
button (Left Ctrl on Windows; command if you are using a Mac). If the player has pressed the Fire1
button, we will drop into the if
statement's scope.
Note
When a developer refers to the scope of a function, if
statement, class, and so on, they are referring to what is happening between the opening and closing of the curly braces. For example, if the following code has a higher value in its money
variable, the following if
statement will run:
if (money > costOfPizza)
{
//Whatever happens between the top and bottom of the two curly braces is within the if statements scope.
}
Within the if
statement, we make another if
statement to make sure that when clicking the mouse, we are clicking on the screen and not anything UI-related. This will become more relevant when we look at adding a Pause button in Chapter 10, Pausing the Game, Altering Sound, and a Mock Test. If we do click on something UI-related, we call return
, which means we exit the if
statement so that we don't fire a shot.
Because we have entered the movement and attack function content we can scroll back up to the Update
function and remove the comments we added.
Our Update
function will now look like the following:
void Update() { Movement(); Attack(); }
Next, we Instantiate
our PlayerBullet
game object from its instance name, fire
. We also face the fire
game object to the right, relative to the screen, and move it toward oncoming enemies. We store the results of creating and orienting our game object in a variable named bullet
.
We then set the size of the bullet to be seven times larger than its original size, which makes it look bigger.
Finally, within the if
statement, we make our bullet
game object sit within a single game object with the variable name _Player
.
That is all of the code required for the Player
script! Make sure to save the script before moving on.
In the next section, we are going to move on to a different player script that controls what happens when the player fires their bullet.
In this section, we will be creating a bullet that will travel across the screen from the player's ship.
You will notice how similar the PlayerBullet
script is to the Player
script because it carries the IActorTemplate
and SOActorModel
scripts, which are already coded into the Player
script.
Let's create our PlayerBullet
script:
Assets/Scripts
folder with the filename PlayerBullet
.using UnityEngine;
By default, we require the UnityEngine
library, as previously mentioned.
public class PlayerBullet : MonoBehaviour, IActorTemplate {
We declare the public
class and by default inherit MonoBehaviour
. We also inherit the IActorTemplate
interface to give our game object-related methods from the other game object scripts, such as SendDamage
and TakeDamage
.
PlayerBullet
script:GameObject actor; int hitPower; int health; int travelSpeed; [SerializeField] SOActorModel bulletModel;
All the variables we add are private
. The last variable has a SerializeField
attribute added. SerializeField
makes it possible for this variable to be visible in the Inspector window, so even though it's private
, we can still drag and drop assets into its field (which we will do shortly). More information on the SerializeField
attributes can be found at https://docs.unity3d.com/ScriptReference/SerializeField.html.
Awake
function along with its content:void Awake() { ActorStats(bulletModel); }
In our Awake
function is the ActorStats
method, which is a requirement because we are inheriting an interface
that declares it.
SendDamage
and TakeDamage
methods:public int SendDamage() { return hitPower; } public void TakeDamage(int incomingDamage) { health -= incomingDamage; }
As mentioned already in this chapter, we require these methods to send and receive damage.
Die
method along with its content:public void Die() { Destroy(this.gameObject); }
Another method to include from our interface
is the Die
method.
ActorStats
method:public void ActorStats(SOActorModel actorModel) { hitPower = actorModel.hitPower; health = actorModel.health; travelSpeed = actorModel.speed; actor = actorModel.actor; }
The last method that we inherit from our interface
is the ActorStats
method, which will hold our ScriptableObject
asset. This asset will then be assigned to our PlayerBullet
script's global variables.
OnTriggerEnter
, along with its if
statement condition checks, as follows:void OnTriggerEnter(Collider other) { if (other.tag == "Enemy") { if(other.GetComponent<IActorTemplate>() != null) { if (health >= 1) { health -= other.GetComponent<IActorTemplate> ().SendDamage(); } if (health <= 0) { Die(); } } } }
In the preceding block of code, we run a check to see whether our bullet has collided with an "Enemy"
tagged collider. If the collider is tagged as "Enemy"
to the player, we then check to see whether the collider holds an IActorTemplate
interface. If it doesn't, then it's likely the "Enemy"
collider is an obstacle. Otherwise, we deduct health
from the Enemy
game object and check to see whether it's dead.
Update
function for the bullet's movement:void Update () { transform.position += new Vector3(travelSpeed,0,0)*Time.deltaTime; }
The Update
function adds to its x-axis each frame based on its travelSpeed
value multiplied by Time.deltaTime
(Time.deltaTime
is the time in seconds from the last frame).
Important Note
If you would like to know more about Time.deltaTime
, check the documentation at https://docs.unity3d.com/ScriptReference/Time-deltaTime.html.
OnBecameInvisible
function:void OnBecameInvisible() { Destroy(gameObject); } }
This last function will remove any unnecessary bullets that have left the screen. This will help the performance of our game and keep it tidy. Make sure you have saved the script before continuing.
Next, we need to apply the PlayerBullet
script to our player_bullet
prefab:
Assets/Prefab/Player
and select player_bullet
.Player_Bullet
selected, click the Add Component button in the Inspector window and type PlayerBullet
until you see the PlayerBullet
script.PlayerBullet
asset to it from the Bullet Model field (drag the asset into the field or click the remote button to the right of its field).The following screenshot shows our player_bullet
with its script and asset:
We can now move on to the next section about making enemies for the player to attack!
We have a player that moves, shoots, and takes damage; we can now start looking into creating an enemy that shares these attributes.
To remind ourselves of the genre we are making, our game carries the same traits as classic arcade shooters such as Konami's Gradius, Capcom's UN Squadron, and Irem's R-Type (https://github.com/retrophil/Unity-Certified-Programmer-Exam-Guide-2nd-Edition/blob/main/Reference/shootEmUps.png). Typically, with these types of games, the player is swarmed by enemies coming from the right of the screen and exiting to the left.
In this section, we will be repeating similar aspects of the PlayerSpawner
and Player
scripts. The EnemySpawner
script needs to be tweaked so that it will instantiate a given number of enemy ships at a certain rate.
The Enemy
game objects will be moving on their own, so there needs to be some extra code applied to their behavior. Before we go into creating our first enemy script, let's look at a part of our game framework and note that the layout is basically the same as the player's side of the game framework:
Before we jump into the EnemySpawner
script, let's do the same housekeeping we did for our player game objects, namely creating an empty game object and storing all game objects relating to it in that one game object. The reason we did this is to remove the clutter in the Hierarchy window, so let's do the same for our enemies:
_Enemies
.Let's move on to our enemy scripts.
In this section, we are going to make a start on our EnemySpawner
script and game object. The purpose of the EnemySpawner
script is to have a game object spawn an enemy game object a series of times at a set rate. As soon as our testLevel
scene begins, our enemy spawners will start releasing enemies. It will then be up to the enemies to move to the left of the screen. This is fairly simple, and as mentioned briefly in the previous section, the EnemySpawner
uses the same interface
and scriptable object as the PlayerSpawner
to instantiate
enemies. Let's start by creating our EnemySpawner
script:
Assets/Scripts
folder with the filename EnemySpawner
.using System.Collections; using UnityEngine;
As usual, we are using the default UnityEngine
library.
We are also going to be using another library, called System.Collections
. This is required when we come to use Coroutines
, which will be explained later in this section.
public class EnemySpawner : MonoBehaviour {
Make sure the class is named EnemySpawner
and that it also inherits MonoBehaviour
by default.
EnemySpawner
script:[SerializeField] SOActorModel actorModel; [SerializeField] float spawnRate; [SerializeField] [Range(0,10)] int quantity; GameObject enemies;
All variables entered in the previous code have an accessibility level of private
, and all of the variables apart from the enemies
variable have a SerializeField
and a Range
attribute of between 0
to 10
applied. The reason for this is so that we or other designers can easily change the spawn rate and quantity of enemies from our EnemySpawner
in the Inspector window, as shown in the following screenshot:
Awake
function along with some content:void Awake() { enemies = GameObject.Find("_Enemies"); StartCoroutine(FireEnemy(quantity, spawnRate)); }
Inside the Awake
function, we make an instance from the empty _Enemies
game object divider and store it in the enemies
variable.
The second line of code inside our Awake
function is a StartCoroutine
.
Important Information
StartCoroutine()
and IEnumerator
go hand in hand with each other. They act similarly to a method, taking parameters and running the code inside it. The main difference with coroutines is that they can be delayed by frame updates or time. You can consider them a more advanced version of Unity's own Invoke
function.
To find out more about coroutines and how to implement them in IEnumerator
instances, check Unity's documentation at https://docs.unity3d.com/ScriptReference/MonoBehaviour.StartCoroutine.html.
This will be used to run our method of creating an enemy, but as you may also notice, it takes two parameters. The first is the quantity
of enemies it holds and the second is the spawnRate
, which delays each spawned enemy.
EnemySpawner
script, we have the FireEnemy
, which will be used to run a cycle of creating and positioning each enemy, before waiting to repeat the process.Awake
function, we can add our IEnumerator
:IEnumerator FireEnemy(int qty, float spwnRte) { for (int i = 0; i < qty; i++) { GameObject enemyUnit = CreateEnemy(); enemyUnit.gameObject.transform.SetParent(this.transform); enemyUnit.transform.position = transform.position; yield return new WaitForSeconds(spwnRte); } yield return null; }
Inside the FireEnemy
IEnumerator
, we start a for
loop that will iterate over its qty
value.
Within the for
loop, the following is added:
CreateEnemy
. The result of CreateEnemy
will be turned into a game object instance called enemyUnit
.enemyUnit
is the enemy flying out of the EnemySpawner
game object.EnemySpawner
position is issued to our enemyUnit
.spwnRte
value is set to.for
loop has reached its total.FireEnemy
IEnumerator
, add the following method:GameObject CreateEnemy() { GameObject enemy = GameObject.Instantiate(actorModel.actor) as GameObject; enemy.GetComponent<IActorTemplate>().ActorStats(actorModel); enemy.name = actorModel.actorName.ToString(); return enemy; } }
As we mentioned, there is a method called CreateEnemy
. Apart from the obvious, this method will do the following:
Instantiate
the enemy
game object from its ScriptableObject
asset.ScriptableObject
asset.ScriptableObject
asset.Don't forget to save the script.
We can now move on to the next section where we will create and prepare the EnemySpawner
with its game object.
Finally, we need to attach our EnemySpawner
script to an empty game object so that we can use it in our testLevel
scene. To set up the EnemySpawner
game object, do the following:
EnemySpawner
._Player
and PlayerSpawner
, we need to move the EnemySpawner
game object inside the _Enemies
game object in the Hierarchy window.EnemySpawner
game object into the _Enemies
game object, we now need to update the EnemySpawner
game object's Transform property values in the Inspector window:EnemySpawner
until you see it in the list, and then click it.Also, for a visual aid in the Scene window, it is recommended to add an Inspector icon to the EnemySpawner
game object, as we did with our PlayerSpawner
game object in the Creating the PlayerSpawner game object section.
The following screenshot shows the icon I gave to my EnemySpawner
:
We can now add an enemy to our Enemy Spawner along with the Spawn Rate and Quantity values specified in the Inspector window. The following screenshot shows an example of a filled-in EnemySpawner
game object with its script in the Inspector window:
We can now move on to creating our enemy script in the next section.
As with our player ship being created from the PlayerSpawner
, our first enemy will be created from its EnemySpawner
. The enemy script will hold similar variables and functions, but it will also have its own movement, similar to the PlayerBullet
moving along its x axis.
Let's make a start and create our enemy script:
Assets/Scripts
folder with the filename EnemyWave
. using UnityEngine;
Like the majority of our classes, we require the UnityEngine
library.
public class EnemyWave : MonoBehaviour, IActorTemplate {
We have a public class
named EnemyWave
that inherits MonoBehaviour
by default but also adds our IActorTemplate
interface.
EnemyWave
class, enter the following global variables:int health; int travelSpeed; int fireSpeed; int hitPower; //wave enemy [SerializeField] float verticalSpeed = 2; [SerializeField] float verticalAmplitude = 1; Vector3 sineVer; float time;
The global variables for the EnemyWave
class are the top four variables updated with values from its ScriptableObject
asset. The other variables are specific to the enemy, and we have given two of these variables SerializeField
attributes for debugging purposes in the Inspector window.
Update
function along with its content:void Update () { Attack(); }
After the global variables, we add an Update
function containing an Attack
method.
ScriptableObject
method, ActorStats
, and its content:public void ActorStats(SOActorModel actorModel) { health = actorModel.health; travelSpeed = actorModel.speed; hitPower = actorModel.hitPower; }
We have our ActorStats
method that takes in a ScriptableObject
SOActorModel
. This ScriptableObject
then applies the variable values it holds and applies them to the EnemyWave
script's variables.
EnemyWave
script, add the Die
method along with its content:public void Die() { Destroy(this.gameObject); }
Another familiar method if you have been following along is the Die
method, which is called when the enemy has been destroyed by the player.
OnTriggerEnter
function to the EnemyWave
script:void OnTriggerEnter(Collider other) { // if the player or their bullet hits you. if (other.tag == "Player") { if (health >= 1) { health -= other.GetComponent<IActorTemplate> ().SendDamage(); } if (health <= 0) { Die(); } } }
Unity's own OnTriggerEnter
function will check to see whether they have collided with the player and, if so, will send damage, and the enemy will destroy themselves with the Die
method.
TakeDamage
and SendDamage
methods:public void TakeDamage(int incomingDamage) { health -= incomingDamage; } public int SendDamage() { return hitPower; }
Another common set of methods from the IActorTemplate
interface is to send and receive damage from the EnemyWave
script.
Next is the Attack
method, which controls the movement/attack of the enemy. This method is called in the Update
function on every frame.
With this attack, we will make the enemy move from right to left in a wavy animation (like a snake) instead of just going straight right to left. The following image shows our enemies moving from right to left in a wavy line:
Attack
method code into the EnemyWave
script:public void Attack() { time += Time.deltaTime; sineVer.y = Mathf.Sin(time * verticalSpeed) * verticalAmplitude; transform.position = new Vector3(transform. position.x + travelSpeed * Time.deltaTime, transform.position.y + sineVer.y, transform.position.z); }}
The Attack
method starts with Time.deltaTime
being collected in a float
variable labeled time
.
We then use a premade function from Unity that returns a sine (https://docs.unity3d.com/ScriptReference/Mathf.Sin.html) using our time
variable, multiplied by a set speed from the verticalSpeed
variable, followed by the result being multiplied by verticalAmplitude
.
The end result is stored in the Vector3
y axis. What this basically does is make our enemy ship move up and down. The verticalSpeed
parameter sets its speed and verticalAmplitude
alters how far it goes up and down.
Then, we do a similar task to what we did with the PlayerBullet
to make the enemy ship move along the x axis, and we also add a sine calculation to its Y
position for it to move up and down.
Make sure to save the script before we wind down this chapter.
Before we summarize, click Play in the Editor, and hopefully, if all is well, you will have a player ship that you will be able to fly around within the boundaries of the Game window's aspect ratio; enemies will come floating into the screen and move from right to left; you will be able to destroy these enemies with your bullets. These enemies will also be able to destroy you if they make contact with you. Finally, our Hierarchy window is all neat and well-structured both before and after playing our game. The following screenshot shows what I have just explained:
You have done so much already! The good news is that you've just conquered one of the biggest chapters in the book – quite sneaky of me, I know. But we already have the backbone of our game, and most importantly, we've covered a good chunk of the Unity Programmer exam.
Understandably, you may have come across some possible issues on the way, and you may feel stuck. Don't worry if this is the case – check the Complete
folder for this chapter to load up the Unity project and compare the code in that folder with your own to double-check. Make sure you have the right game objects in your scene, check that the right game objects are tagged, check the radius size of your Sphere colliders, and if you have any errors or warnings appear in the Console window, double-click them, and they will take you to the code that's causing the issue.
Let's wrap up this chapter and talk about our game so far.
We have reached the end of this chapter, and we have conquered the majority of our game framework, as we can see in the following diagram:
We have created a game framework that would need only a few changes whether we added 1 or 1,000 more enemies to our game. Some of the benefits of this use of reusable code and ScriptableObject
is that it will benefit non-programmers, save time, and prevent collaborators from being bogged down in the code.
We have also made it possible that if and when we want to add more EnemySpawner
points, we can drag and drop more prefabs into our scene and update its ScriptableObject
to change the enemy without coding in exact Vector3
locations.
We've covered other common Unity features, including instantiating game objects such as enemies and player bullets.
In the next chapter, we will be covering the following scripts:
ScoreManager
: When an enemy is destroyed, the player will receive a score.ScenesManager
: If the player dies, one life will be deducted; if the player loses all of their lives, the level will reset.Sounds
: Our ships and bullets will also have added sounds.Finally, we will be updating the overall structure of our code.
Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.
If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.
Please Note: Packt eBooks are non-returnable and non-refundable.
Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:
If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:
Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.
You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.
Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.
When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.
For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.