2.2 Building the Core Framework Scripts
2.2.2 Scene Manager
A manager script controls the game flow between physical scenes. Buttons from the menu interface do not load scenes directly; it falls upon the scene manager script below to load them:
public class SceneManager : MonoBehavior {
public string[] levelNames; public int gameLevelNum; public void Start () {
// keep this object alive
DontDestroyOnLoad (this.gameObject);
}
public void LoadLevel( string sceneName ) {
Application.LoadLevel( sceneName ); }
public void GoNextLevel() {
// if our index goes over the total number of levels in the // array, we reset it
if( gameLevelNum >= levelNames.Length ) gameLevelNum = 0;
// load the level (the array index starts at 0, but we start // counting game levels at 1 for clarity’s sake)
LoadLevel( gameLevelNum );
// increase our game level index counter gameLevelNum++;
}
private void LoadLevel( int indexNum ) {
// load the game level
LoadLevel( levelNames[indexNum] ); }
public void ResetGame() {
// reset the level index counter gameLevelNum = 0;
} }
2.2.2.1 Script Breakdown
Any script may call for a scene change, and menus, game controllers, or tutorial control- lers may easily move between sections of the game without having to have filenames hard- coded into them.
The level manager may also be a good place to call to show loading screens, implement scene transitions (such as fade ins/outs or animations), or start downloading scenes from a remote location.
Note that in the Start() function of the class (shown below), we use DontDestroyOnLoad to keep this script and its gameObject alive even when new scenes are loaded. This means we need to prevent duplicate scene managers being instanced, but we can easily use GameObject.Find to have a look for a scene manager gameObject before instantiating a new one.
public class SceneManager : MonoBehavior {
public string[] levelNames; public int gameLevelNum; public void Start () {
// keep this object alive
DontDestroyOnLoad (this.gameObject); }
The SceneManager.cs script provides a scene-loading function in its simplest form. Here Application.LoadLevel is used to load a scene:
public void LoadLevel( string sceneName ) {
Application.LoadLevel( sceneName ); }
On top of simple scene loading, the class has support for a basic progressive level load- ing game format. An array named levelNames holds strings referring to the names of each of the levels in a game. We use an integer variable named gameLevelNum to refer to which level the player is in. When GoNextLevel() is called, we do a quick check to make sure that gameLevelNum is within range:
public void GoNextLevel() {
// if our index goes over the total number of levels in the // array, we reset it
if( gameLevelNum >= levelNames.Length ) gameLevelNum = 0;
Then, another version of the LoadLevel function (declared after this part) is called to start loading the next scene based on the index number. Finally, gameLevelNum is incremented, ready for next time. Note that the increment happens after the load and not before. We could have done this either way, but this way, we have the minor inconvenience of gameLevelNum always being one step ahead, which we need to be aware of if we ever try to access its value from other classes. On the other hand, by incrementing it after the load, we use GoNextLevel right from the start of the game to load the first level in the array, keeping the code consistent in level loading:
// load the level (the array index starts at 0, but we start // counting game levels at 1 for clarity’s sake)
LoadLevel( gameLevelNum );
// increase our game level index counter gameLevelNum++;
19 2.2 Building the Core Framework Scripts
This alternative version of the LoadLevel function shown below uses an index to get a level name from the levelNames array. It then uses the original LoadLevel function declared earlier to handle loading. Again, keeping the focus on using a single function for all load- ings rather than duplicating the code or having loading happen in more than one place:
private void LoadLevel( int indexNum ) {
// load the game level
LoadLevel( levelNames[indexNum] ); }
The final function in the scene manager class is used to reset gameLevelNum when- ever we start a new game. It is assumed that the script restarting the game will call this function before calling GoNextLevel() again.
public void ResetGame() {
// reset the level index counter gameLevelNum = 0;
} }
2.2.3 ExtendedCustomMonoBehavior.cs
Extending MonoBehavior is a useful way to avoid repeating common functions or variable declarations. Many classes share functions and/or variables. For example, most of the scripts in this book use a variable named myTransform to refer to a cached version of their transforms to save having to look up the transform every time we need to access it. As MonoBehavior provides a host of system functions and calls, making a new class that extends, it can be a use- ful tool to have in the kit. Whereas a new class would use MonoBehavior, we just derive from the new ExtendedCustomMonoBehavior script instead.
For the scripts in this book, the ExtendedCustomMonoBehavior.cs script extends MonoBehavior to include
1. a myTransform variable to hold a cached reference to a transform 2. a myGO variable to hold a cached reference to a gameObject 3. a myBody variable for a cached reference to a rigidBody
4. a didInit Boolean variable to determine whether or not the script has been initialized
5. an integer variable called id to hold an id number
6. a Vector3 type variable called tempVEC to use for temporary vector actions 7. a Transform variable called tempTR used for any temporary references to
transforms
The scripting for this class is straightforward and contains mostly variable declarations:
public class ExtendedCustomMonoBehavior : MonoBehavior {
// This class is used to add some common variables to // MonoBehavior, rather than constantly repeating // the same declarations in every class.
public Transform myTransform; public GameObject myGO; public Rigidbody myBody; public bool didInit; public bool canControl; public int id;
[System.NonSerialized] public Vector3 tempVEC; [System.NonSerialized]
public Transform tempTR;
public virtual void SetID( int anID ) {
id= anID;
} }
2.2.4 BaseUserManager.cs
The user manager is a script object made by the player manager to store player properties such as ◾ Player name ◾ Current score ◾ Highest score ◾ Level ◾ Health
◾ Whether or not this player has finished the game
The user manager also contains functions to manipulate its data Below is the BaseUserManager.cs script in full:
public class BaseUserManager : MonoBehavior {
// gameplay specific data
// we keep these private and provide methods to modify them // instead, just to prevent any accidental corruption // or invalid data coming in
21 2.2 Building the Core Framework Scripts
private int score; private int highScore; private int level; private int health; private bool isFinished;
// this is the display name of the player public string playerName ="Anon";
public virtual void GetDefaultData() { playerName="Anon"; score=0; level=1; health=3; highScore=0; isFinished=false; }
public string GetName() {
return playerName;
}
public void SetName(string aName) {
playerName=aName; }
public int GetLevel() {
return level;
}
public void SetLevel(int num) {
level=num; }
public int GetHighScore() {
return highScore;
}
public int GetScore() {
return score;
}
public virtual void AddScore(int anAmount) {
score+=anAmount; }
public void LostScore(int num) {
score-=num; }
public void SetScore(int num) {
score=num; }
public int GetHealth() {
return health;
}
public void AddHealth(int num) {
health+=num; }
public void ReduceHealth(int num) {
health-=num; }
public void SetHealth(int num) {
health=num; }
public bool GetIsFinished() {
return isFinished;
}
public void SetIsFinished(bool aVal) {
isFinished=aVal; }
}
2.2.4.1 Script Breakdown
BaseUserManager.cs contains just variable declarations and functions to get or set those variables.
2.2.5 BasePlayerManager.cs
The player manager acts like glue among the input manager, the user manager, and the game-specific player controllers. It is intended to be a central point where managers are tied to game-specific player scripts.
Below is the BasePlayerManager.cs in full:
public class BasePlayerManager : MonoBehavior {
public bool didInit;
// the user manager and AI controllers are publically accessible so that // our individual control scripts can access them easily
23 2.2 Building the Core Framework Scripts
// note that we initialize on Awake in this class so that it is // ready for other classes to access our details when
// they initialize on Start public virtual void Awake () {
didInit=false;
// rather than clutter up the start() func, we call Init to // do any startup specifics
Init(); }
public virtual void Init () {
// cache ref to our user manager
DataManager= gameObject.GetComponent<BaseUserManager>(); if(DataManager==null)
DataManager= gameObject.AddComponent<BaseUserManager>(); // do play init things in this function
didInit= true; }
public virtual void GameFinished() {
DataManager.SetIsFinished(true); }
public virtual void GameStart() {
DataManager.SetIsFinished(false); }
}
2.2.5.1 Script Breakdown
BasePlayerManager.cs derives from MonoBehavior so that it can use the system function Awake() to call its Init() initialization:
public class BasePlayerManager : MonoBehavior {
The Awake() function is based on the assumption that it may be reused, so it starts out by setting the Boolean variable didInit to false. This variable can then be used to tell whether or not the Init() function has been called and completed execution:
public virtual void Awake () {
didInit=false;
// rather than clutter up the start() func, we call Init to do any // startup specifics
Init(); }
Init() starts out by looking to find a reference to a DataManager script component (BaseUserManager as explained earlier in this chapter). gameObject.GetComponent() will return a reference to BaseUserManager if it has been added to the gameObject that this script is attached to:
public virtual void Init () {
// cache ref to our user manager
DataManager= gameObject.GetComponent<BaseUserManager>();
At this point, if the DataManager variable is null, we know that the component has not been added to this gameObject, and this code goes on to use gameObject.AddComponent() to add a BaseUserManager component through the code
if(DataManager==null)
DataManager= gameObject.AddComponent<BaseUserManager>();
Now that the Init() function is done, didInit can be set to true:
// do play init things in this function didInit= true;
}
GameFinished() will be called from an outside script (possibly the game controller script or similar game-state management script) to tell this player when the game is over:
public virtual void GameFinished() {
DataManager.SetIsFinished(true); }
Just like GameFinished(), GameStart() will be called from an outside script when it is time to start the game. DataManager.SetIsFinished() is called to set the isFinished vari- able of this player’s data manager to false:
public virtual void GameStart() {
DataManager.SetIsFinished(false); }
}
2.2.6 BaseInputController.cs
The input controllers provide input for use by the player controller. The BaseInputController. cs looks like this:
public class BaseInputController : MonoBehavior {
// directional buttons public bool Up; public bool Down; public bool Left; public bool Right;
25 2.2 Building the Core Framework Scripts
// fire / action buttons public bool Fire1; // weapon slots public bool Slot1; public bool Slot2; public bool Slot3; public bool Slot4; public bool Slot5; public bool Slot6; public bool Slot7; public bool Slot8; public bool Slot9; public float vert; public float horz;
public bool shouldRespawn; public Vector3 TEMPVec3;
private Vector3 zeroVector = new Vector3(0,0,0); public virtual void CheckInput ()
{
// override with your own code to deal with input
horz=Input.GetAxis ("Horizontal");
vert=Input.GetAxis ("Vertical");
}
public virtual float GetHorizontal() {
// returns our cached horizontal input axis value
return horz;
}
public virtual float GetVertical() {
// returns our cached vertical input axis value
return vert;
}
public virtual bool GetFire() {
return Fire1;
}
public bool GetRespawn() {
return shouldRespawn;
}
public virtual Vector3 GetMovementDirectionVector() {
// temp vector for movement dir gets set to the value of an // otherwise unused vector that always has the value of 0,0,0 TEMPVec3=zeroVector;
// if we’re going left or right, set the velocity vector’s X // to our horizontal input value
{
TEMPVec3.x=horz; }
// if we’re going up or down, set the velocity vector’s X to // our vertical input value
if(Up || Down) {
TEMPVec3.y=vert; }
// return the movement vector
return TEMPVec3;
} }
2.2.6.1 Script Breakdown
BaseInputController.cs derives from MonoBehavior:
public class BaseInputController : MonoBehavior {
The input scripts use CheckInput() as their default main update function, which is intended to be called from the player class. This CheckInput() only contains the very least amount required for input. It just takes the axis input from the Input class:
public virtual void CheckInput () {
The Input class provides an interface to Unity’s input systems. It may be used for everything from gyrometer input on mobile devices to keyboard presses. Unity’s input system has a virtual axis setup, where developers can name an axis in the Input Manager section of the Unity editor. The Input Manager is available in Unity via the menus Edit –> Project Settings –> Input.
By default, Unity has inputs already set up for horizontal and vertical virtual axis. Their names are Horizontal and Vertical, and they are accessible via Input.GetAxis(), passing in the name of the virtual axis as a parameter and receiving the return value of a float between −1 and 1. The float variable horz and vert hold the return values:
// override with your own code to deal with input
horz=Input.GetAxis ("Horizontal");
vert=Input.GetAxis ("Vertical");
}
A function is provided to return the value of horz, the horizontal axis provided by the virtual axis named Horizontal:
public virtual float GetHorizontal() {
// returns our cached horizontal input axis value
return horz;
27 2.2 Building the Core Framework Scripts
A function is provided to return the value of vert, the vertical axis provided by the virtual axis named Vertical:
public virtual float GetVertical() {
// returns our cached vertical input axis value
return vert;
}
GetFire() returns the value of the Boolean variable Fire1. GetRespawn() returns the value of the Boolean variable shouldRespawn. At this stage, you may notice that there is no actual code to set either Fire1 or shouldRespawn in this default input script. They are both provided as a guide and with the intention of being built upon on a case-by-case basis depending on what the game requires:
public virtual bool GetFire() {
return Fire1;
}
public bool GetRespawn() {
return shouldRespawn;
}
It may be useful in some circumstances to have a movement vector rather than hav- ing to process the numbers from horz and vert. GetMovementDirectionVector() will return a Vector3 type vector based on the current state of input. Here, TEMPVec3 receives input values and gets returned by the function
public virtual Vector3 GetMovementDirectionVector() {
// temp vector for movement dir gets set to the value of an // otherwise unused vector that always has the value of 0,0,0 TEMPVec3=zeroVector;
The vector starts out at 0,0,0, but its x and y values will be set to the values of horz and vert, respectively. Finally, the vector in TEMPVec3 is returned:
TEMPVec3.x=horz; TEMPVec3.y=vert;
// return the movement vector
return TEMPVec3;
} }
29
3
Player Structure
A player can take just about any form—humans, aliens, animals, vehicles; designing a struc- ture that can deal with all of them is a challenge in itself. The way that players store data, the data they need to store, the various types of movement codes and their control systems make for hundreds of different possible combinations. Dealing with a player structure in a modular way requires a little careful consideration not just for how to deal with all these different sce- narios but also for how our components will need to communicate with each other and com- municate with the rest of the game—for example, player objects often need to communicate with the game controller, the game controller often needs to communicate with players (to relay game states, etc.), and players may also need to interact with the environment and other objects within it.
The overall player structure for this book may be broken down into several main components, as shown in Figure 3.1.
1. Game-specific player controller. This is a script to deal with game-specific player
actions. For example, some games may require a vehicle that requires weapons, whereas some games may not. The game-specific control script will “add on” the specific extra functionality and tie together the main components to work together or communicate with other game-specific elements.
2. Movement controller. The movement controller takes the job of moving the player
around and defines the type of player we are using. For example, a vehicle-based player would have a vehicle-based movement control script that would drive the
wheels, turn them, or operate extra vehicle-specific functions like a horn, head- lights, etc. A human would require a code to drive a character controller. A space- ship might use physics forces to drive a spaceship.
3. Player manager. This script is intended to be a bridge between the player control-
ler and the user data manager. The user data manager takes care of storing player stats, such as score, lives, level, etc.
4. Data manager. The data manager takes care of the manipulation of player data
and how the data are stored and retrieved. Score and health values would be stored here, and the data manager would also provide functions for saving and loading its data.
Along with these main components, we can add some extra components to mix and match player types into different forms. A weapon control script may be attached to a human to create the player in a first-person shooter, or the same weapon control script could be added to a vehicle-type player to help turn it into a tank. An AI control script may be added to drive the player or to operate the weapons and so on.
In this chapter, we will be going through some of the main components that will be used later in the book to create a user-controlled player.
Player User manager Player manager Input controller Slot-based weapon system
AI controller movement controllerWheeled vehicle
Path waypoint controllers
Waypoint gameObjects
Class derived from
31 3.1 Game-Specific Player Controller
3.1 Game-Specific Player Controller
The player controller derives from a movement controller (which specializes in nothing more than movement of a particular player type) and builds on its movement functional- ity to provide full player logic.
The movement controllers are explained later in this book. Chapter 5 outlines three different movement controllers for three different player types: a spaceship, a humanoid player, and a wheeled vehicle.
What logic goes into the player controllers depends on the game; the example games in this book use the movement controllers from Chapter 5 and add extra functionality to them, such as combining the movement controller with the weapon controllers to make armed players or combining the AI controller to have computer controller players.
For real examples of player controllers, take a look at the example games in Chapters 10 to 13.
In this section, we build a simple player controller to move a spaceship. It derives from BaseTopDownSpaceShip.cs, which can be found in full in Chapter 5. This script also includes a player manager.
using UnityEngine; using System.Collections;
public class SimplePlayerController : BaseTopDownSpaceShip {
public BasePlayerManager myPlayerManager; public BaseUserManager myDataManager; public override void Start()
{
// tell our base class to initialize
base.Init ();
// now do our own init this.Init();
}
public override void Init ()
{
// if a player manager is not set in the editor, let's try // to find one if(myPlayerManager==null) myPlayerManager= myGO.GetComponent<BasePlayerManager>(); myDataManager= myPlayerManager.DataManager; myDataManager.SetName("Player"); myDataManager.SetHealth(3); didInit=true; }
public override void Update () {
// do the update in our base
UpdateShip ();
if(!didInit) return;
// check to see if we're supposed to be controlling the // player before checking for firing
if(!canControl) return; }
public override void GetInput () {
// we're overriding the default input function to add in the // ability to fire
horizontal_input= default_input.GetHorizontal();
vertical_input= default_input.GetVertical();
}
void OnCollisionEnter(Collision collider) {
// React to collisions here }
void OnTriggerEnter(Collider other) {
// React to triggers here }
public void PlayerFinished()