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:
The 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:
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:
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:
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:
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:
The 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.
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:
Now, let's create a new tag for the enemy_wave game object by doing the following:
We 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:
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:
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.
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:
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:
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.
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:
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:
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:
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.
We now need to apply the same methodology to the SphereCollider component of our player_bullet:
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:
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:
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:
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:
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:
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:
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:
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:
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.
void Start()
{
CameraSetup();
}
void CameraSetup()
{
}
GameObject gameCamera =
GameObject.FindGameObjectWithTag("MainCamera");
//Camera Transform
gameCamera.transform.position = new Vector3(0,0,-300);
gameCamera.transform.eulerAngles = new Vector3(0,0,0);
//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:
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:
As 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:
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);
}
The LightSetup method does three things:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
That's all that we need to do. Now, let's make a start with the PlayerSpawner script:
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.
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.
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).
//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:
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:
Because our PlayerSpawner game object doesn't have anything visually applied to it, we can give it an icon.
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:
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:
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.
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:
Now that we are familiar with the Player script's relation to the other scripts, we can start coding it:
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.
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.
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.
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.
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.
Before we test what we have so far, we need to attach our Player script to our player_ship in the Project window.
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:
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:
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.
The second method (SendDamage) will return an integer of our hitPower value.
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:.
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);
}
}
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.
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 (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:
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.
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.
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.
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.
public void Die()
{
Destroy(this.gameObject);
}
Another method to include from our interface is the Die 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.
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.
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.
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:
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:
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:
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.
[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:
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.
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:
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:
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:
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:
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.
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.
void Update ()
{
Attack();
}
After the global variables, we add an Update function containing an Attack method.
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.
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.
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.
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:
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:
Finally, we will be updating the overall structure of our code.