CRC Csharp Game Programming Cookbook For Unity 3D 2nd Edition 036732170X
CRC Csharp Game Programming Cookbook For Unity 3D 2nd Edition 036732170X
Jeff W. Murray
Second Edition published 2021
by CRC Press
6000 Broken Sound Parkway NW, Suite 300, Boca Raton, FL 33487-2742
and by CRC Press
2 Park Square, Milton Park, Abingdon, Oxon, OX14 4RN
First Edition published by CRC Press 2014
CRC Press is an imprint of Taylor & Francis Group, LLC
© 2021 Taylor & Francis Group, LLC
The right of Jeff W. Murray to be identified as author of this work has been asserted by
him/ in accordance with sections 77 and 78 of the Copyright, Designs and Patents Act
1988.
Reasonable efforts have been made to publish reliable data and information, but the
author and publisher cannot assume responsibility for the validity of all materials or the
consequences of their use. The authors and publishers have attempted to trace the copy-
right holders of all material reproduced in this publication and apologize to copyright
holders if permission to publish in this form has not been obtained. If any copyright
material has not been acknowledged please write and let us know so we may rectify in any
future reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted,
reproduced, transmitted, or utilized in any form by any electronic, mechanical, or other
means, now known or hereafter invented, including photocopying, microfilming, and
recording, or in any information storage or retrieval system, without written permission
from the publishers.
For permission to photocopy or use material electronically from this work, access www.
copyright.com or contact the Copyright Clearance Center, Inc. (CCC), 222 Rosewood
Drive, Danvers, MA 01923, 978-750-8400. For works that are not available on CCC please
contact mpkbookspermissions@tandf.co.uk
Trademark notice: Product or corporate names may be trademarks or registered trade-
marks and are used only for identification and explanation without intent to infringe.
ISBN: 978-0-367-32170-3 (hbk)
ISBN: 978-0-367-32164-2 (pbk)
ISBN: 978-0-429-31713-2 (ebk)
Typeset in Minion
by SPi Global, India
This book is dedicated to my
amazing wife, Tori, and to
my boys, Ethan and William.
Boys, be nice to the cat and
the cat will be nice to you!
Contents
Acknowledgments xiii
Introduction xv
Prerequisites xvii
vii
2. Making a 2D Infinite Runner Game 9
2.1 Anatomy of a Unity Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Open Unity, Then the Example Project for This Chapter . . . . . . . . . . 10
2.2.1 A Few Notes on the Example Project . . . . . . . . . . . . . . . . . .12
2.2.2 Sprites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.3 Animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3 Open the Main Game Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 The Game Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.5 Making Platforms to Run On . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6 Building the Player, RunMan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6.1 Adding the RunMan Sprite . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6.2 Adding Physics and Collisions to RunMan . . . . . . . . . . . . 23
2.6.3 Player Scripts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6.4 RunMan Sounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.5 Animating RunMan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.6.6 Scoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.7 The Game Loop and the Game Manager . . . . . . . . . . . . . . . . . . . . . . . 28
2.7.1 Game States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.7.2 The Main Loop of RunMan_GameManager.cs . . . . . . . . . 29
2.7.3 Setting Up the RunMan_Game Manager
Component in the Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.8 The Demise of RunMan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.9 Adding the User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.9.1 Main Menu Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4. Player Structure 61
4.1 A Player Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.2 Dealing with Input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.3 User Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.3.1 The UserData Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.3.2 The BaseUserManager Class . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.4 The BasePlayerStatsController Class . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.5 Managing Players . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
viii Contents
5.3 Camera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.3.1 Third-Person Camera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.3.2 Top-Down Camera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.4 Game Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.4.1 GlobalRaceManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.4.2 RaceController . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.5 Input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5.1 Mouse Input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.6 Level Loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
5.7 ScriptableObjects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.7.1 ProfileScriptableObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.8 Spawning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.8.1 A Spawner Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.8.2 Trigger Spawning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
5.8.3 Timed Spawning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.9 User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.9.1 CanvasManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.9.2 ScreenandAudioFader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.9.3 MenuWithProfiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.10 Utility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.10.1 AlignToGround . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5.10.2 AutomaticDestroyObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
5.10.3 AutoSpinObject . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
5.10.4 FaceCamera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
5.10.5 LookAtCamera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
5.10.6 PretendFriction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
5.10.7 TimerClass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
5.10.7.1 Modifying the Timer to Update
Automatically����������������������������������������������������100
5.10.8 WaypointsController . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.11 Weapons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Contents ix
9. Recipe: Sound and Audio 145
9.1 Audio Mixers and Mixing Audio in Unity . . . . . . . . . . . . . . . . . . . . . 145
9.1.1 Mixing Audio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
9.1.2 Exposing Mixer Properties to Modify Them with Scripts . . 146
9.1.3 Audio Effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
9.2 The BaseSoundManager Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
9.3 Adding Sound to Weapons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
x Contents
14.4.5 Audio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
14.4.5.1 Vehicle Audio����������������������������������������������������259
14.4.5.2 Incidental Sounds����������������������������������������������259
14.4.6 User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Index 283
Contents xi
Acknowledgments
Thanks to all of my family, friends and everyone who has helped me with my
books, games and projects over the years: Brian Robbins, Chris Hanney,
Technicat, Molegato, NoiseCrime, Quantum Sheep, Mickael Laszlo, Juanita
Leatham, CoolPowers, Dave Brake, Sigurdur Gunnarsson, Kevin Godoy R.,
Lister, Wim Wouters, Edy (VehiclePhysics), IdeaFella, Liam Twose, RafaEnd,
Lynxie, Alistair Murphy, McFunkypants, Dani Moss, Malcolm Evans, Vinh,
Scott Wilson, Jon Keon, Dunky, Huck Z, Paradise Decay, Kryten, Silverware
Games, Pete Patterson, Dr. Not Oculus VR, Say Mistage, Andy Hatch, Lydia, RT
Warner, GermanRifter VR, Sylvain Demers, Kenneth Seward Jr, Dancer (Space
Dad), Steve Fulton, Paul Bettner, Derek Scottishgeeks, Jonny Gorden, Edward
Atkin, Ottawa Pete, Sam W., Dylan Stout, Shane McCafferty, Will Goldstone,
Sanborn VR, Gary Riches, J. Dakota Powell, Mayonnaise Boy, Stephen Parkes,
Ram Kanda, Alex Bethke, Itzik Goldman, Joachim Ante, Robert Scoble, Tony
Walsh, Andreas ‘BOLL’ Aronsson, Cat, Darrel Plant, Mike Baker, Rimsy, Cassie,
Christopher Brown, Phil Nolan, Pixel Hat Studio, Marcus Valles, Trev, Karyl,
Tami Quiring, Nadeem Rasool, Dwayne Dibley, Liz and Pete Smyth, Isaac and
Aiden, David Helgason, VR Martin, James Gamble, Vasanth Mohan, Simona
Ioffe, Alexander Kondratskiy, Tim and Paul, The Oliver Twins, Jeevan Aurol,
Rick King, Aldis Sipolins, Ric Lumb, Craig Taylor, Rob Hewson, Dani Moss,
Jayenkai (JNK), Matthew Kirubakaran, Elliot Mitchell, Ethan and William,
Pablo Rojo, Paul Bettner, AdrellaDev, Gordon Little, Ryan Evans, Sasha Boersma,
Matt Browning at Perfect Prototype, Hermit, Dirty Rectangles and the whole
Ottawa game dev community.
xiii
I would also like to sincerely thank Anya Hastwell, Thivya Vasudevan and
the team at SPI Global, the whole team at Routledge/CRC Press/AK Peters,
including Randi Cohen, Jessica Vega and Rick Adams, for making this book a
reality.
Thank you for buying this book and for wanting to do something as cool as
to make games. I wish I could tell you how awesome it feels to know that someone
else is reading this right now. I cannot wait to see your games and I sincerely hope
this book helps you in your game making adventures. Have fun making games!
xiv Acknowledgments
Introduction
The overall goal of this book is to provide a library of C# code with which to
jumpstart your projects and to help you with the overall structure of your games.
Many development cookbooks focus on only providing snippets of code, but,
here, we take a different approach. What you are holding in your hands right now
(or on your screen) is a cookbook for game development that has a highly flexible
core framework designed to speed up development of just about any type of Unity
project.
You might think of the framework as a base soup and the scripting compo-
nents as ingredients. We can mix and match script components and we can share
the same core scripts in many of them. The framework takes care of the essentials
and we add a little extra code to pull it all together the way we want it to work.
The framework is optional, however – you can use a lot the components indi-
vidually. If you intend on using the components in this book for your own games,
the framework could either serve as a base to build your games on or simply as a
tutorial test bed for you to rip apart and see how it all works. Perhaps you can
develop a better framework or maybe you already have a solid framework in
place. If you do find a way to develop your own framework, I say do it. The key to
game development is to do what works for you and your game projects – what-
ever it takes to cross the finish line.
I hope it helps you to make your games and tell your stories. I also hope you
remember to try to have fun doing it!
xv
Prerequisites
You can get up and running with the required software for the grand total of zero
dollars. Everything you need can be downloaded free of charge with no catches.
All you need is:
C# programming knowledge.
This is not a book about learning how to program. You will need to know
some C# and there are several other books out there for that purpose, even if I
have tried to make the examples as simple as possible!
xvii
1 Making Games in a
Modular Way
1.1.1.1 Managers
Managers deal with overall management, in a similar way to how a Manager
would work in a workplace situation.
1.1.1.2 Controllers
Controllers deal with systems that the managers need to do their jobs. For exam-
ple, in the racing game example game for this book, we have race controller
scripts and a global race manager script. The race controller scripts are attached
to the players and track their positions on the track, waypoints, and other rele-
vant player-specific race information. The global race manager script talks to all
1
the race controller scripts attached to the players to determine who is winning
and when the race starts or finishes.
1. Direct referencing scripts via variables set in the editor by the Inspector
window.
The easiest way to have script Components talk to each other (that is, scripts
attached to GameObjects in the Scene as Components) is to have direct refer-
ences, in the form of public variables within your code. They can then be popu-
lated in the Inspector window of the Unity editor with a direct link to another
Component on another GameObject.
Above, this would call the function DoSomething() on any of the script
Components attached to the GameObject referenced by someGameObject.
3. Static variables.
The static variable type makes a variable accessible to other scripts without a
direct reference to the GameObject it is attached to. This is particularly useful
behavior where several different scripts may want to manipulate a variable to do
things like adding points to a score or set the number of lives of a player, and so on.
An example declaration of a static variable might be:
public static GameManager aManager;
1.1.1.4 Public Static
A public static variable exists everywhere and may be accessed by any other
script.
For example, imagine a player script which needs to tell the Game Manager
to increase the current score by one:
2. In any other script, we can now access this static variable and alter the
score as needed:
GameController.gameScore++;
When a player script is first created, it uses the value of uniqueNum for itself
and increases uniqueNum by one:
myUniqueNum = uniqueNum;
uniqueNum++;
The value of uniqueNum will be shared across all player scripts. The next
player to be spawned will run the same start up function, getting uniqueNum
again – only this time it has been increased by one, thanks to the player spawned
earlier. This player again gets its own unique number and increases the static
variable ready for the next one.
1.1.2 The Singleton
A singleton is a commonly used method for allowing access to an instance of a
class, accessible to all other scripts. This pattern is ideal for code that needs to
communicate with the entire game, such as a Game Manager.
instance = this;
}
}
1.1.3 Inheritance
Inheritance is a complex concept, which calls for some explanation here because
of its key role within the scripts provided by this book. Have a read through this
section but don’t worry if you don’t pick up inheritance right away. Once we get
to the programming it will probably become clearer.
Car–
–Wheels
–Engine
Car class.
Wheels function
Engine function
If we were building a game with lots of cars in it, having to rewrite the car class for
each type of car would be silly. A far more efficient method might be to write a base
class and populate it with virtual functions. When we need to create a car, rather
than use this base class, we build a new class, which inherits the base class. Because
our new class is inherited, it is optional whether we choose to override wheels or
engine functions to make them behave in ways specific to our new class. That is, we
can build ‘default’ functions into the base class and if we only need to use a default
behavior for an engine, our new class doesn’t need to override the engine function.
A base class might look something like this:
public class BaseCar : MonoBehaviour {
There are two key things to notice in the above script. One is the class declara-
tion itself and the fact that this class derives from MonoBehaviour. MonoBehaviour
is itself a class – the Unity documentation describes it as “the base class every
script derives from” – this MonoBehaviour class contains many engine-specific
functions and methods such as Start(), Update(), FixedUpdate(), and more. If our
script didn’t derive from MonoBehaviour it would not inherit those functions and
the engine wouldn’t automatically call functions like Update() for us to be able to
work with. Another point to note is that MonoBehaviour is a class that is built into
the engine and not something we can access to edit or change.
The second point to note is that our functions are both declared as virtual
functions. Both are public, both are virtual. Making virtual functions means that
the behavior in our base class may be overridden by any scripts that derive from it.
The behavior we define in this base class could be thought of as its default behav-
ior. We will cover overriding in full a little further on in this section.
The first thing you may notice is that the OctoCar class derives from BaseCar
rather than MonoBehaviour. This means that OctoCar inherits functions and meth-
ods belonging to our BaseCar script. As the functions described by BaseCar were
virtual, they may be overridden. For OctoCar, we override Wheels with the line:
public override void Wheels () {
Let’s take a look at what this script actually does: In this case, if we were to
call the Engine() function on OctoCar, it would do the same as the BaseCar class;
it would write “Vroom” to the console. It would do this because we have inherited
the function but have not overridden it, which means we keep that default behav-
ior. In OctoCar, however, we have overridden the Wheels() function. The BaseCar
behavior of Wheels would print “Four wheels” to the console but if we call
Wheels() on OctoCar, the overridden behavior will write “Eight wheels,” instead.
Inheritance plays a huge part in how our core game framework is structured.
The idea is that we have basic object types and specific elaborated versions of
these objects inheriting the base methods, properties, and functions. By building
our games in this manner, the communication between the different game com-
ponents (such as game control scripts, weapon scripts, projectile controllers, etc.)
becomes universal without having to write out the same function declarations
over and over again for different variations of script. For the core framework, our
main goal is to make it as flexible and extensible as possible and this would be a
much more difficult if we were unable to use inheritance.
1.1.4 Coroutines
Unity lets you run a function known as a coroutine, outside of the regular built
in functions like Update, FixedUpdate, LateUpdate, and so on. A coroutine is
self-contained code that will just go off and do its own thing once you have told
it to run. As it is running on its own, you can do some cool stuff like pause it for
a set amount of time and then have it start again, and coroutines are ideally suited
to time-based actions like fade effects or animations.
Here is an example of a coroutine (this example code comes from the Unity
engine documentation):
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return null;
}
}
Do not be alarmed by this odd code above! All it does is tell the engine that,
at this point, the coroutine is ready to end this update and go on to the next one.
You need this in your code, but don’t worry too much about why at this stage.
I will go into some more detail below, but for now let us continue by looking at
how you would start this coroutine running:
StartCoroutine("Fade");
You can think of IEnumerator like a cursor making its way through your
code. When the cursor hits a yield statement, that tells the engine that this update
is done and to move the cursor on to the next block.
Another thing you can do with coroutines is to pause the code inline for a
certain amount of time, like this:
yield return new WaitForSeconds(1.0f);
Using WaitForSeconds(), you can quite literally pause the coroutine script
execution inline. For example:
IEnumerator DebugStuff()
{
Debug.Log("Start.");
yield return new WaitForSeconds(1.0f);
Debug.Log("Now, it’s one second later!");
Earlier in this section, I mentioned that the yield statement is used to tell
Unity the current update is done and to move on to the next. In the code above,
rather than just telling Unity to move on to the next bit of code, it tells Unity to
wait for the specified number of seconds first.
1.1.5 Namespaces
Namespaces compartmentalize chunks of code away from each other. You can
think of a namespace as a box to put your own scripts in, so that they can stay
separated from other code or code libraries. This helps to prevent possible
namespace MyNamespace
{
public class MyClass() : Monobehaviour
{
}
When you want to refer to the code from another class, you need to be either
wrapped in the same namespace or to have the using keyword at the top of the
script, like:
using MyNamespace;
Switch(currentGameState)
{
Case GameStates.GameLoaded:
GameLoaded();
Break;
Case GameStates.GameStarting:
GameStart();
Break;
Having code in each case statement can get unruly. To counter this, I like to
split all of the code out into individual functions. This keeps the code tidy, easier
to manage, and easier to debug when things do not go to plan.
Where scripts need to be initialized before they can be used, in this book we
always use a Boolean variable named didInit which gets set to true after initial-
ization. You can use didInit to make sure initialization has completed.
Many programmers frown on the idea of declaring temporary variables, but
I like to have _tempVEC and _tempTR variables available for whenever I need to
refer to a quick Vector3 or another Transform inline. Having these variables
already declared is just a little quicker than having to declare a new Vector3 each
time, or to make a new Transform variable.
This chapter is different from the rest of the book and a dramatic diversion from
what you may have seen in its first edition. Elsewhere in the text, I focus mainly on
the code behind the games. This chapter offers up a step by step tutorial to using the
framework to make a 2D infinite runner game. The goal of this chapter is to dem-
onstrate two things: 1) How quickly you can turn around a game when you have a
framework in place that takes care of a lot of the repetitive tasks. 2) The basics of
how this books framework fits together to give you some background knowledge
before we get down into nitty gritty of the framework code in Chapter 3.
This infinite runner (Figure 2.1) has a character that can move left, right, and
jump. Platforms are spawned off-screen and moved to the left to create the illu-
sion of movement.
I have already set up animations and imported the required graphics into the
example project to get things going quickly. As the focus of this book is more
toward code, project structure, and programming, I want to avoid using up too
many pages on making graphics or on Unity editor-specifics. The Unity docu-
mentation features plenty of content on this.
Players jump and move to stay on the platforms if possible, with the score incre-
mented at timed intervals. If the player falls off the bottom of the screen, the game ends.
To accomplish this, we will need:
3. Platforms (auto moving to the left) and a method to spawn platforms off
screen
9
Figure 2.1 In the Infinite Runner example game, players jump and run to try to stay on the
platforms for as long as possible.
4. A Game Manager script to keep track of game state and deal with scor-
ing and so forth
6. An animated character
With just the framework and assets, by the end of this chapter, we will have
a working game.
complete example as a part of the other example project found in the folder
named All_Completed_ProjectFiles.
To keep everything in line with the information in this section, you should
change the editor layout. In the top right of the editor, find the Layout dropdown
button. Click on it and choose 2 by 3. After a short delay, the editor should arrange
its panels into the default 2 by 3 layout.
Now that we have the editor laid out in similar ways, let’s look through each
panel to make sure we are using the same terminology. Check out Figure 2.2 –
outlined below are the different sections shown in the figure, what those sections
are and what they are for:
A. Scene
The Scene panel is your drag and drop visual window into a Unity Scene.
You can manipulate GameObjects directly inside the currently loaded
Scene.
B. Game
The Game panel shows a preview of the game. When you press Play, the
editor runs the game inside the Game panel as if it were running in a
standalone build. There are also a few extra features, such as Gizmos, that
the Game preview offers to help you build out and debug your games.
C. Project
The Project panel is very similar to a file browser, in that it lists out all
the files that go up to make your project. You can click and drag them,
right click, and access a menu, use shortcuts, and delete them – almost
all the regular functionality of a Windows Explorer window right inside
the editor.
D. Hierarchy
The Hierarchy works in a similar way to the Project panel, only instead
of being an interface to manipulate project files, it’s there to manipulate
GameObjects inside the current Scene.
E. Inspector
Whenever you have a GameObject selected in the Scene panel, or high-
lighted in the Hierarchy, the Inspector window shows you editable prop-
erties of the GameObject and any Components attached to it. Whatever
you can’t do with the Scene panel, you can probably do here instead.
Another point of interest in the main Unity editor are along the toolbar
across the top of the editor, below the menus.
In the top left, find the Transform toolbar (Figure 2.3). This toolbar is to
manipulate objects in the Scene panel. Tools, from left to right:
The Hand Tool Allows you to pan the view around the Scene.
Move Selection and movement of GameObjects.
Rotate Selection and rotation of GameObjects.
Scale Selection and scaling of GameObjects.
Rect Transform Modify a GameObjects boundaries as a Rect (combined scaling and movement).
Transform A method to manipulate scale, position, and rotation in a single, combined tool.
Custom Th is tool allows access to custom Editor tools and will not be covered in this
book.
At the center of the toolbar, find the Play, Pause and Step buttons (Figure 2.4)
which are used to preview your game and to control what happens in the Game
panel.
2.2.2 Sprites
2D sprites may be found in the projects Assets/Games/RunMan/Sprites folder.
The sprites were made with a sprite editing program called Aesprite and exported
as sprite sheets. Sprite sheets are groups of sprites which are split up into indi-
vidual sprites by the engine. We tend to use sprite sheets for animations, export-
ing multiple frames of animation in a single image to save memory and having
2.2.3 Animation
Animation is a complicated subject and outside the scope of this book, but this
section acts as a quick guide on how the animations for this game works.
Look out for RunMan_Animator in the Sprites folder. You can open it to view
by double clicking on the RunMan_Animator file in the Project pane. An Animator
contains a state machine graph (Figure 2.5) which controls the flow of animation.
When an animated GameObject is initialized, the state machine graph begins in
its Entry state and will transition to the default state. In Figure 2.5, you can see that
RunMan_Animator has a default state of RunMan_Idle (that is, an arrow goes
from Entry to RunMan_Idle) – this will play an idle animation for whenever
RunMan is standing still. The animation is looped and there is no further flow (no
more arrows pointing to boxes!) out from RunMan_Idle in the Animator graph;
so, until told otherwise, the animation will stay in the idle state.
In the graph (Figure 2.5) look for the Any State state – this literally means any
state in that this state can be triggered from any animation. The Any State has two
arrows coming off it; one pointing to RunMan_Jump state and the other to a
RunMan_Run state. So, how do we get the animation to change to run or jump
when the Animator graph has no link from the endlessly looping RunMan_Idle
state? Parameters are the answer! Parameters are variables that can be set and
modified through code and used as Conditions by the Animator graph to control
flow. In the case of Any State, whenever a Jump parameter is triggered the Animator
will transition from Any State (that is, whatever state the graph is currently in) to
RunMan_Jump. Whenever a Run parameter is triggered, the Animator will transi-
tion from Any State to RunMan_Run. RunMan_Run is a looping animation that,
again, will play until the Animator graph is told to do otherwise.
In conclusion, the animation system for this game is set up to be as straight-
forward as possible. The Animator can be controlled by two parameters: Jump
and Run. In our code, to transition to a run animation we just set the Run param-
eter. To play a jump animation, we set the Jump parameter.
Games
RunMan
Audio
Prefabs
Scenes
Scripts
Sprites
Stay as organized as possible as you move forward with game projects. This will
help a lot as your project grows (and, often, they grow much bigger than you
might expect!) to stay on course and to spend less time looking for lost assets.
In the Project panel, click on the Scene folder to show its contents. There is a
single Scene inside that folder, named runMan_core. This is the Scene that will
contain the main game. There will also be a main menu Scene, which we will add
later in this chapter.
Double click on the runMan_core Scene in the Project panel, to open it. The
Game panel will show the user interface already set up (Figure 2.6) but there’s no
other logic in the Scene yet, so pressing Play will do nothing. By using the frame-
work, at the end of this chapter you will have a fun infinite runner game to play
here instead. The books game framework takes care of most of the heavy lifting
Figure 2.6 To save time, the User Interface is already set up in the game Scene of the
example project for this chapter.
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
The framework in this book exists within a namespace called GPC, so the
first thing we need to do to be able to access everything inside the framework is
to add a using statement to the top of the script. Below using UnityEngine, add
the following line:
using GPC;
The framework includes a base version of a Game Manager script that we can
inherit (see Chapter 1 for a brief guide to inheritance) to give this script some of
the useful functionality we will need as the game grows. Let’s change our new
Game Manager script. The base script is called BaseGameManager. Change:
public class RunMan _ GameManager : MonoBehaviour
To:
public class RunMan _ GameManager : BaseGameManager
Press CTRL+S to save the script, or save the script via the menus File>Save.
Remember when we first created the GameManager through the Add
Component button in the Inspector and the new Component appeared as some-
thing called (Script)? Flip back to the editor and you will see we now have a whole
host of new things in the Inspector. The BaseGameManager script contains lots
What this does is to make a variable that is accessible from anywhere because
it’s public and static. As the constructor runs when a GameManager instance gets
created, our public static variable gets populated with a reference to ‘this’ instance.
Other scripts can now talk to this instance by referring to its static variable like this:
RunMan _ GameManager.instance
Going in via this instance variable, any other script in your game can call
functions on RunMan_GameManager, access public variables, or set public vari-
ables. Just make sure that only one instance of RunMan_GameManager ever
exists at any time, otherwise you may find a script trying to access the wrong
object. If you do find yourself making more complicated scenarios that could lead
to multiple instances you may want to add check to see if the instance variable is
already populated before setting it. In the case of this game, we only ever need
this single instance to be attached to a single GameObject in a single Scene and
this is enough.
In the Scene panel, the green line can now be moved around. Hover the mouse
over the green line and a small Gizmo block appears. By clicking and dragging
the block, the green line will move. First, grab a point at the left side and move it
up. Then, grab a point on the right and do the same so that the green line aligns
perfectly with the top of the platform.
To complete the platform set up, just two more Components need to be
added to it. In the Inspector, click Add Component, and find and add an Auto
Destroy Object Component. In the field labeled Time Before Object Destroy,
type 10. We want this object to delete itself after 10 seconds. The RunMan_
Platform Components should now look like those in Figure 2.8.
The second Component we are going to build ourselves. Click Add Component
and type AutoTransformMove. Click New Script and then Create and Add to cre-
ate a new C# script and add it to the GameObject.
Once the new script is attached to the GameObject and visible in the
Inspector, double click on the name of the script to open it in your script editor.
In your script editor, add the following line just above the class declaration and
below the existing using statements:
using GPC;
To move the platforms, this script requires three variables. Add these just
inside the class declaration:
When the script first starts, we need to grab a reference to the GameObject’s
Transform. A Transform is a built-in Component we use for modifying position,
rotation, and scale.
Scripts that derive from Monobehaviour (on any level) can contain functions
that will be automatically called by the game engine. The Start() function will be
called when the game starts. Change the Start() function to look like this:
void Start()
{
// cache the transform ref
_TR = GetComponent<Transform>();
}
In the code above, moveSpeed gets its value from the runSpeed variable you
made in RunMan_GameManager earlier in this chapter. It uses that Singleton
variable instance, to gain access to it.
Beneath that, we use the Translate function (part of Transform) to move
the platform. moveVector is a Vector3 typed variable which holds a direction
vector used to define the direction of movement of the object. This gets multi-
plied by the moveSpeed to give us a Vector suitable for Translate that will
change with the speed in RunMan_GameManager. It is then multiplied by
Time.deltaTime. Unity provides some a helper class named Time to help track
or manipulate how the engine deals with time. deltaTime is a variable holding
the amount of time that has passed since the last frame Update, so we can use
that deltaTime amount to make movement that adapts to time rather than being
locked to framerates. This is a better idea than just move x amount each frame,
because if the system does something in the background that causes the game
to stall or if your game is running slower than it should on another system, the
movement won’t work as intended. By having movement that adapts to time
elapsed, if things run slow or there are glitches it will move the same amount
even if it needs to jump a bit.
Finally, add this function below the Update() function above the final curly
bracket in the script:
Above, we just add a simple method for another script to tell this script how
fast or slow to move. This will be used a little further in this chapter, called at the
start of the game to tell the starting platforms to move once the game begins.
Press CTRL+S to save your script and go back to Unity.
With RunMan_Platform selected in the Hierarchy, look to the Auto
Transform Move Component you made, leave Move Speed at 0 but change Move
Vector so that X is –1, leaving Y and Z at 0.
Finally, we need to set up the Layer of the platform so that it can be detected
as ground. In the top right of the Inspector, just below the name of the GameObject
to the right, there is a dropdown menu labeled Layer (Figure 2.9). Click the drop-
down and choose Ground from the list.
Press Play in the editor. The platform should zip off to the left of the screen.
If it did, nice job! That is how it is supposed to behave. Stop playback by clicking
the Stop button in the editor.
Now that the platform has everything it needs for the game, we need to turn
it into a Prefab so that it can be spawned by another script whenever we need one.
To turn the platform into a Prefab, click and drag RunMan_Platform from the
Hierarchy into the Games/RunMan/Prefabs folder in the Project panel.
At the start of the game, RunMan should have three platforms to run
across before the jumping starts so that the player has a chance to acclimatize
to the environment. The last thing to do in this section is to duplicate the plat-
form to make another two that will go alongside the one you already have in
the Scene.
In the Hierarchy, right click on RunMan_Platform and choose Duplicate
from the menu. A new GameObject should appear in the Hierarchy named
RunMan_Platform (1) but you will not see any difference in the Scene or Game
panels because it is currently in the exact same place as the original platform.
Move it to the right by clicking on the red (X axis) arrow on the Gizmo shown in
the Scene panel. Drag this duplicate platform to a position where the top of it is
touching the original platform so there is no gap for the player to fall through
(as Figure 2.10).
Now duplicate another platform to go to the right of the one you just added.
In the Hierarchy, right click RunMan_Platform and choose Duplicate. Move it to
Figure 2.10 The three starting platforms are set up in the runMan_core Scene.
Offset:
X: 0
Y: 0.01
Size:
X: 0.04
Y: 0.15
Direction:
Vertical
Once you have entered in all the numbers via the Inspector, compare the
shape of your capsule to the one in Figure 2.12 – if it looks good, move on to the
next stage of adding player scripts.
2.6.3 Player Scripts
The structure of players within the framework will be explained in depth in the
next chapter. For this section, however, we will just focus on setting up a basic
player and getting things ready for this specific game.
Make sure the RunMan GameObject is selected in the Hierarchy or click on
it to highlight it. Click the Add Component button in the Inspector. Click New
Script and type RunMan_CharacterController as the script name, and click
Create and Add.
Double click on the RunMan_CharacterController name to open the script
in your script editor. Replace the default script with this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GPC;
void Start()
{
Init();
}
isRunning = false;
allow_jump = true;
allow_right = true;
allow_left = true;
_soundControl = GetComponent<BaseSoundController>();
}
As a side note: Above, this script derives (meaning it inherits functions and
variables from) another script named BasePlayer2DPlatformCharacter. This
script is explained in full in Chapter 6, Section 6.4. 2D Platform Character
Controller.
With this script Component in place, RunMan will be able to move left and
right and jump. It will even play sounds. Save the script with CTRL+S or File/
Save, then return to Unity.
Look over in the Inspector and you will see that not only did it add the Run
Man_Character Controller Component but it now has another Component
[AddComponentMenu("CSharpBookCode/Base/Input Controller")]
2.6.4 RunMan Sounds
RunMan needs to make a little sound when he jumps, and we will also play a
sound when he falls to his doom. The audio system, Audio Mixers, and how we
deal with audio in the framework will be discussed in detail in Chapter 9.
Select RunMan in the Hierarchy, if it is not already highlighted. In the Inspector,
hit Add Component, and add the Component named BaseSoundController. This
Component holds a list of sounds, which may then be played by calling into the
script and passing an index number of the sound we want to play. The code to do
that is already contained in the scripts you have already added, so at this point it is
a matter of adding sounds to this Component.
2.6.5 Animating RunMan
As I mentioned at the start of this chapter, all the animations and the Animator
have been already set up ready to go but you will need to add an Animator
Component to the sprite itself (not the RunMan GameObject, the actual sprite).
In the Hierarchy, click on RunMan_Run-Sheet_0 under the RunMan
GameObject. Over in the Inspector, you should see that this sprite GameObject
has a Sprite Renderer attached to it. Our Animator Component will take care of
changing the sprite to whatever animation we need at the time, but it will need
access to the Sprite Renderer which is why we add the Animator to this
GameObject rather than its parent.
Click Add Component and add an Animator.
In the Project panel, find the folder Assets/Games/Sprites and click on it to
show its contents. Click and drag RunMan_Animator out of the Project panel
and drop it into the Controller field of the Animator Component you just added.
The final part of the puzzle is to tell the player script about this new Animator
Component. Click on RunMan in the Hierarchy, then find the Run Man
Animator field of the Run Man_Character Controller Component.
In the Hierarchy, click and drag RunMan_Run-Sheet_0 GameObject into
that Run Man Animator field. Unity will automatically figure out that there is an
Animator Component attached to the GameObject you just dropped into the field,
and in turn will use the reference to the Animator rather than the GameObject.
Now, if you press Play to try out the game the player will fall down onto the
platform and for the brief time that he is on a platform, you should be able to
move left, right, and even jump by pressing the Space key. Next, we need to give
RunMan some more platforms to run and jump on.
2.7.1 Game States
The BaseGameManager includes 17 different states for larger, more complicated
games, though for this project we only use 5 of them:
Game.State.loaded This state is called when the main game Scene first loads.
Game.State.gameStarting After initialization and set up of anything we need in the main game
Scene, gameStarting will be set and we use this to display a message
that will notify the player to ‘get ready’ to play.
Game.State.gamePlaying Once the game is running, the states stays in gamePlaying.
Game.State.gameEnding Ifthe player falls off a platform and hits a hidden trigger, the game
ends. The gameEnding state takes care of stopping movement and
preventing any more score being earned. We also show the final
‘game over’ user interface as this state begins.
Game.State.gameEnded When the gameEnded state occurs, we leave the main game Scene and
load the main menu.
{
Debug.Log("targetGameState=" + targetGameState);
if (targetGameState == currentGameState)
return;
switch (targetGameState)
{
case Game.State.loaded:
Loaded();
break;
case Game.State.gameStarting:
GameStarting();
StartGame();
break;
case Game.State.gameStarted:
// fire the game started event
GameStarted();
SetTargetState(Game.State.gamePlaying);
break;
case Game.State.gamePlaying:
break;
case Game.State.gameEnding:
GameEnding();
EndGame();
break;
case Game.State.gameEnded:
GameEnded();
break;
}
BaseGameManager already has the above function in it, but it was declared
as a virtual function. Having a virtual function like that means we can use the
override keyword to use a new version of the function – note that we override the
original function, not swap it out, or destroy the original. Any calls to
UpdateTargetState() will just be rerouted but, if we need to, we can always call the
original function too – it still exists – some times in this book, we will call the
original function as well as the overridden version, but not this time. Above, we
simply override the original function and use that.
Most of what we do in each state above is to call other functions to do the
actual work. For example, in the Game.State.loaded case statement, it just calls out
to Loaded(). I try as much as possible to keep code out of the state machine func-
tions to make debugging easier. Calling out to separate functions is a good way to
avoid your game state code from getting out of hand – having code in each state
can soon get unruly and harder to debug, so it really is best if you try to keep
blocks of code in their own functions, instead.
There are a couple of calls to functions that we do not have in this script, such
as GameStarting() and GameStarted(). These are inherited functions and their
code can be found in BaseGameManager.cs. The reason we call out to those is
// reset score
_RunManCharacter.SetScore(0);
SetTargetState(Game.State.gameStarting);
}
Above, Loaded() starts with a call to base.Loaded(), which tells Unity to run
the original Loaded() function from the base class (BaseGameManager). Although
here we are overriding that function, the original function still exists and we can
still call it via base. This is useful when the base class has things in it that we don’t
want to replicate in an overridden function, but we still want to run. In this case,
the base class only contains a call to invoke the UnityEvent OnLoaded. Again, we
will get to the UnityEvents later in the chapter when dealing with user interface.
The next line resets our players score to zero at the start of the game:
_ RunManCharacter.SetScore(0);
Finally, now that we have finished everything that needs doing when the
game first loads, there is a call to SetTargetState to move on to the new state
Game.State.gameStarting.
Next, we add the function that will be called when UpdateTargetState()
receives a call to change the state to gameStarting (from above, at the end of the
Loaded() function). Add this code below the Loaded() function:
void StartGame()
{
runSpeed = 0;
Above, StartGame() sets the runSpeed to 0, so that RunMan does not start
running right away. Remember when we added code to the platforms that used
runSpeed from this Game Manager script? When the game starts, we hold every-
thing in place – with runSpeed at 0, the platforms will not move.
Next, the StartGame() function schedules a call to a function named
StartRunning() in 2 seconds, using Unity’s built in scheduling function Invoke:
Invoke("StartRunning", 2f);
At this stage, after StartGame() has been called, the game sits in the Game.
State.gameStarting state as we wait for the call to StartRunning to be made (by
void StartRunning()
{
isRunning = true;
runSpeed = 1f;
distanceCounter = 1f;
// start animation
_RunManCharacter.StartRunAnimation();
StartRunning kicks the game to life! The first line sets the Boolean isRunning
to true so that movement and animations can get started.
Below that, runSpeed is set to 1. The platform code we added earlier will use
this variable to know how quickly to move the platforms across the screen, so
now it is set to 1 the platforms will move.
The distanceCounter variable is used to keep a track of how far platforms
move, which in turn is used to decide when to spawn new platforms for the player
to jump on. As it is the start of the game, it is reset above to 1.
A call to StartRunAnimation() on the RunManCharacter object (an instance
of the RunMan_CharacterController script Component attached to the player)
gets the running animation started.
As well as the Invoke function, Unity provides another useful method for
scheduling function calls. InvokeRepeating will call the specified function at
regular intervals until either the Scene is unloaded or you use CancelInvoke() to
stop it. In the code above, the AddScore() function is called every 0.5 seconds
where the player will be awarded with score. The scoring system for this game is
very simple, but it works well enough! Survive longer, get a higher score.
The last line of StartRunning() advances the game state:
SetTargetState(Game.State.gameStarted);
The UpdateTargetState() function you added earlier will deal with this
change of state to Game.State.gameStarted, invoke the OnGameStarted()
UnityEvent and change the state to Game.State.gamePlaying. From here on, the
game just sits in Game.State.gamePlaying until the game ends.
Next, we need to add a function to deal with the end of the game. Add this
code to your RunMan_GameManager class next:
void EndGame()
{
// stop running
isRunning = false;
runSpeed = 0;
In the script above, we deal with what happens when the game is over. This
will be called after the player falls off-screen. The first part of the script stops run-
ning and sets runSpeed to 0 so that the platforms stop moving.
The game is ending, so the next line schedules a call to ReturnToMenu in
4 seconds. We will get to this function further on in this section, but it may not
come as a big surprise that ReturnToMenu() loads the main menu.
After setting up the call to return to the menu, the script above uses
CancelInvoke to stop the AddScore() function from being called. Remember we set
up the InvokeRepeating call for this in the StartRunning() function? This is how we
stop it.
Finally, for the EndGame() function, we set the target state to gameEnded
with this line:
SetTargetState(Game.State.gameEnded);
UpdateTargetState() has nothing specific for Game.State.gameEnded other
than a call to Invoke the OnGameEnded UnityEvent, which will be used for user
interface later in this chapter.
Add the following code to your RunMan_GameManager script next:
void AddScore()
{
_RunManCharacter.AddScore(1);
}
void ReturnToMenu()
{
SceneManager.LoadScene("runMan_menu");
}
void SpawnPlatform()
{
runSpeed += 0.02f;
New platforms are generated by the SpawnPlatform() function. The first line
of SpawnPlatform() increases runSpeed by 0.02. This means that, over time, the
game will gradually get faster and faster. It is a very simple way of increasing the
difficulty over time so that the player should eventually concede.
When a new platform spawns, we reset the variable distanceCounter ready
to measure the distance to the next spawn. A quick look to the Update() function
you will tell you that we check distanceCounter against the variable distanceTo-
SpawnPlatform, to decide how far to allow between platforms – but that’s further
down. Let’s get back to SpawnPlatform().
The next line uses the built-in random number generator function Random.
Range to ask the engine to come up with a number between playAreaTopY and
playAreaBottomY:
float randomY = Random.Range(playAreaTopY, playAreaBottomY);
randomY can now be used as the height to spawn the next platform. We add
it into a Vector2 on the next line:
Vector2 startPos = new Vector2(platformStartX, randomY);
Now that we have a Vector2 containing a position to spawn the new platform
out (consisting of a nice random Y position and a start position held in platform-
StartX that will be off-screen to the right of the play area), we can go ahead and
spawn the platform on the next line:
Instantiate( _ platformPrefab, startPos, Quaternion.identity);
The effect of this is that the next platform will have a random amount of space
between it because we are changing where zero is in the distance counting system.
We finish this programming section with an Update() function. Update() is
called every frame. In RunMan_GameManager, it is where we keep an eye on the
distanceCounter to decide whether it is time to spawn a new platform.
Add this to your script above the final curly bracket:
void Update()
{
if (isRunning)
{
distanceCounter += (runSpeed * Time.deltaTime);
Above, Update() begins by checking that isRunning is true first. If the player
is not running, we will never need to spawn any new platforms, so it makes sense
not to run this code unless we are on the move.
distanceCounter is increased next by runSpeed multiplied by Time.deltaTime.
We talked about Time.deltaTime in the platform movement script back in Section
2.5. By multiplying the runSpeed by deltaTime, we make movement time-based
rather than frame based. This variable distanceCounter is essentially keeping
stock of how far the player has run so that we know when to spawn the next plat-
form. The check for that comes next in that Update() function:
Figure 2.14 The RunMan_GameManager fields reference other GameObjects in the main
Scene that the Game Manager needs to create instances of or communicate with.
There is a little more cheating going on here with the user interface – remember
at the start of the chapter when we hid it all? Yeah, the user interface is already
there but it needs some additional code to be shown and hidden as needed.
In the Hierarchy, click the GameManager GameObject to highlight it. In the
Inspector, scroll down to the Add Component button and click it. Choose New
Script and type RunMan_UIManager in as the name of the script. Click Create
and Add to make the new template Component. Once the Component appears,
double click on the script name to open the script up in your script editor.
Replace the code with this:
using UnityEngine.UI;
using GPC;
}
public void SetHighScore(int scoreAmount)
{
The first thing to notice about the code above is that it derives from a class
named CanvasManager. CanvasManager is part of the framework for this book,
and it provides methods for showing and hiding Canvases. To use CanvasManager,
each Canvas GameObject has a Component on it named Canvas Group. Canvas
Group is a Component provided by Unity that allows you to control the alpha
level of an entire UI Canvas and to enable or disable interactions with it. What
Add the following variable declaration just after the opening curly bracket:
public RunMan _ UIManager _ uiManager;
_uiManager.SetHighScore(GetComponent<BaseProfileManager>().
GetHighScore());
Now find the EndGame() function and add this before the closing curly
bracket at the end of the function:
if (GetComponent<BaseProfileManager>().
SetHighScore(_RunManCharacter.GetScore())== true)
{
_uiManager.ShowGotHighScore();
}
The last piece of the puzzle adds score during the Update() loop. Find the
Update() function. Find this code:
if ((distanceCounter >=
distanceToSpawnPlatform))
{
SpawnPlatform();
}
And add the following right after that closing bracket for the If statement
(so that it has three curly brackets after it, at the end of the function):
_uiManager.SetScore(_RunManCharacter.GetScore());
using UnityEngine;
using UnityEngine.SceneManagement;
using GPC;
[RequireComponent(typeof(BaseProfileManager))]
case Game.State.gameStarting:
StartGame();
break;
case Game.State.gameStarted:
// fire the game started event
OnGameStarted.Invoke();
SetTargetState(Game.State.
gamePlaying);
break;
case Game.State.gamePlaying:
break;
case Game.State.gameEnding:
EndGame();
break;
case Game.State.gameEnded:
OnGameEnded.Invoke();
break;
}
}
_uiManager.SetHighScore(GetComponent<BaseProfileMana
ger>().GetHighScore());
// reset score
_RunManCharacter.SetScore(0);
SetTargetState(Game.State.gameStarting);
}
void StartGame()
{
runSpeed = 0;
SetTargetState(Game.State.gameStarted);
// start animation
_RunManCharacter.StartRunAnimation();
void EndGame()
{
// stop running
isRunning = false;
runSpeed = 0;
void AddScore()
{
_RunManCharacter.AddScore(1);
}
void ReturnToMenu()
{
SceneManager.LoadScene("runMan_menu");
}
void SpawnPlatform()
{
runSpeed += 0.02f;
distanceCounter = 0;
float randomY = Random.Range(playAreaTopY,
playAreaBottomY);
Vector2 startPos = new Vector2(platformStartX,
randomY);
Instantiate(_platformPrefab, startPos, Quaternion.
identity);
{
if (isRunning)
{
distanceCounter += (runSpeed * Time.
deltaTime);
_uiManager.SetScore(_RunManCharacter.
GetScore());
}
}
}
Play the game now. At the start, there should be a message to say get ready
and once the game is in play the score and high score will be shown. When
RunMan falls off the screen, you should also get a nice game over message with a
final score!
The only problem is that ending the game makes an error like “Scene
‘runMan_menu’ couldn’t be loaded because it has not been added to the build
This chapter will give context to the design decisions made in the core framework
as well as providing a good base for understanding how the individual code reci-
pes work together and why they may sometimes have dependencies on other
Components.
The framework for this book (as seen in Figure 3.1) features six main areas:
1. Game Manager
The Game Manager acts like a central communication script for all parts
of the game and oversees game state.
2. Players
Players may already be present in the game scene or instantiated by the
Game Manager. The players structure will be explored later in Chapter 4
of this book.
3. AI and enemies
As with players, AI controlled characters, or enemies may already be
present in the scene at load time or created at runtime. We look at AI in
full in Chapter 10.
4. UI Manager
The UI Manager draws all the main in game user interface elements,
such as score display, and lives. We look at UI in Chapter 11.
5. Sound Manager
The Sound Manager handles sound playback. Sound is in Chapter 9.
49
BASE USER
GAME MANAGER USER DATA
MANAGER
3.1.1 Managers
Managers are used in this book as a term to represent a script that specifically
manages objects or data (dealing with players, audio or other game elements in a
similar way to how a manager might manage staff in a company). For example, it
could be a GlobalRaceManager class used to track and get information from a set
of RaceController script Components attached to racing vehicles. The manager
script uses information from the RaceController Components to calculate race
positions of players as well as informing players as to the game’s current running
state, managing overall race logic.
3.1.2 Controllers
Controllers specialize in specific tasks such as helping scripts communicate with
each other, holding game-specific player information or dealing with session
data. For example, the players in this book have a player controller script. Enemies
have an enemy controller. Controllers can be independent of other scripts or
managed by manager scripts, depending on what it is they need to do.
▪ 3.2.1. BaseGameManager
▪ 3.2.2. ExtendedCustomMonoBehaviour
▪ 3.2.3. BaseCameraController
Other base scripts will be covered in specific parts of this book. For example,
the scripts found in GPC_Framework/BASE/PLAYER are covered in Chapter 4
when we look at the player structure.
3.2.1 BaseGameManager
The Game Manager is the core of gameplay, where overall game functions are
described and dealt with and where current game state is held.
Core functions of the Game Manager are:
2. Creating player(s)
3. Camera set up and specifics such as telling the camera what to follow
Keeping track of game state is important, as some elements will need to change
their behavior. If the game is paused, for example, we may want to suspend all input,
freeze game time, and show a pop up. When the game ends, we will need to disable
player input and so on. Tracking game states is the job of the Game Manager script.
A typical game may have several different core states, such as:
1. Game running
2. Game paused
3. Loading
4. Game ended
There may also be several custom states, such as a state for showing a cutscene
or perhaps a state for playing a special audio clip. I recommend creating a new
using UnityEngine;
using UnityEngine.Events;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/GameManager")]
Using namespaces provides scope to the code inside them. It is used to orga-
nize code and it is helpful to prevent your code from conflicting with others (such
as duplicate class names and so forth). As we tend to use a lot of library C# code
with Unity, I recommend using namespaces for all your classes.
Unity's AddComponentMenu function is used to make a menu item in the
Unity editor. This is completely optional, but I like to have those menu items
available.
We now move into the BaseGameManager class itself:
The advantage of having UnityEvents is that I can set up events in the editor,
via the Inspector, to tell other scripts when state changes happen. As these are
UnityEvents, I can add as many references to other Components via the Inspector
The above functions are here as placeholders. The intention is that Game
Manager scripts for use in real game projects will derive from this class and over-
ride these virtual functions with real functionality. If you do override them,
remember that you will be overriding the calls to corresponding UnityEvents.
Just remember to either run Invoke() on those GameEvents or call the base class
with base.(functionname).
Next, the UpdateTargetState function, which is called whenever
SetTargetState() is called upon to change the game state:
public virtual void UpdateTargetState()
{
// we will never need to run target state
functions if we're already in this state, so we check for that and
drop out if needed
if (targetGameState == currentGameState)
return;
switch (targetGameState)
{
case Game.State.idle:
break;
case Game.State.loading:
break;
case Game.State.loaded:
Loaded();
break;
case Game.State.gameStarting:
GameStarting();
break;
Remember that big list of virtual functions a couple of pages back? Inside
these case statements are where they are called. Above, Loaded() and
GameStarting() are called respectively.
Below, I skip ahead past the rest of the case statements to reach the end of the
UpdateTargetState() function:
The last line of UpdateTargetState() sets the current game state to the target
state, as the target state has now been processed by the case statements above it.
UpdateCurrentState() follows on next, which follows a similar pattern of
case statements:
public virtual void UpdateCurrentState()
{
switch (currentGameState)
{
case Game.State.idle:
break;
case Game.State.loading:
break;
case Game.State.loaded:
break;
case Game.State.gameStarting:
break;
if (paused)
{
// pause time
Time.timeScale = 0f;
}
else
{
// unpause
Time.timeScale = 1f;
}
}
}
The Get part simply returns the value of the Boolean variable paused.
The Set part sets the Boolean variable paused and goes on to set Time.
timeScale to either 0 (paused) or 1 (unpaused). Setting Time.timeScale affects
how time passes. For example, setting timescale to 0.5 would mean that time
passes 2× slower than real time. The Unity documentation states:
Except for realtimeSinceStartup, timeScale affects all the time and delta time mea-
suring variables of the Time class. If you lower timeScale it is recommended to also
lower Time.fixedDeltaTime by the same amount.
FixedUpdate functions will not be called when timeScale is set to zero.
3.2.2 ExtendedCustomMonoBehaviour
Extending MonoBehaviour is a useful way to avoid repeating common functions
or variable declarations. For example, most of the scripts in this book use a
variable named _TR to refer to a cached version of a Transform.
ExtendedCustomMonoBehaviour contains the declaration for it. Wherever a new
class would use MonoBehaviour, we just derive from ExtendedCustom
MonoBehaviour and, in the new class, we do not have to declare _TR or any of the
other commonly used variables or functions we choose to put in
ExtendedCustomMonoBehaviour.
9. A function called SetID(integer) so that other classes can set the variable id
using UnityEngine;
public class ExtendedCustomMonoBehaviour : MonoBehaviour
{
[Header("Transform and RB references")]
public Transform _TR;
public GameObject _GO;
[System.NonSerialized]
public Vector3 tempVEC;
[System.NonSerialized]
public Transform _tempTR;
if (_GO == null)
_GO = gameObject;
}
3.2.3 BaseCameraController
Each camera script in the example games for this book derive from a class named
BaseCameraController. BaseCameraController.cs can be found in the folder
Assets/GPC_Framework/Scripts/BASE/CAMERA
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Camera Controller")]
A player can take on any form – humans, aliens, vehicles, and so forth.
Designing a structure that can deal with all of them is a challenge. The way
we need to store data, the types of data we need to store, the various types
of movement code and weapons and so on, can soon become complicated.
A basic player using the framework in this book would be made up of a
GameObject containing Unity-specific physics or collision Components, then
four additional script Components (as seen in Figure 4.1):
2. A movement controller
To move the player around (depending on the type of movement
required this could be anything, such as a vehicle, or a humanoid)
3. An input controller
To provide input to the player controller from the user
61
PLAYER
CONTROLLER
BASE USER
AI CONTROLLER
MANAGER
MOVEMENT
CONTROLLER
INPUT
(VEHICLE, USERDATA
CONTROLLER
HUMANOID, SHIP
ETC.)
PHYSICS
(TRANSFORM /
RIGIDBODY/
COLLIDER ETC.)
Figure 4.1 The Player GameObject has physics and collision Components attached to it, as
well as Components to deal with data, sound, movement, and input.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Input Controller")]
public class BaseInputController : MonoBehaviour
{
public enum InputTypes { player1, noInput };
public InputTypes inputType;
// directional buttons
public bool Up;
public bool Down;
public bool Left;
public bool Right;
// weapon slots
public bool Slot1;
public bool Slot2;
public bool Slot3;
public bool Slot4;
public bool Slot5;
public bool Slot6;
Above, the slot-based weapon system uses the keyboard buttons from 1 to 9
to directly select which weapon slot to use. For that reason, the input system has
nine Boolean variables to take input from the number keys – though not imple-
mented here, you can see how they are used in Chapter 7 when we look at a
weapon system.
Although the script includes a function called GetFire(), returning the value
of the Boolean variable Fire1, the script does not include any code to do anything
with Fire1. It is just there for consistency and it is expected that the player con-
troller script handles firing as needed – for an example of this, please see Chapter
7 – Weapon Systems.
TEMPVec3.x = horz;
TEMPVec3.y = vert;
1. Score
2. High score
3. Level
4. Health
You can easily add (or remove) variables to the UserData class, if you need to
keep a track of other data. For ease of access, I use a class named Base
PlayerStatsController that contains all the functions needed to ‘talk’ to the
BaseUserManager. If you add new variables to the UserData class, you may also
want to add methods to access them via BasePlayerStatsController. See Section
4.5. of this chapter for a full breakdown of BasePlayerStatsController.
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Base User Manager")]
didInit = true;
}
The Init() function checks to see if the global_userDatas List has already
been initialized, if not we create a new List of type UserData.
The ResetUsers() function is provided to wipe out all user information. This
may be called at the start of the game, just before new players are added before the
main game loads or perhaps at the end of the game to tidy everything up.
return global_userDatas;
}
To process the List of players at the start of the game, you can grab it by using
the above function, GetPlayerList() which will return a List containing every-
thing in global_userDatas. In the games examples for this book, the Game
Manager gets this List when the core Scene is loaded, and we iterate through the
List to spawn one player into the Scene per entry in global_userDatas.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace GPC
{
[RequireComponent(typeof(BaseUserManager))]
_inputController =
GetComponent<BaseInputController>();
SetupDataManager();
}
if (_myDataManager == null)
_myDataManager =
GetComponent<BaseUserManager>();
if (_myDataManager == null)
_myDataManager =
gameObject.AddComponent<BaseUserManager>();
if (_myDataManager == null)
_myDataManager =
GetComponent<BaseUserManager>();
if (!disableAutoPlayerListAdd)
{
// add the player (set up data) to
the ScriptableObject we use to store player info
myID = _myDataManager.AddNewPlayer();
_myDataManager.SetName(myID, "Player");
_myDataManager.SetHealth(myID, 3);
}
}
Right away, in the code above you may notice the similarities to the
BaseUserManager class. A large portion of the script above is taken up with calls
to the corresponding functions in BaseUserManager – by duplicating those func-
tions here to call out to them, we are adding all of the functionality of
BaseUserManager to the player script and simplifying how we need to call it. For
5.1 Introduction
The utility scripts and commonly used systems in this chapter may come in
handy for a range of different types of game projects. In the Assets/GPC_
Framework/Scripts/COMMON folder you will find an eclectic bunch of reusable
scripts like timers and camera controllers. In this chapter, we focus on those
standalone scripts. You can easily take these and use them in any project, with or
without using the GPC framework from this book.
Within the COMMON folder, there are several sub-folders. Those are;
AI
CAMERA
GAME CONTROL
The scripts in this folder are about overall game state control. In the example
files, you will find:
75
RaceController and GlobalRaceManager – containing the base logic for rac-
ing games, such as lap counting and position tracking
INPUT
Different player and game types require different control systems or input
methods. Any common scripts dealing with input go in this folder.
LEVEL LOADING
SCRIPTABLE OBJECTS
All ScriptableObjects require files to define them and this is where you can
find those files.
SPAWNING
USER INTERFACE
UTILITY
The Utility folder is home to small helper scripts, such as the timer class or a
script to automatically spin a GameObject.
WEAPONS
Any weapons-related scripts outside of the base framework may be found here.
The scripts in this chapter are organized in order of how they appear in the
respective folders, for ease of reference.
5.2 AI
You can also find more information about these scripts in Chapter 10, dedicated
to AI.
5.3 Camera
The camera is the window into the game world, and it makes a huge difference to
how the gameplay feels. Be sure to prototype, iterate, and put some time into
finding the perfect camera set up! There are just two camera scripts for the exam-
ple games in this book. Both cameras should provide a good starting point for
camera systems in your own games.
The following scripts are found in example project, in the folder Assets/
GPC_Framework/Scripts/COMMON/CAMERA.
76 5. Recipes
5.3.1 Third-Person Camera
A third-person camera sits behind the player. It is not quite the same as a follow
camera; a third-person camera usually orbits around the player rather than sim-
ply following it around. To visualize this, imagine that the camera is tied to a pole
attached to the target player. As the player moves around, the camera remains to
be the length of the pole away from it, but the camera is free to move around the
player in a circle. The position of the camera is usually somewhere behind the
player, with a little damping on its horizontal movement applied to it, to allow for
a good sense of movement when the player turns. In some third-person camera
setups, the camera may be allowed to move in or out from the target player.
using UnityEngine;
using System.Collections;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Common/Cameras/Third
Person Cam Controller")]
float wantedRotationAngle;
float wantedHeight;
float currentRotationAngle;
float currentHeight;
Quaternion currentRotation;
Vector3 wantedPosition;
void Update()
{
5.3 Camera 77
Code from this script will eventually interpolate heights and rotation values,
but it requires a little extra before we can do that:
wantedRotationAngle =
_cameraTarget.eulerAngles.y;
currentRotationAngle = _TR.eulerAngles.y;
In this part of the code, the yVelocity variable holds the current velocity of the
interpolated move, but it is not actually used anywhere else in the script. It’s a
required field for SmoothDampAngle, so we just provide it for the function to
work right and forget about it. Note that one possible use for getting the velocity
may be speed limiting the move, but SmoothDampAngle can take care of this with
no extra code by providing a maxSpeed parameter. This is useful for limiting inter-
polation speeds without having to do it yourself with velocity.
The currentRotationAngle is, in effect, a portion of the rotation we need to
make around the y-axis (hence using the name yVelocity to describe its speed).
The variable rotationSnapTime decides how much time it should take for the
rotation to happen, which is public and designed to be set in the Unity editor
Inspector window on whichever GameObject the script is attached to, so that the
rotation speed can be easily set in the editor.
Height is interpolated using Mathf.Lerp, which takes three parameters: the
start amount, the target amount, and a time value clamped between 0 and 1.
78 5. Recipes
We use the float heightDamping, multiplied by Time.deltaTime, to make the
transition time based. Wondering why do we need to make the time parameter time
based? Good question! How the time parameter of Mathf.Lerp works has been a
very popular question on various forums and help pages. The time value represents
how much of a portion, of the difference between the two values, that should be
applied each time this line of code is executed. If the time value is zero, the return
value will be the start amount. If time is 1, the return value will be the target amount.
By calculating the portion with Time.deltaTime, the transition from start
value to target value is smoothed out over a repeatable, fixed amount of time
rather than it being frame-rate dependent and based on how many times the
Mathf.Lerp calculation line is called:
currentHeight = Mathf.Lerp(currentHeight,
wantedHeight, heightDamping * Time.deltaTime);
Now that the script has calculated how much to rotate the camera and how
high it should be on the y-axis, the script goes on to deal with positioning:
wantedPosition is a Vector3 that will hold the target position. The target posi-
tion is made from the target Transform.position followed by setting its y position
to the height calculated earlier in this section in currentHeight variable:
wantedPosition = target.position;
wantedPosition.y = currentHeight;
The next part of the code calculates how far behind the player we need to
move the camera. For this, the script uses Mathf.SmoothDampAngle. zVelocity
represents the speed of movement of the camera along its z-axis:
usedDistance = Mathf.SmoothDampAngle(usedDistance,
distance, ref zVelocity, distanceSnapTime);
To place the camera at the correct position around the target using only a rota-
tion angle, the solution lies within Quaternion.Euler. This rather frightening sound-
ing function converts Euler angle vector3-based rotations into a vector. That is; you
pass in Euler rotation values and you get back a direction vector instead. Multiply
this by how far back we want the camera from the player; add that to the player’s cur-
rent position and we have the correct rotation, position and offset for the camera:
wantedPosition += Quaternion.Euler(0,
currentRotationAngle, 0) * new Vector3(0, 0, -usedDistance);
_TR.position = wantedPosition;
The final part of the function causes the camera to look at the target (player).
It does this by using Unity’s built-in Transform.LookAt() function. We pass in
the position of the target object (as a Vector3) and add an extra offset to where the
camera is going to look, via the lookAtAdjustVector variable. Note that you can
also add an up vector to the LookAt() function, but in this case, we don’t need to:
_ TR.LookAt( _ cameraTarget.position);
_TR.Rotate(lookAtAdjustVector);
Above, in the last line we also add a small rotation adjustment. I like to use
this so that the camera aims above the player as if it were looking forward, rather
than looking right at the players vehicle or body. This can make the camera view
5.3 Camera 79
more parallel to the ground level and keep good visibility, making the game eas-
ier to play in some situations. Of course, as with anything in this book, it is up to
you to decide where the camera goes and how it moves to make your personal
vision of your game. Try adjusting parameters and testing out the feel of different
rotations or movement speeds.
5.3.2 Top-Down Camera
The TopDownCamera class is a very basic target following system from a top-
down perspective:
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Common/Cameras/Top Down
Cam Controller")]
public class TopDownCamera : BaseCameraController
{
public Vector3 targetOffset;
public float moveSpeed = 2f;
public float maxHeight;
public float minHeight;
void LateUpdate()
{
if (cameraTarget != null)
_TR.position = Vector3.Lerp(_TR.position,
cameraTarget.position + targetOffset, moveSpeed *
Time.deltaTime);
}
}
}
The single line controlling this camera uses the Vector3.Lerp function to
interpolate the camera’s Transform’s position to that of the target stored in fol-
lowTarget. We add an offset to the position (so that we end up above the target
rather than inside it) and use the moveSpeed multiplied by Time.deltaTime to
Lerp based on time.
5.4.1 GlobalRaceManager
GlobalRaceManager.cs will be explained in Chapter 14, Section 14.4.3.
5.4.2 RaceController
RaceController.cs will be explained in Chapter 14, Section 14.4.4.
80 5. Recipes
5.5 Input
Your games could take a variety of different inputs, or in some cases you may
want to implement a third-party input solution like Rewired (made by Guaveman
Enterprises) to provide extended detection of input hardware or force feedback
effects.
In Chapter 4, we saw that the main player structure contained a single input
script designed to take keyboard input. Adding input systems is case of building
custom input scripts that follow a similar format, so that the existing player code
need not be changed to accommodate different controls.
It may be required to take input from other methods than the keyboard, such
as mouse, or joystick. You can easily set up alternate controls in the Unity editor
via the Edit > Project Settings > Input menu.
Input scripts can be found in Assets/GPC_Framework/Scripts/COMMON/
INPUT.
5.5.1 Mouse Input
Mouse_Input.cs calculates movement of the mouse between each frame and uses
it for input. The script:
public class MouseInput : BaseInputController
{
private Vector2 prevMousePos;
private Vector2 mouseDelta;
5.5 Input 81
CheckInput() starts by calculating a scale to work at. The floats scalerX and
scalerY hold the equivalent of one percent of the screen’s width and height
respectively.
// calculate and use deltas
float mouseDeltaY = Input.mousePosition.y -
prevMousePos.y;
float mouseDeltaX = Input.mousePosition.x -
prevMousePos.x;
The delta amount of mouse movement is calculated in the code by taking the
current mouse position and subtracting it from the position that the mouse was
at the end of the last update.
// scale based on screen size
vert += ( mouseDeltaY * speedY ) * scalerY;
horz += ( mouseDeltaX * speedX ) * scalerX;
// store this mouse position for the next time we're
here
prevMousePos= Input.mousePosition;
When the mouse position is multiplied by the scale variables (scalerX and
scalerY) we end up with a percentage amount. We say that the mouse is at a per-
centage amount across the screen and use that percentage amount as input.
// set up some Boolean values for up, down, left and
right
Up = ( vert>0 );
Down = ( vert<0 );
Left = ( horz<0 );
Right = ( horz>0 );
The mouse-based input script also populates the Boolean variables above, for
directional movement, if required.
// get fire / action buttons
Fire1= Input.GetButton( "Fire1" );
}
To make the fire button work properly, mouse button input must be set up
correctly in the input settings of the Unity editor, under the Fire1 entry.
public void LateUpdate()
{
// check inputs each LateUpdate() ready for the next
tick
CheckInput();
}
}
The script calls its own update function from LateUpdate(), which is where
Unity recommends all input checking happen.
Note: To set up inputs correctly, open Unity’s Input menu Edit > Project
Settings > Input
82 5. Recipes
5.7 ScriptableObjects
ScriptableObjects are a great way to manage data that needs to persist across the
entire game. There is just one script in the framework, ProfileScriptableObject.
5.7.1 ProfileScriptableObject
Saving and loading is covered, in full, in Chapter 12. For save profiles, this frame-
work uses a ScriptableObject to store everything we want in the profile. This
script needs to be serialized, so be aware of using any variable types that may not
be easily serializable by the engine. It forms the basis of a save profile. You can
find this script in the folder Assets/GPC_Framework/Scripts/COMMON.
The ProfileScripableObject.cs script looks like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
namespace GPC
{
[CreateAssetMenu(fileName = "ProfileData", menuName =
"CSharpFramework/ProfileScriptableObject")]
[Serializable]
public class ProfileScriptableObject : ScriptableObject
{
public Profiles theProfileData;
}
[Serializable]
public class Profiles
{
public ProfileData[] profiles;
}
[Serializable]
public class ProfileData
{
[SerializeField]
public int myID;
[SerializeField]
public bool inUse;
[SerializeField]
public string profileName = "EMPTY";
[SerializeField]
public string playerName = "Anonymous";
[SerializeField]
public int highScore;
[SerializeField]
public float sfxVolume;
[SerializeField]
public float musicVolume;
}
}
5.8 Spawning
In many cases using Unity’s built-in Instantiate function would be enough for
creating objects for a desktop computer-based game, but instancing GameObjects
in Unity can be an expensive process, particularly noticeable on mobile devices.
5.8 Spawning 83
One common method of getting around the processor hit is to use pooling.
Pooling is where you have a group of objects that you use and reuse without
destroying them. Building a pool management system is beyond the scope of this
book, although centralizing the spawning system will make it easy to switch out
spawning for a better solution in the future.
Find these scripts in Assets/GPC_Framework/Scripts/COMMON/SPAWNING.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Common/Spawn Prefabs (no
path following)")]
The class is added to the GPC namespace, then a menu item is added to the
Unity editor via the AddComponentMenu tag. The class derives from
ExtendedCustomMonoBehaviour so that we get access to its IsInLayerMask()
function, if needed in future. For example, the TriggerSpawner class in Section
5.8.2 of this chapter derives from Spawner and makes use of IsInLayerMask().
84 5. Recipes
The only variable declaration above of note is that the spawn objects are in an
array. This means you can add more than one type of prefab to spawn.
void Start()
{
// cache ref to our Transform
_TR = transform;
if(_cameraTransform==null)
_cameraTransform = Camera.main.
transform;
Above, we cache the Transform and, if one has not been set in the Inspector,
we try to find the Transform of the main Camera in the Scene with Unity’s built-
in Camera.main.
if (shouldAutoStartSpawningOnLoad)
StartSpawn();
}
We find out how many objects are in the spawn list array and store the result
in a variable named totalSpawnObjects. This just makes it easier:
A Gizmo is a shape that can be used to provide a visual aid to your Component
or GameObject – a cube or a sphere – which gets displayed in the Scene view of
the editor. Thanks to the code, whenever this Component is attached to a
GameObject, Unity will draw a Gizmo around it to show you the distance at
which our Spawner will activate. Drawing takes place in the function above,
OnDrawGizmos(), which is called automatically by the game engine both when
the game is playing and when it is idle, too.
Above, we check the Boolean variable named distanceBasedSpawnStart to
see if we are using distance-based spawning. If it is true, we draw the sphere
based on the spawn distance. If distanceBasedSpawnStart is false, we just draw
the sphere with a fixed radius. This way, we always have a Gizmo to make it easier
to see where spawning will occur.
To set the color of the Gizmo about to be drawn, you call Gizmos.color()
with the parameters of red, green, and blue followed by an alpha value. All should
be between 0 and 1. Once you have made a call to Gizmos.color() in your func-
tion, anything you draw after that point will be drawn with that color until you
add another Gizmos.Color() call.
Drawing a sphere Gizmo is as simple as calling Gizmos.DrawSphere() and
you pass in the position to draw it followed by the radius of the sphere. Above,
we take our Transform’s position, and in the first instance, we pass
5.8 Spawning 85
distanceFromCameraToSpawnAt as the radius so that the Gizmo exactly repre-
sents our spawn distance. In the second instance, when distanceBasedSpawn-
Start is false, we pass an arbitrary value as the radius instead.
public void Update()
{
float aDist = Vector3.Distance(_TR.position,
_cameraTransform.position);
if (distanceBasedSpawnStart && !spawning &&
aDist < distanceFromCameraToSpawnAt)
{
StartSpawn();
spawning = true;
}
}
The Update() function checks the distance between our Transform and the
camera, by using Vector3.Distance(). This is not the most optimized thing to call
every single frame, so be aware of this if you use it in a complicated project that
may be impacted by this. An alternative might be to only check distance every
few frames, instead, or perhaps on a timer.
Once we have the distance value in aDist we check that distanceBaseSpawn-
Start is true (indicating that we want to do distance-based spawning) and that we
are not already spawning – spawning is false – then compare aDist to the dis-
tanceFromCameraToSpawnAt value. If these conditions are met, we can call
StartSpawn() to spawn the prefab(s) and we set spawning to true to avoid any
duplicate calls to spawn now that the process is in motion.
void StartSpawn()
{
StartWave(totalAmountToSpawn,
timeBetweenSpawns);
}
To get the spawning process in motion, we call StartSpawn() above that will
then call another function StartWave(). You will see below what StartWave() does,
but note for now just that you pass in how many prefabs you want to spawn fol-
lowed by the amount of time the system should wait before spawning another one.
Spawning is a process in this function, rather than a single action. StartWave()
will continue to spawn as many objects as you tell it to, ultimately disabling its
own GameObject once all prefabs have been added to the Scene.
public void StartWave(int HowMany, float
timeBetweenSpawns)
{
spawnCounter = 0;
totalAmountToSpawn = HowMany;
// reset
currentObjectNum = 0;
CancelInvoke("doSpawn");
86 5. Recipes
Inspector) then currentObjectNum will be increased every time a new prefab is
spawned so that we get the next item from the array next time.
If there are more prefabs to spawn, a function named doSpawn() will be called
repeatedly – until all prefabs are made. We use Unity’s Invoke() function to sched-
ule a call to doSpawn(), so whenever we run StartWave we make sure there are no
outstanding Invoke calls to doSpawn() by using the CancelInvoke() function. If
there any outstanding, CancelInvoke() will flush all of them out at once.
// the option is there to spawn at random
times, or at fixed intervals...
if (shouldRandomizeSpawnTime)
{
// do a randomly timed invoke call,
based on the times set up in the inspector
Invoke("doSpawn", Random.Range
(minimumSpawnTimeGap, timeBetweenSpawns));
}
else
{
// do a regularly scheduled invoke
call based on times set in the inspector
InvokeRepeating("doSpawn",
timeBetweenSpawns, timeBetweenSpawns);
}
}
void doSpawn()
{
if (spawnCounter >= totalAmountToSpawn)
{
if (shouldRepeatWaves)
{
spawnCounter = 0;
}
else
{
CancelInvoke("doSpawn");
this.enabled = false;
return;
}
}
5.8 Spawning 87
Above, if spawnCounter is greater or equal to totalAmountToSpawn then we
need to end this wave of spawning and either start another wave by resetting
spawnCounter to 0, or to cancel further calls to doSpawn() entirely with
CancelInvoke(). If we are dropping out entirely and ending all spawning, the
this.enabled = false line will disable the GameObject so that it no longer receives
any calls to its functions from the engine (no more FixedUpdate(), Update(), or
LateUpdate() calls and so on). We also know that enough spawns have been
made, so we drop out of the function.
// create an object
Spawn( _spawnObjectPrefabs[ currentObjectNum ].transform,
_TR.position, Quaternion.identity);
if (shouldRandomizeSpawnTime)
{
// cancel invoke for safety
CancelInvoke("doSpawn");
With the new prefab added to the Scene, our counters spawnCounter, and
currentObjectNum are incremented. After that, we check currentObjectNum to
see if it has been incremented too far – past the end of the _spawnObjectPrefabs
array. If we tried to use this as an index number to access the prefabs array with,
Unity would throw an error, so at this point if we find that currentObjectNum is
too high it gets reset to 0.
Earlier in the class, we used the shouldRandomizeSpawnTime flag to decide
whether to call this function with Invoke() or call it with InvokeRepeating().
Above, if we are using a randomize spawn time, we need to schedule the next call
to doSpawn at a different, randomized period of time. For this, first any out-
standing calls to doSpawn() are cancelled with CancelInvoke() – this is just a
safety measure, as in the current state there should never be any duplicate calls.
Again, we use Invoke() and Random.Range() to choose a random period of time.
Random.Range() takes the minimum value of minimumSpawnTimeGap and the
maximum value of timeBetweenSpawns.
5.8.2 Trigger Spawning
The TriggerSpawner class instantiates an object when another object enters the
trigger that this script Component is attached to.
The easiest way to use this Component is to add a cube primitive to a Scene and
set its Box Collider’s IsTrigger property to true. Add this TriggerSpawner Component
88 5. Recipes
to your cube and then whenever your player enters the area outlined by the cube, it
will trigger this script to spawn a prefab. Once you have the area worked out correctly,
disable the cube’s Mesh Renderer Component and you have a trigger spawn area.
One such use for this script would be to spawn an enemy when the player
reaches an area in a level.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Utility/Trigger Spawner")]
Spawn(ObjectToSpawnOnTrigger.transform,
_TR.position + offsetPosition, Quaternion.identity);
if (onlySpawnOnce)
Destroy(gameObject);
}
}
}
5.8.3 Timed Spawning
This TimedSpawner Component will spawn a prefab in a set amount of time. In
an arena blaster game, this might be used to delay the spawning of an enemy
from the start of a level, using a random delay to make the start of the level more
dynamic.
using UnityEngine;
namespace GPC
{
public class TimedSpawner : Spawner
{
public float initialSpawnDelay;
public bool shouldRandomizeSpawnTime = true;
public float randomDelayMax = 0.5f;
5.8 Spawning 89
[Space]
public bool waitForCall;
private Vector3 myPos;
private Quaternion myRot;
[Space]
public Transform _prefabToSpawn;
The class derives from Spawner, which provides a path to the Spawn() func-
tion in ExtendedCustomMonoBehaviour and the shouldRandomizeSpawnTime
variable. Unlike the Spawner class, this script uses a single prefab rather than an
array – the prefab reference should be placed into the 1_prefabToSpawn field via
the Inspector in Unity.
public override void Start()
{
base.Start();
if (waitForCall)
return;
StartSpawnTimer();
}
The Start() function overrides the one found in our base class, Spawner. We
still want to do everything that the base class does, which is remedied with a call
to base.Start(). Above, Start() looks at waitForCall to see whether or not it should
just get stuck in and start spawning. Whenever waitForCall is set to true, it is
assumed that you will call StartSpawnTimer() from another script rather than
have it called automatically in the Start() function above.
public void StartSpawnTimer()
{
float spawnTime = initialSpawnDelay;
if (shouldRandomizeSpawnTime)
spawnTime += Random.Range(0,
randomDelayMax);
Invoke("SpawnAndDestroy", spawnTime);
}
90 5. Recipes
Above, SpawnAndDestroy() wraps up the class. The Spawn() function from
ExtendedMonoBehaviour is used here to spawn a prefab from _prefabToSpawn
at the same position and rotation as the Transform of the GameObject this script
is attached to.
With the prefab spawned, this script (and this GameObject) has served their
purpose so we remove them from the Scene with a call to Unity’s Destroy() func-
tion. Destroy() takes a reference to the GameObject you want to destroy as a
parameter.
5.9.2 ScreenAndAudioFader
The ScreenAndAudioFader class mixes the functionality from two other classes –
BaseSoundManager and CanvasManager – to fade in or out both a Canvas and an
audio mixer at the same time. In the Blaster example game in this book, I used this
class to fade out the Scene at the end of each level. The audio and visual will fade
out, the next level Scene is loaded, then the audio and visuals fade back in again.
We will be looking at Audio Mixers in detail in Chapter 9, when we look at
the BaseSoundManager and how to deal with audio in your games. For that rea-
son, we will avoid listing out the code for ScreenAndAudioFader in this section
because all it does it call out to those other two classes to do all the work.
5.9.3 MenuWithProfiles
This script will be covered in detail in Chapter 12, when we look at save profiles.
5.10 Utility
The Utility folder in Assets/GPC_Framework/Scripts/COMMON/UTILITY holds
a host of files that do not fit into a category but could serve to be useful all the same.
5.10.1 AlignToGround
The AlignToGround class uses raycasting to find out what angle the ground under-
neath is and align a Transform to it. Raycasting is the process of drawing an imagi-
nary line and finding out which Colliders it hits. It uses Unity’s collision systems,
so if you want something to be detected by raycasting, the GameObject must have
a Collider Component attached to it. Unity’s raycasting system will return a whole
host of information along with a reference to the Collider(s) it finds along the ray-
cast, such as the exact point in 3d space the line intersects the object and more.
This script also uses Unity’s Quaternion.Lerp function to smooth out the
alignment. The alignment speed can be adjusted via the value in the variable
named smooth.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace GPC
{
public class AlignToGround : MonoBehaviour
5.10 Utility 91
{
public LayerMask mask;
private Transform _TR;
RaycastHit hit;
Quaternion targetRotation;
void Start()
{
_TR = GetComponent<Transform>();
}
void Update()
{
RaycastHit hit;
if (Physics.Raycast(_TR.position, -Vector3.
up, out hit, 2.0f))
{
targetRotation = Quaternion.
FromToRotation(_TR.up, hit.normal) * _TR.rotation;
_TR.rotation = Quaternion.Lerp
(_TR.rotation, targetRotation, Time.deltaTime * smooth);
}
}
}
}
92 5. Recipes
_TR.rotation = Quaternion.Lerp
(_TR.rotation, targetRotation, Time.deltaTime * smooth);
5.10.2 AutomaticDestroyObject
The main use for a script that automatically destroys its GameObject might be for
special effects. Particle effects such as explosions will be instantiated, their effect
will play out and then they need to be destroyed.
In the example games, a short and simple class called AutomaticDestroy
Object.cs is attached to a GameObject. After a set amount of time, set in the Unity
editor Inspector window, the GameObject is destroyed along with any associated
child objects attached to it.
The full script:
public class AutomaticDestroyObject : MonoBehaviour
{
public float timeBeforeObjectDestroys;
void Start () {
// the function destroyGO() will be called in
timeBeforeObjectDestroys seconds
Invoke("DestroyGO",timeBeforeObjectDestroys);
}
void DestroyGO () {
// destroy this gameObject
Destroy(gameObject);
}
}
The class, which derives from MonoBehaviour, is very simple. It uses Invoke
to schedule a call to the DestroyGO() function at a time set by the public variable
timeBeforeObjectDestroys. It probably goes without saying that DestroyGO()
takes care of destroying itself, with Destroy(gameObject).
5.10.3 AutoSpinObject
One common action you may need to do is spin an object. This may be for several
reasons, the most obvious being to draw attention to pickups. By spinning the
pickups, they are more obvious to the player than static which could be easily
missed or ignored.
Making an object spin in Unity may be very simple but having a script to
take care of this is a useful little tool to keep in the toolkit.
using UnityEngine;
using System.Collections;
5.10 Utility 93
AutoSpinObject.cs derives from MonoBehaviour, so that it can use Unity’s
automatic calls to the Start() and Update() functions:
public class AutoSpinObject : MonoBehaviour
{
void Start ()
{
myTransform=transform;
}
void Update () {
myTransform.Rotate (spinVector*Time.deltaTime);
}
}
5.10.4 FaceCamera
The FaceCamera script Component uses Unity’s built-in LookAt function to
make a GameObject face toward the main Camera in the Scene. Note that this
uses Camera.main to find the camera, which means it relies on the camera
GameObject’s tag being set to MainCamera for it to work.
5.10.5 LookAtCamera
The LookAtTransform script uses Unity’s built-in LookAt function to make a
GameObject face toward another Transform (set as target on the Component via
the Inspector).
5.10.6 PretendFriction
The easiest way to move objects around in Unity is to simply apply forces to your
rigidbodies. By using Rigidbody.AddForce and perhaps a multiplier based on
Input.GetAxis or something similar, it is quite easy to get objects moving around
but the only way to have them come to a stop is either to have them make friction
against something else in the game world or to change drag values on the
94 5. Recipes
Rigidbody. This may not always be an ideal solution and can present challenges
when trying to control the turning behavior of objects floating in space or perhaps
even character controllers. To help with controlling sideways slip, this simple
PretendFriction.cs script can help.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Utility/Pretend
Friction")]
The script derives from MonoBehaviour, to tap into the Start() and
FixedUpdate() system calls. Above, note that you can change the value of the
variable theGrip to alter how much this script will control sliding.
void Start ()
{
// cache some references to our RigidBody, mass and
transform
_RB = GetComponent<Rigidbody>();
myMass = _RB.mass;
_TR = transform;
}
The Start() function caches the Rigidbody, amount of mass on the Rigidbody
and the Transform.
void FixedUpdate ()
{
// grab the values we need to calculate grip
myRight = _TR.right;
5.10 Utility 95
The FixedUpdate() function will calculate how much an object is sliding – to
do this, it finds how much sideways movement the object is making (to the right
relative to the Transform’s rotation) – to find out how much slip is happening.
5.10.7 TimerClass
Our timer system is named TimerClass.cs:
using UnityEngine;
using System.Collections;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Common/Timer class")]
In this script, we will only update time whenever the time is requested.
Although there may be cases where the timer would need to be updated con-
stantly, we only need to do this on demand for the games in this book. For the
example games, we do not derive the class from MonoBehaviour and we call
UpdateTimer() from another script; the Game Manager. If you want this script to
update automatically, we will outline a method to modify the script to work like
that at the end of this section.
The timer works by having a variable called currentTime that stores the
amount of time since the timer was started. currentTime starts at zero, then
UpdateTimer() calculates out how much time goes by between updates and adds
it to currentTime. The value of currentTime can then be parsed into minutes,
seconds, and milliseconds and returned as a nice, tidy formatted string in the
GetFormattedTime() function of the TimerClass class.
UpdateTimer() will be the only function that updates the time system:
public void UpdateTimer()
{
// calculate the time elapsed since the last
Update()
timeElapsed = Mathf.Abs(Time.
realtimeSinceStartup - lastTime);
96 5. Recipes
// if the timer is running, we add the time
elapsed to the current time (advancing the timer)
if (isTimerRunning)
{
currentTime += timeElapsed *
timeScaleFactor;
}
Note: One commonly used method of pausing Unity games is to set Time.time
Scale to 0, which would stop a timer that didn’t use Time.realtimeSinceStartup. If
you use Time.timeScale to pause the game, you will also need to be used to start
and stop the timer as it will be unaffected by Time.timeScale.
5.10 Utility 97
reset when we start the timer, just in case the timer had previously been running.
If the timer had been running, we do this to make sure that any time ‘in-between’
the timer being stopped and the timer starting up again, is not be counted.
Above, we reset all the time variables to their starting states and call
UpdateTimer() to start the timer process again.
Whenever we need to display the time on the screen, there is a good chance
that we will need it to be formatted in a way like minutes: seconds: milliseconds.
The GetFormattedTime function takes this value and breaks it up into the units
we need, then puts together a nicely formatted string and returns it:
// grab hours
aHour = (int)currentTime / 3600;
aHour = aHour % 24;
// grab minutes
98 5. Recipes
aMinute = aMinute % 60;
// grab seconds
aSecond = (int)currentTime % 60;
// grab milliseconds
aMillis = (int)(currentTime * 100) % 100;
After minutes, seconds, and milliseconds values have been calculated and
stored in the integer variables aMinute, aSecond, and aMillis, three new strings
called seconds, minutes and mills are built from them:
// format strings for individual mm/ss/mills
tmp = (int)aSecond;
seconds = tmp.ToString("D2"); // ToString()
formats .. in this case, D followed by how many numbers
tmp = (int)aMinute;
minutes = tmp.ToString("D2");
tmp = (int)aHour;
hour = tmp.ToString("D2");
tmp = (int)aMillis;
mills = tmp.ToString("D2");
In C#, you can convert numbers to strings with the ToString() function.
Something great about ToString() is that you can also pass in a format string. The
format string takes the form of a letter followed by an integer number. Above, we
use the format string D2 to tell ToString() to use two digits, which will convert
our times into a neat format ready to display. Note that you can set as many digits
as you like, simply by changing the number in the format string. For more infor-
mation on format strings, you can find a full breakdown and a full list of all of the
available formatting options online at Microsoft’s .NET Standard numeric for-
mat strings page: https://docs.microsoft.com/en-us/dotnet/standard/base-types/
standard-numeric-format-strings
Once the individual parts of the formatted time string are ready, we put them
all together into a final timeString and return it at the end of the function, like this:
return timeString;
}
At the very end of the script, a GetTime() function provides a way for other
scripts to process the value of currentTime:
5.10 Utility 99
GetTime() just returns the value of the variable currentTime so that you can
take care of formatting or processing in another script.
5.10.8 WaypointsController
This script is so big and important that it gets its own chapter! Check out Chapter
8 to see how this script fits together and how it should be used in your own games.
5.11 Weapons
In the Weapons folder are four scripts: AutoShooter.cs, ProjectileController.cs,
StandardSlotWeaponController.cs and ThreeWayShooter.cs. These scripts and
how the weapons system can be used will be covered in full, in Chapter 7 of this
book.
This chapter is all about movement. We build the scripts that will determine the
way that a player moves and behaves in the physics world. It is entirely focused on
movement.
In this chapter, we look at three different movement controllers:
1. 3D humanoid character
It is assumed that this control script will be able to do humanoid things,
such as run forwards, backwards and turn around.
2. Wheeled vehicle
The vehicle controller utilizes Unity’s Wheel Colliders to work toward a
realistic physics simulation. There will be no gearing, but the vehicle will
be able to accelerate, brake or steer left and right.
3. 2D Platform Player
This 2D platform character controller offers left/right and jump func-
tionality with basic ground detection.
You can find all of the scripts for this chapter in the folder
Assets/GPC_Framework/Scripts/BASE/PLAYER/MOVEMENT.
6.1 Humanoid Character
The human control script uses Unity’s character controller and is based on the
third person character controller provided by Unity as a part of the game engine.
This scripts original behavior was to move the player based on the direction of the
camera and, here, it has been adapted to work either directionally or based on its
own rotation by toggling the moveDirectionally Boolean variable to true or false.
101
The BasePlayerCharacterController class derives from
ExtendedCustomMonoBehaviour, as described in Chapter 4 of this book.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Character/Base Player
Character Controller")]
[Space]
public float horz;
public float vert;
public bool isRespawning;
public bool isFinished;
public BaseInputController _inputController;
Above, the Init() function is called from Start(). As with many of the scripts
in this book, Init() contains set up code.
_RB = GetComponent<Rigidbody>();
_inputController =
GetComponent<BaseInputController>();
}
Above, moveDirection is a Vector3 used later in the script to tell the charac-
ter controller which direction to move in. Here in the Init() function, it is given
the world forward vector as its default value.
Init() then calls GetComponents(), which is a function in
ExtendedCustomMonoBehaviour.cs (which this class derives from) that will
grab references to the Transform and GameObject this script is attached to. We
also grab the Rigidbody (as this script relies on physics to work) and stores the
reference in the variable _RB. _inputController is populated with a reference to a
BaseInputController Component (or a Component that derives from
BaseInputController) so we can process input from the user.
The BaseInputController class is used to provide input from the player to
move the character around and it should be attached as a Component to the same
GameObject as this script. BaseInputController.cs is provided as part of the
framework for this book. You can use this script for input, or you could replace
the input system with whatever you need just as long as you feed the variables
named horz and vert with horizontal and vertical input values as floats.
public void SetUserInput( bool setInput )
{
canControl= setInput;
}
if (!canControl)
return;
horz = _inputController.GetHorizontal();
vert = _inputController.GetVertical();
}
LateUpdate() is used to call for an update to the input Boolean variables via
the GetInput() script. Unity advises that LateUpdate as the best place to check for
keyboard entry, after the engine has carried out its own input updates.
void UpdateSmoothedMovementDirection ()
{
if(moveDirectionally)
UpdateDirectionalMovement();
else
UpdateRotationMovement();
}
void UpdateDirectionalMovement()
{
// find target direction
targetDirection= horz * Vector3.right;
targetDirection+= vert * Vector3.forward;
Directional movement uses the world x and z axis to decide which way to
move; that is, the player moves along the world x or z axis based on the button
pressed by the player. Above, the UpdateDirectionalMovement() function starts
by taking the horizontal and vertical user inputs (from the variables horz and vert)
and multiplying them by either a vector that points along the axis that we want
horz or vert to affect. The targetDirection variable is a Vector3 that encompasses
both vertical and horizontal movement into a single vector representing the direc-
tion our player will move in.
The targetDirection variable will be zero when there is no user input, so before
calculating any movement amounts, we check that movement is intended first:
if (targetDirection != Vector3.zero)
{
moveDirection =
Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed
* Mathf.Deg2Rad * Time.deltaTime, 1000);
moveDirection = moveDirection.normalized;
}
The variable curSmooth will be used further in the script, to decide how long
it takes for the movement speed of the player to go from zero to max speed.
Time.time is used above to keep track of how long the player has been walk-
ing, which means that Time.timeScale will affect it (if the game is paused or time
is scaled, this timing will remain relative to the time scale).
targetSpeed *= runSpeed;
}
else
{
walkSpeed is a multiplier for the targetSpeed when the player is in its walk-
ing state:
targetSpeed *= walkSpeed;
}
When the speed changes, rather than changing the animation instantly
from slow to fast the variable curSmooth is used to transition speed smoothly.
The variable speedSmoothing is intended to be set in the Unity inspector window
and it is then multiplied by Time.deltaTime to make the speed transition time
based:
// Smooth the speed based on the current target direction
float curSmooth= speedSmoothing * Time.deltaTime;
The next line of the script caps the targetSpeed at 1. Taking the target
Direction vector from earlier, targetSpeed takes a maximum value of 1 or, if it is
less than 1, the value held by targetDirection.magnitude:
The rest of the function transitions between walking and running in the
same way that UpdateDirectionalMovement() function did earlier in this
section:
Above, Update() begins with a call to stop input when canControl is set to
false. More than anything, this is designed to bring the player to a stop when
canControl is set at the end of the game. When the game ends, rather than inputs
continuing and the player being left to run around behind the game over mes-
sage, inputs are set to zero.
UpdateSmoothedMovementDirection() calls one of two movement update
scripts, based on the value of the Boolean variable moveDirectionally.
void ApplyGravity()
{
// apply some gravity to the character
controller
float gravity = -9.81f * Time.deltaTime;
_charController.Move(new Vector3(0, gravity, 0));
}
6.2 Wheeled Vehicle
The BaseWheeledVehicle.cs script relies on one of Unity’s built-in WheelCollider
system. WheelCollider components are added to GameObjects positioned in
place of the wheels supporting the car body. The WheelCollider components
themselves do not have any visual form and meshes representing wheels are not
aligned automatically. WheelColliders are physics simulations; they take care of
the physics actions such as suspension and wheel friction.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Vehicles/Wheeled
Vehicle Controller")]
[RequireComponent(typeof(Rigidbody))]
Above, we start the class with the usual reference to UnityEngine, and then
add a menu item for this Component. Having a Rigidbody Component on the
same GameObject this script is attached to is important. The next tag (those lines
wrapped in square brackets are what Unity refers to as tags) is a RequireComponent.
This will check to see if a Rigidbody is attached to our GameObject and will add
one, if needed. We then set the namespace that all the framework comes under,
our GPC namespace.
Now into the class:
[Header("Wheel Colliders")]
The default input system is set to use the BaseInputController class from this
book’s framework. You can, of course, switch out input for whatever you like. We
use the _inputController variable to talk to BaseInputController and find out
what is going on with the inputs each update, which happens further down in
this class, in the GetInputs() function.
[Header("Steer settings")]
public float steerMax = 30f;
public float accelMax = 5000f;
public float brakeMax = 5000f;
[Space]
public float wheelToGroundCheckHeight = 0.5f;
[Space]
public bool fakeBrake;
public float fakeBrakeDivider = 0.95f;
[Space]
public bool turnHelp;
public float turnHelpAmount = 10f;
[System.NonSerialized]
public float mySpeed;
[System.NonSerialized]
public Vector3 velo;
[Header("Audio Settings")]
Most of the other variables in here will be covered further on in the script, as
we break it down, so we skip to the Start() function. Start()is called by the engine
automatically. It calls the Init() function, which begins by caching references to
commonly used objects:
public virtual void Start ()
{
Init ();
}
The center of mass can be set on a rigidbody to change the way it behaves in
the physics simulation. The further the center of mass is from the center of the
rigidbody, the more it will affect the physics behavior. Setting the center of mass
down at –1 helps make the vehicle stable and stops the vehicle from toppling over
when going around corners.
// see if we can find an engine sound source, if we need to
if (_engineSoundSource == null)
{
Above, if no engine sound source has been set up via the Inspector window
of the Unity editor, the script tries to find it with a call to GameObject.
GetComponent(). The sound source will be stored in engineSoundSource and
used solely for engine sound effects.
As with all the movement controllers, SetUserInput (next) sets a flag to
decide whether inputs should be used to drive the vehicle:
Unlike other movement controllers from this book, this one introduces a
lock state:
The lock state is for holding the vehicle in place without affecting its y-axis.
This differs from just disabling user input as it adds physical constraints to the
physics object to hold it still (its purpose being to hold the vehicle during the
counting in at the start of a race or other similar scenario).
public virtual void LateUpdate()
{
if(canControl)
GetInput();
UpdateEngineAudio();
}
Above, LateUpdate() starts by checking for input via the GetInput() func-
tion, when canControl is set to true. Next, UpdateEngineAudio() is called to
update the pitch of the engine sound based on the engine speed.
CheckLock() is called first, which holds the vehicle in place when the lock is
on (when isLocked is true) – ideal for holding vehicles in place at the start of a
race!
Above, the variable velo is set to hold the velocity of our vehicle rigidbody.
The next line converts that velocity to local coordinate space, so that we get values
for how fast the vehicle is moving along each of its axis. Our vehicle’s forward
speed is along the z axis and sideways along the x. We take that z axis velocity and
put into the variable mySpeed so that we know how fast our vehicle is moving.
mySpeed is used the next section, where we simulate a reverse gear when the
vehicle is moving below an arbitrary speed:
Above, bear in mind that this section of code is to simulate a reverse gear.
mySpeed is checked to see if its value is less than 2. When the speed is this slow
and the brake key is held down, brakeTorque is no longer applied to the wheels
and instead of brake torque, a reverse amount of the maximum amount of brake
torque (brakeMax) is applied to make the vehicle move backwards.
rearWheelLeft.brakeTorque = 0;
rearWheelRight.brakeTorque = 0;
In reverse mode, zero brakeTorque will be applied (by the code above) to the
WheelColliders so that the vehicle is free to move backward. Although braking is
dealt with above, we deal with the reverse motor further in the script.
When the vehicle is reversing, steering is applied in reverse too, just like a
real car.
if (turnHelp)
_RB.AddTorque(Vector3.up * steer * turnHelpAmount *
_RB.mass);
Above, we tell the WheelColliders how to move. The motor, brake and steer
values will be set by the GetInput() function later in this script.
if (Physics.Raycast(_frontWheelLeft.
transform.position, Vector3.down, wheelToGroundCheckHeight))
FLGrounded = true;
if (Physics.Raycast(_rearWheelLeft.transform.
position, Vector3.down, wheelToGroundCheckHeight))
RLGrounded = true;
if (Physics.Raycast(_rearWheelRight.
transform.position, Vector3.down, wheelToGroundCheckHeight))
RRGrounded = true;
When wheels are off the ground, our enhanced braking and turn helper code
needs to be disabled to avoid steering or braking in mid-air. To detect the ground,
the function above uses Unity’s Raycasting function to cast a ray straight down
from the center of each WheelCollider. A raycast distance value is used, held in
the float WheelToGroundCheckHeight. Individual Boolean flags are set for each
wheel’s grounded state. Those flags are checked at the end of the function to see
if all of them are true – if so, we know that all four wheels are on the ground so
isGrounded gets set to true. Otherwise, isGrounded is set to false.
public void CheckLock()
{
if (isLocked)
{
// control is locked out and we should be stopped
steer = 0;
brake = 0;
motor = 0;
// hold our rigidbody in place (but allow the Y to move
so the car may drop to the ground if it is not exactly matched to
the terrain)
Vector3 tempVEC = myBody.velocity;
tempVEC.x = 0;
tempVEC.z = 0;
myBody.velocity = tempVEC;
}
}
The CheckLock() function, above, looks to see if the Boolean variable isLocked
is set to true, then when it is freezes all inputs to zero and zeroes x and z velocities of
the vehicle’s rigidbody. The y velocity is left as is, so that gravity will still apply to the
vehicle when it is being held in place. This is important at the start of a race when the
vehicle is being held until the 3,2,1 counting in finishes. If the velocity along
the y-axis were zeroed too, the car would float just above its resting place against the
ground until the race started when it would drop down some as gravity is applied.
steer, motor and brake are the main float variables used for applying to the
vehicle to move it around. The function below, GetInput() gets values from the
inputs set by the default input system:
using UnityEngine;
namespace GPC
{
public class BaseWheelAlignment :
ExtendedCustomMonoBehaviour
{
Above, we only need access to the UnityEngine namespaces for this script,
and then we are adding this class to the GPC namespace so that it acts as a part of
the framework for this book. The class derives from ExtendedCustomMonoBehaviour
to use its Spawn() function and also one of its variables, _TR.
Skipping by the variable declarations (as they will become clearer as we work
through the script), we move down to the Start() function:
void Start()
{
_TR = GetComponent<Transform>();
_colliderTransform = _correspondingCollider.
transform;
}
_TR grabs a reference to the wheel Transform. This will be used to position
the visual wheel. On the following line above, we grab _colliderTransform, which
is a reference to the corresponding WheelCollider GameObject and will be used
to find out where to position the visual wheel further down in the script.
void Update()
{
RaycastHit hit;
Vector3 ColliderCenterPoint = _colliderTransform.
TransformPoint(_correspondingCollider.center);
if (Physics.Raycast(ColliderCenterPoint, -
_colliderTransform.up, out hit, _correspondingCollider.
suspensionDistance + _correspondingCollider.radius))
{
Above, the Update() function works by casting a ray down from the center of
the WheelCollider with the intention of positioning the wheel mesh based on
Above, we deal with the positioning of the wheel based on whether Physics.
Raycast finds a hit or not. If a hit is found, the position for the visual wheel is
made up of the hit point, plus the wheel Collider’s local up vector multiplied by
the wheel radius. This should sit the wheel just above the hit surface.
The second statement above calculates the wheel position at a fixed distance
from the WheelCollider’s center, that distance made up of the Collider’s local up
vector multiplied by the suspension distance. This is how we get to that fixed,
fully extended suspension distance mentioned earlier in this section.
_ correspondingCollider.GetGroundHit( out
correspondingGroundHit );
if ( Mathf.Abs( correspondingGroundHit.sidewaysSlip
)> slipAmountForTireSmoke )
{
The first task above is to fill our GroundHit typed variable corresponding-
GroundHit with information from our wheel’s WheelCollider. You do this by
calling GetGroundHit on the WheelCollider and passing in a WheelHit object
for it to populate on the way out.
Now correspondingGroundHit has data in it, in the next line we access that
sidewaysSlip value. To always ensure we get a positive number back from it, we use
Mathf.Abs and compare that value to the float variable slipAmountForTireSmoke.
if ( slipPrefab ) {
Spawn( slipPrefab, correspondingGroundHit.point,
Quaternion.identity );
}
}
}
using UnityEngine;
namespace GPC
{
public class BasePlayer2DPlatformCharacter :
ExtendedCustomMonoBehaviour
[System.NonSerialized]
public bool allow_left;
[System.NonSerialized]
public bool allow_right;
[System.NonSerialized]
public bool allow_jump;
Above, there is nothing particularly unusual about the variables or class dec-
larations for this script until we reach the groundLayerMask declaration.
groundLayerMask is a LayerMask type variable that is used by this class
to decide which collision layers to look for when trying to find ground under-
neath the player. When you see this Component attached to a GameObject,
and the GameObject is selected, as the Component appears in the Inspector
this variable will appear as a dropdown menu. The dropdown contains all
the defined collision layers in your project, and you can choose one or mul-
tiple, if required. Layers in Unity are represented as bitmasks. A bitmask is
used to access specific bits in a byte of data – you can think of a bitmask as a
binary number that represents something else. Layers can be difficult to
work with, but when you use a LayerMask, you avoid having to mess around
with bit shifting. To help out, the class that this class derives from,
ExtendedCustomMonoBehaviour, has a function in it called IsInLayerMask()
that simplifies working with layers even more.
Above, _inputController is declared too. _inputController uses the
BaseInputController class to read input from the player (in this case, keypresses)
and we can access the values in _inputController to know what the user is asking
our player to do and react.
Above, we grab the Rigidbody2D component for _RB2D and populate the
_inputController. Once we have those two variables set, didInit is set to true so
that we know set up has completed.
_inputController.CheckInput();
You can decide whether to allow your player to steer in the air by enabling
the canAirSteer Boolean (either set it in code, or via the Inspector on the
Component). Above, we check to see that either the player is on the ground or air
steering is enabled before grabbing inputs from _inputController for left and
right movements.
if ( _ inputController.Left && allow _ left)
{
moveVel.x = -runPower;
}
else if (_inputController.Right &&
allow_right)
{
moveVel.x = runPower;
}
else
{
if (allow_right || allow_left)
{
// stop if no left/
right keys are being pressed
moveVel.x = 0;
}
}
moveVel is a Vector2 variable used to hold the target movement amount each
step. Above, we set moveVel to either -runPower (running to the left), runPower
(right) or zero it altogether to stop the player in place. As this script is intended to
be used as a starting point for different types of games, it also contains the option
to turn on or off left or right movement with the allow_left and allow_right
Booleans. For the infinite runner example in this book, left and right movement
is enabled but not all infinite runners allow the player to move left and right;
some only require a jump button.
BaseInputController has both linear inputs in variables named vert and horz
(standing for vertical and horizontal) which will return values between –1 and 1,
as well as digital inputs in the variables Up, Down, Left and Right. Above, rather
// jump key
if (_inputController.Fire1 && allow_jump &&
isOnGround)
{
// stop if no left/right keys are
being pressed
moveVel.y = jumpPower;
Jump();
}
_RB2D.velocity = moveVel;
}
void CheckGround()
{
// assume we're NOT on the ground, first..
isOnGround = false;
// If it hits something...
if (hit.collider != null)
{
isOnGround = true;
}
}
To detect the ground beneath the player, this script uses the Physics2D.
Raycast function. We pass in the start position, the direction (a down vector), the
distance we want the ray to stop at and a LayerMask – groundLayerMask from
the top of the script we discussed earlier in this section. A ray will be cast down
from the player Transforms position and if the ray intersects with anything (if a
Collider on the right layer is found), information about the Collider and the hit
will be put into the RaycastHit2D variable hit.
All we need to do then is to check hit.collider. If no Collider was found dur-
ing the raycast, Physics2D.Raycast returns null as the Collider. If we know a
Collider was found, we know that there is ground underneath the player and we
can set isOnGround to true.
In this base class, there is only a virtual version of the Jump() function. This
should be overridden by your own derived scripts if you need to do anything else
when a jump happens. For example, the RunMan infinite runner game from
Chapter 2 of this book uses an overridden version of the Jump() function to tell
the player sprite to play its jumping animation.
1. BaseWeaponController
This is the framework for our weapon control system. It offers everything
the system requires and it is intended to be overridden for customization.
2. BaseWeaponScript
This takes care of individual weapon types. If you were to think of the
baseWeaponController as the arms of the player, the baseWeaponScript
would be the design of the weapon it is holding.
123
3. StandardSlotWeaponController
This class provides a method for taking input from the player and chang-
ing the currently selected weapon.
using UnityEngine;
using System.Collections.Generic;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Base Weapon
Controller")]
Default values are set for the currently selected weapon slot and the last
selected weapon. When the lastSelectedWeaponSlot variable is set to –1, it just
lets the weapon system know that it needs to set the weapon (to avoid duplicate
calls later on, whenever the weapon slot is set it checks to make sure that the new
weapon it is trying to set is different to the one currently set up – hence the –1 to
make the values of selectedWeaponSlot vs. lastSelectedWeaponSlot different on
initialization).
// initialize weapon list List
_weaponSlots = new List<GameObject>();
_TR = transform;
if (_weaponMountPoint == null)
_weaponMountPoint = _TR;
Above, we loop through the _weapons array and instantiate each one. When
the game starts and the above Start() function runs, the weapon prefab refer-
ences from the _weapons List will be instantiated one by one and positioned at
the weapon mount Transforms position. To make the weapon move around
with the player, we set our new weapons parent to the weapon mount (you can
only set the parent of a Transform, not a GameObject hence using .transform.
parent). We copy the layer used by the weapon mount and set the new weapon to
the same layer, before setting the position and rotation above.
weaponSlots.Add( TEMPgameObject );
These new objects are going to be the ones that the script manages, enabling
and disabling them as required and calling on them to act as required. Each new
weapon is added to a new List called weaponSlots:
The weapons need to start out disabled, otherwise they will all appear on top
of each other and all active. They are disabled here using the GameObject.
SetActive() Unity function:
When a projectile hits an enemy or a player, the object being hit needs to
know where the projectile came from to know how to react to it. Projectile iden-
tification can be done by layer. Set the layer on the gun mounting point object
(the object that will be the parent to the weapon) and this weapon script will use
the same layer for its projectiles.
Another method for checking where projectiles have come from is to assign
each player with a unique ID number and to use the SetOwner() function to tell
the projectiles who owns each one. The ID number is passed on to projectiles and
stored in the script each projectile has attached to it:
The new value of selectedWeaponSlot is then validated to make sure it is not less
than 0 or more than the total number of occupied slots –1 (since the weaponSlots
// next slot
selectedWeaponSlot++;
// prev slot
selectedWeaponSlot--;
Disabling the current weapon means that it will be both invisible and unable
to fire:
if(weaponScripts.Count==0)
return;
To disable the weapon, the script will need to talk to the BaseWeaponScript.
cs attached to it. Since weaponScripts and weaponSlots Lists were created in the
same order, the selectedWeaponSlot can be used as the index for either array.
To make sure that there are scripts in the weaponScripts array, its Count
property is checked before going any further:
// grab reference to currently selected weapon script
TEMPWeapon= ( BaseWeaponScript )weaponScripts[select
edWeaponSlot];
As well as disabling the actual weapon script, it now needs to be hidden from
view. First, variable _tempGO receives a reference to the weapon’s GameObject
from the weaponSlots List. The _tempGO is then set to inactive by GameObject.
SetActive():
GameObject _tempGO =
( GameObject )weaponSlots[selectedWeaponSlot];
_tempGO.SetActive( true );
}
By default, the Fire() function will launch projectiles along the Transform’s
forward axis, but there is support for firing along a fixed vector by setting the
useForceVectorDirection to true.
The function takes no parameters, making it easy to call from any other script:
if(weaponScripts==null)
return;
if(weaponScripts.Count==0)
return;
The weaponScripts List is checked to make sure that is has been properly
initialized and that it contains entries (a quick count check):
theDir = _ TR.forward;
The ownerNum was mentioned earlier in this section, where it is set by the
SetOwner() function. When the call to the currently selected weapon’s Fire()
function goes out, it takes a Vector3, and the owner ID.
7.1.2 BaseWeaponScript
The BaseWeaponScript class derives from ExtendedCustomMonoBehaviour:
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Base Weapon Script")]
[System.NonSerialized]
public Transform _theProjectile;
Start() calls Init(). Init() begins by grabbing a reference to the Transform. A call
to Reloaded() ensures that the weapon will be loaded and ready to go from the start.
public virtual void Enable()
{
canFire=true;
The Enable() or Disable() functions set the Boolean variable canFire to true or
false, respectively. This will be checked elsewhere in the code before allowing any
projectile firing. Note that the visual representation of the weapon is not hidden here;
that task is left to the slot control script to deal with, rather than the weapon itself:
When the weapon is fired, the assumption is made that it will not be loaded
for a certain period (otherwise you could in theory fire out thousands of projec-
tiles each second). To keep a track of when the weapon is in a loaded state, this
script uses the Boolean variable isLoaded. A timed call to the Reloaded() func-
tion, after firing, will reset the weapon state again:
public virtual void SetCollider( Collider aCollider )
{
_parentCollider= aCollider;
}
The Fire() function takes two parameters, a Vector3 to represent the direc-
tion of fire and an integer providing an ID number for its owner (which gets used
primarily by collision code to know how to react):
if( !canFire )
return;
The Boolean variable canFire needs to be true (the weapon can fire) and
isLoaded needs to be true (the weapon is ready to fire) before the function can
progress to check the state of ammunition:
// decrease ammo
ammo--;
// generate the actual projectile
FireProjectile( aDirection, ownerID );
Since all of the criterion has been met, the function goes ahead, and decre-
ments ammo in anticipation of the projectile about to be generated by the
FireProjectile() function. The direction held by aDirection and the owner ID in the
variable ownerID also get passed on to the FireProjectile() function as parameters:
This is where the Boolean variable isLoaded gets reset to false, to delay firing
for a reasonable amount of time (set by the value held in the variable
reloadTime):
CancelInvoke( "Reloaded" );
Invoke( "Reloaded", fireDelay );
}
CancelInvoke() is called to make sure that there is never more than one
Invoke call to Reloaded() waiting to activate:
The function MakeProjectile will do the work in getting a physical projectile into
the scene, but it returns a Transform that this function can then use to set up with:
// create a projectile
_theProjectile = Spawn(_projectilePrefab.transform,
_spawnPositionTR.position, _spawnPositionTR.rotation);
_theProjectile.SendMessage("SetOwnerType", ownerID,
SendMessageOptions.RequireReceiver);
Physics.IgnoreCollision( _theProjectile.
GetComponent<Collider>(), _parentCollider);
}
return _ theProjectile;
}
}
}
Above, the newly created projectile needs to be returned to the function call-
ing (especially when this is called from the Fire() function shown earlier in this
section).
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Common/Weapons/Standard
Slot Controller")]
// keys 1–9
string theKey = Input.inputString;
if (theKey == "")
return;
Rather than checking input for every key from 1 to 9, we take the string
theKey and convert it into a number. Char.ConvertToUtf32 will convert a string
into a Unicode code. The numbers 1–9 are represented by the numbers 49 to 58.
If we take the code from Char.ConvertToUtf32 and subtract 49 from it, we get a
number between 0 and 9 – perfect numbers to pass into the SetWeaponSlot()
function for choosing which weapon slot to use. Char.ConvertToUtf32 takes two
if (Input.GetKey("1"))
{
SetWeaponSlot(0);
}
if (Input.GetKey("2"))
{
SetWeaponSlot(1);
}
2. Provide an interface that can allow other scripts to search for the nearest
waypoint to a given point in 3d space
137
4. Provide an interface that allows for other scripts to find out the total
number of waypoints: as our cars hold their own waypoint counter num-
bers, they need some way to ensure that the counters stay within the
boundaries of how many waypoints there actually are.
In the racing game example in this book, we use waypoints for the AI and for
the main player;
1. To check that the main player is heading in the right direction: the car
controller code will track the player’s position on the track (based on
which waypoint has been passed) and check its forward vector to make
sure that it is facing the next waypoint along the track. If the player’s for-
ward vector is not within a certain tolerance angle, the game will display a
wrong way message and eventually respawn the car facing the right way.
2. The respawning system uses waypoints to find a ‘safe’ place along the
track to respawn the player as well as using its rotation to point the
respawned car in the right direction along the track.
3. To find out how far the vehicle has traveled around the track, which is used
to compare to the other players progress amounts to calculate race positions
using UnityEngine;
using System.Collections.Generic;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Utility/Waypoints
Controller")]
void Start()
{
GetTransforms();
}
After the variable and class declarations, above, the Start() function calls a
function called GetTransforms(). We will look at that in detail further down in
the function.
At the start of the function, we call GetTransforms() to make sure that the
information stored about all of the Transforms that make up our path are up to
date (in case they have been moved or new ones added).
When you first build a path, obviously it will start with a single waypoint.
With only one point, there is no end point for rendering a line to, so this function
checks that there are enough waypoints to draw a line and drops out if the
totalTransforms count is less than 2.
Transform _ tempTR = (Transform) _ transforms[0];
lastPos = _tempTR.position;
Above, we grab the first waypoint transform from the _transforms List, at
index 0, and store it in _tempTR. We do this so that we have a starting point for
drawing the lines that represent the path in the editor.
lastPos will act as the starting point for the next line, so at this stage we just
need it to have a value:
firstPoint = _ tempTR.position;
lastPoint = firstPoint;
_pointT = (Transform)_transforms[0];
Having all the Gizmos, lines, and editor helper graphics the same colors
would make life harder. For that reason, Unity provides the Gizmos.color func-
tion to set their colors. It takes just one call to Gizmos.color (passing in a Color
object) and everything after that point will be rendered using it.
Gizmos.color=Color.green;
Gizmos.DrawSphere(currentPoint,2);
As stated earlier in this chapter, the waypoints are used at one stage as
respawn points for vehicles that may be stuck or off-track. Their rotations are also
used to ensure that the repositioned vehicle is facing in the correct direction; for
that reason, we need the waypoints to face ‘forward’. To make sure that the rota-
tions of our waypoints are correct, we go through and point each one forward
toward the next one:
pointT.LookAt(currentPoint);
You can easily rotate a transform so that its forward points toward another
transform by using the transform.LookAt() function. It takes one or two param-
eters, the first being the transform you want to point toward and the second an
optional up vector.
Note that having this functionality happening all the time during
OnDrawGizmos() can make drag and drop editing difficult, as object pivots
rotate around automatically when you drag waypoints. Comment out the line
above if you find you are having trouble with automatically rotating points.
As the function continues, we continue to iterate through the waypoints and
then close the path, if the Boolean variable closed is set to true (you can set that
in the Inspector window of the editor when the gameObject that has this script
attached is selected):
// update our 'last' waypoint to become this
one as we
// move on to find the next...
lastPos = currentPos;
To make the waypoints control script easy to use, we need to make sure that
our waypoints are set up in a way in the scene where we want to use them. This
format is an empty GameObject with the WaypointsController.cs component
attached to it, with all waypoints as child objects in the Hierarchy. All of the
Transforms found in the GetTransforms() function are added to a List named _
transforms like this:
foreach(Transform t in transform)
{
// add this transform to our arraylist
_transforms.Add(t);
}
totalTransforms=(int)_transforms.Count;
}
distance = Mathf.Infinity;
int tempIndex = 0;
To find the nearest waypoint to the 3d vector fromPos, the variable distance
starts out at Mathf.Infinity (the computer equivalent of an infinite number!). We use
In some cases, using Transforms that are too far away may not be so useful,
so maxRange is provided to provide a distance limit, if needed.
// set our current 'winner' (closest transform) to
the transform we just found
_closest = _tempTR;
With the addition of the maxRange check, we can no longer be one hundred
percent sure of a return result, so a quick check makes sure that closest has some-
thing other than a zero in it before returning the result:
if( _ closest)
{
// return the waypoint we found in this test
return tempIndex;
} else {
// no waypoint was found, so return -1 (this
should be acccounted for at the other end!)
return -1;
}
This is the only place in the class where shouldReverse is used. GetWaypoint
will reverse the index numbers of the waypoints when shouldReverse is true (i.e.
Using GetWaypoint to get a waypoint with the index number of zero would
return the last waypoint in the path, instead of the first).
To reverse the index passed into this function, the variable index is modified
by subtracting it from the number of Transforms in totalTransforms.
if(index<0)
index=0;
}
If the index being requested is too high, rather than return a wrong waypoint
it will return a null value. The code on the requesting side can then check for a
null return value and act accordingly:
if(index>totalTransforms - 1)
return null;
Now that everything has been set up correctly, the transforms List contains
waypoints and the index number is within range, all that is left to do is return the
transform from the array:
The final piece in the waypoints controller script is GetTotal(), which is used
by other scripts to find out how many waypoints there are:
9.1.1 Mixing Audio
To access the Audio Mixer in Unity, use the menu Window/Audio/Audio Mixer.
The Mixer shows a list of mixers, snapshots, groups, and views down the left side.
The main part of the window will show sliders for groups within the currently
selected Mixer.
145
Figure 9.1 The Audio Mixer window allows you to change the balance between different
audio groups.
Figure 9.2 The Inspector shows properties of the currently selected Audio Mixer. You can
click on a field to expose it to code.
9.1.3 Audio Effects
Unity offers a variety of audio effects; you can add to AudioMixers to make your
sounds more interesting or to help balance them dynamically. To add effects, first
open the AudioMixer via the menu Window/Audio/Audio Mixer. Choose the
Group you want to add effects to. In the Audio Mixer panel, there is an Add but-
ton at the bottom of each audio panel. Click Add to show the effects menu. At this
point, let me draw your attention to the Duck Volume effect. Duck Volume will
take input from another audio group and drop the volume of the group it is
attached to. You may have already heard this effect in action in commercial vid-
eogames and tv shows during dialog, as it is most often used to drop the volume
of music whenever dialog is played. To accomplish this, you would have your
music and dialog channeled through two audio groups. Your music group would
have the Duck Volume effect on it. Your dialog group would have the Send effect
attached. In the Inspector with the dialog group selected, the Send effect has a
field named Receive that lets you choose which group should receive audio levels.
Setting this to the music channel would mean that the Duck Volume effect could
get this groups levels and duck the volume of the music accordingly. Using audio
ducking with notification sounds can also make the process of highlighting
important sounds easier.
There are two ways to play a sound with the BaseSoundManager class. The
sound manager can either play a sound at a set position, or you can tell it to play
a sound and it will either find the AudioListener in the Scene and play the sound
at the position of the Listener, or if it cannot find an AudioListener it will play the
sound at the position 0,0,0. Syntax looks like this:
BaseSoundManager.Instance.PlaySoundByIndex( the index number from
the array of sounds );
Or
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Audio;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Sound Controller")]
Above, we need System.Collections for the fading coroutine (see below, fur-
ther down in the script) and System.Collections.Generic to be able to use Lists for
storing SoundObject instances in. We also use UnityEngine.Audio for accessing
Unity's audio classes. We use AddComponentMenu to make a menu item for this
script in Unity and this script, like all the others in the framework, exists inside
the GPC namespace.
The SoundObject class is declared next – and just in case you are wondering?
Yes, it is okay to declare two functions in one script, in Unity:
Unlike almost all of the other classes in this book, the SoundObject class
does not derive from anything, as it is mostly used as a data container and its only
function just calls out to play a sound via Unity's PlayOneShot() function.
Above, the constructor of the SoundObject class (a function that runs auto-
matically when SoundObject is instanced) contains set up code to create a new
GameObject in the Scene for its AudioSource Component to be attached to.
When playing sounds, we do not call SoundObject directly – this is done further
down, by the BaseSoundManager class. Note that the constructor for SoundObject
has 5 parameters – when a SoundObject instance is created, each one will need
these parameters passed into it. You will see how this is done further down in the
BaseSoundManager class.
The constructor starts by creating a new GameObject with new GameObject(),
which takes a name as a parameter – naming the new GameObject based on this.
To make sure we know what this GameObject is, in the Scene's Hierarchy, it is
named AudioSource followed by a name passed into this constructor as a param-
eter. Next, we set the parent of the new GameObject here, too, or more specifi-
cally the parent of its Transform. This is all about keeping sound GameObjects
together in the Scene to keep it all neat and tidy and we get the parent Transform
from a passed in parameter named myParent.
sourceTR = sourceGO.transform;
source = sourceGO.AddComponent<AudioSource>();
source.outputAudioMixerGroup = theMixer;
source.playOnAwake = false;
source.clip = aClip;
source.volume = aVolume;
source.maxDistance = 2000;
}
Every time we want to set the position of a sound, we will need to access
the Transform of this GameObject and we store a reference to the Transform
in the variable sourceTR, so that we do not have to look up the Transform
repeatedly.
Next, we add an AudioSource to our new GameObject with GameObject.
AddComponent(). We keep a reference to the AudioSource in the variable source
for easy access later, and so that we can set the relevant properties on it, above.
The properties we set on the AudioSource are:
Clip – This is the AudioClip (the sample) that will be played by this AudioSource.
maxDistance – This is the maximum distance at which our sound should be heard.
There is nothing much to note about the variable declarations. Most of these
should become clear as we work through the script below.
As this script does not check for multiple instances, care must be taken to
ensure that there is only one instance per Scene.
void Start()
{
soundObjectList = new List<SoundObject>();
foreach (AudioClip theSound in GameSounds)
{
SoundObjects are stored in a generic List. Above, we initialize the List soun-
dObjectList ready to be populated by information about our sounds and their
AudioSources and GameObjects.
To create the instances of SoundObjects, we then iterate through each
AudioClip in the GameSounds array (which should be set up in the Inspector in
the editor).
tempSoundObj =
(SoundObject)soundObjectList[anIndexNumber];
if (theListenerTransform != null)
tempSoundObj.PlaySound(theListenerTransform.position);
else
tempSoundObj.PlaySound(Vector3.zero);
}
This will fade out the exposed parameter in exposedParam of the AudioMixer
from 0 decibels to –80, in time duration x seconds.
The AI controller in this book is a state machine. You may have heard of this
before, but do we mean when we say state machine? Our AI system stores the
current state of the AI and runs code according to whichever state it is. When the
state changes, it runs different code. For example, when our state is set to chase,
the AI should move toward an object. When the state is set to stopped, the AI
should stand still. This is state machine AI.
In this book, the AI is split into several different scripts that fit together to add
functionality. You can think of our main AI class, BaseAIController, as a class con-
taining a set of utility functions for AI rather than a single all-encompassing script.
1. BaseAIController
This contains base code for AI such as chase/follow behavior.
2. WaypointsController
The code for dealing with waypoints: this is optional, depending on
whether you want waypoint following.
3. AIBotController
Using BaseAIController, this class can move a bot around, chase, and attack
a target and stand on guard to look for a target to chase and attack. This is
used by the Blaster game example in this book to move the bots around.
4. AISteeringController
This uses the BaseAIController and WaypointsController to provide steer-
ing inputs that will follow a waypoint path (used in the example racing
game in this book to drive the AI cars around the track).
We will be looking at the above scripts and breaking them down in this
chapter, as well as a method for getting the weapon system firing with AI.
Note that this AI system is only intended for 3D games.
155
10.1 AI States
Before getting to the main BaseAIController.cs script, take a look at AIStates.cs
below. It contains all of the AIStates from the previous section stored in an enu-
merator list similar to the one used by the character controller from Chapter 5 of
this book. This list will be used by the AI to determine its current required behav-
ior. The list is in its own file (the AIStates.cs file) and in a namespace called
AIStates so that we can easily make it accessible from any script used by the game:
using UnityEngine;
namespace AIStates
{
public enum AIState
{
moving_looking_for_target,
chasing_target,
backing_up_looking_for_target,
stopped_turning_left,
stopped_turning_right,
paused_looking_for_target,
translate_along_waypoint_path,
paused_no_target,
steer_to_waypoint,
steer_to_target,
}
}
using UnityEngine;
using AIStates;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/AI Controller")]
[Header("AI States")]
public AIState currentAIState;
public AIState targetAIState;
[Header("Enemy input")]
public float horz;
public float vert;
[System.NonSerialized]
public bool reachedLastWaypoint;
public float waypointDistance = 5f;
public float pathSmoothing = 2f;
public bool shouldReversePathFollowing;
public bool loopPath;
public bool destroyAtEndOfWaypoints;
public bool startAtFirstWaypoint;
There are a lot of variables to declare, so don’t worry about those. We will see how
they are used later in the breakdown. Other than ExtendedCustomMonoBehaviour,
this script only uses one more script from the framework in this book – the
WaypointsController class for using waypoints. Everything else is standalone and
ready to be used in your projects regardless of whether you use the framework.
Once we get past the class declaration, we kick things off with a Start() function:
public virtual void Start()
{
Init();
}
The Init() function takes care of making cached references to the GameObject,
the Transform and its Rigidbody (this script assumes that there will always be a
Rigidbody attached to the same GameObject it is attached to as a Component):
public virtual void Init ()
{
// cache ref to gameObject
_GO = GetComponent<GameObject>();
As with all of the other scripts in this book, didInit is a Boolean variable used
to determine whether or not the Init() function has successfully completed.
public void SetAIControl( bool state )
{
AIControlled= state;
}
The Update() function is called by Unity each frame update, but we need to be
sure that the script has properly initialized before doing anything significant with it
because it may be possible for Update() to be called before Init() has finished its run.
if( !AIControlled )
return;
// do AI updates
UpdateCurrentState();
}
In some cases, the AI script does not manipulate its GameObject’s physics
directly but instead acts as an input provider to another script which deals with
Above, the AI script uses horz and vert (both floats) to store horizontal and
vertical input, which are the standard variable names used in other scripts in this
book. Here in the UpdateCurrentState() function, they are reset to zero. This is
centering the inputs before other functions change them.
switch (currentAIState)
{
case AIState.paused_no_target:
// do nothing
break;
default
// idle (do nothing)
break;
}
As we know that this will only happen when a new current state is about to
be set, we can call code that only needs to be called when the AI enters the new
state. Just as UpdateCurrentState(), UpdateTargetState() is a placeholder that
should be overridden by your own logic.
Next is a collection of functions to set inputs for turning left and right or
moving forwards and backward:
public virtual void TurnLeft ()
{
horz= -1;
}
Each function above is there to make it easier to interface with other types of
control systems. You can override them in a derived class or call them from
another.
Next up is the LookAroundFor() function. This will be called when we need a
bot to chase a target. For an example of using it, check out the AIBotController.cs
script.
1. We have a target.
If the above conditions are met, SetAIState() is called to change the target
state to AIState.chasing_target – making our AI turn and move toward it.
Without a Transform in _followTarget, we have nothing to follow and the
function drops through to the else statement. Here, we will instead try to find a
suitable target using GameObject.FindGameObjectWithTag(). The new target
object will be the first object (if any object is returned) from that search.
Coming up next is one of the core Components of the AI controller – the
IsObstacleAhead() function. In this part, Physics.Raycast is used to raycast out
ahead (and slightly to the left or right) to check for obstacles. It returns an integer,
expressing its findings with the following values:
if (_TR == null)
{
return 0;
}
The return result for the function is stored in a variable named obstacle
HitType, which starts out getting set to a default value of 0 at the top of the
function.
The beginning of the function above checks to make sure that we have an
actual Transform in _TR. If not, we know that either this GameObject's Transform
has been destroyed, or something has gone wrong, so we return a result of 0.
To make it easier to visualize what is happening with the AI, this function
includes Debug.DrawRay calls which will make lines appear in the editor where
the rays are being cast. This will only happen in the editor when Gizmos are set
to true – use the Gizmos icon above the Scene or Game panels to set this.
Above, the position and direction data used to draw the debug rays match
with the ones we use to do the actual ray casting, below:
RaycastHit hit;
// cast a ray out forward from our AI and put the
'result' into the variable named hit
if (Physics.Raycast(_TR.position, _TR.forward +
(_TR.TransformDirection(Vector3.right) * 0.5f), out hit,
obstacleAvoidDistance, obstacleAvoidLayers))
The raycasting uses the AI bot’s forward vector (_TR.forward) with an offset
to the side. _TR.right is also multiplied by half and added to the forward vector,
so that the angle goes diagonally out from the bot – with the ray starting at
_TR.position.
What we consider to be obstacles are defined as anything which is set to use
one of the collision layers defined in the obstacleAvoidLayers LayerMask. You
can set one or multiple collision layers as obstacle layers in the Unity editors
Inspector on this GameObject.
The variable obstacleAvoidDistance is used as the distance parameter for ray
casting, which can be set in the Unity editor Inspector window.
If a call to Physics.Raycast finds something, it returns true, and that is exactly
what the line above is looking for. If something is found we will react to that,
next:
// obstacle
// it's a left hit, so it's a type 1 right
now (thought it could change when we check on the other side)
obstacleHitType=1;
}
obstacleHitType will form the return value for the function. Above, when
the ray cast finds an obstacle, the variable obstacleHitType is set to 1. This repre-
sents an obstacle on the left side (remember the list of return results from earlier
in this section?). Now a ray is cast out from the other side of the bot:
Above, the only difference to this ray cast and the one previous is that we
now reverse that right vector by multiplying it by –0.5 instead of 0.5, casting a ray
out in the opposite direction.
// obstacle
if( obstacleHitType==0 )
{
// if we haven't hit anything yet, this is
type 2
obstacleHitType=2;
} else {
// if we have hits on both left and
right raycasts, it's a type 3
obstacleHitType=3;
}
}
Next, we deal with the situation where obstacleHitType is not at its default 0.
If it is not 0, we know that both this ray cast and the previous one has found obsta-
cles. To represent obstacles found on both sides, obstacleHitType gets set to 3.
All that is left to do now is to return the findings of our ray casting:
return obstacleHitType;
}
If we are going to be doing anything with a target, the first step is making
sure we have a target to turn toward (above) in the variable aTarget.
relativeTarget = _ rotateTransform.InverseTransformPoint
( aTarget.position );
Above, we calculate a Vector3 that will represent the target position relative
to the AI so that we can look at this vector's values and establish the direction
toward the target. e.g. a positive x value means the target is to the right of the car,
negative to the left, and a positive z means the target is in front of the car and
negative z behind.
The newly calculated relativeTarget is then used above with Unity’s Mathf.
Atan2 function to find an angle of rotation from the vector. Atan returns the
angle in radians, whereas radians are preferable for this, so we use Mathf.
Rad2Deg to convert targetAngle from radians to degrees.
targetAngle = Mathf.Clamp(targetAngle, - _
followTargetMaxTurnAngle - targetAngle, _ followTargetMaxTurnAngle);
rotateTransform.Rotate( 0, targetAngle *
modelRotateSpeed * Time.deltaTime, 0 );
}
It creates a normalized vector from the two positions, then uses the new vec-
tor to cast a ray out from the bot at the length by maxChaseDistance.
Above, just before the actual ray cast, there is a debug line to represent what
is going on in this function so that it is easy to see what the AI is supposed to be
doing when you are in the Unity editor.
if (Physics.Raycast( _ TR.position +
(visionHeightOffset * _ TR.up), tempDirVec, out hit, Mathf.Infinity))
{
if (IsInLayerMask(hit.transform.gameObject.
layer, playerLayer))
{
return true;
}
}
// nothing found, so return false
return false;
Above, the ray cast starts from our Transform (_TR) position along with a
height offset. Sometimes you may not want the ray to be cast from the point of
origin on the AI – one reason for this could be that the point of origin is at the
bottom of the GameObject and may just hit the ground, so you can adjust the
height offset in the Inspector on this Component to make this happen.
tempDirVec (that vector we calculated earlier between the AI and its target)
is passed into Physics.Raycast() too, as the direction vector. Mathf.Inifinity is the
limit of the ray cast, since we do not need to limit it. When a hit occurs in the ray
cast, the next line of code checks to see if the hit object's layer is in the LayerMask
playerLayer. If the target is on the correct layer, we know that there is nothing
blocking the way so we can return true, otherwise execution falls through to
return false at the end of the function.
Since the AI bot does not know which waypoints to use, another script (such
as the game controller) will call SetWayController and pass in a
WaypointsController object.
if( shouldReversePathFollowing )
{
currentWaypointNum = totalWaypoints - 1;
} else {
currentWaypointNum = 0;
}
Init();
Among other things, the Init() function in this class caches a reference to the
AI bot’s Transform in the variable _TR. The SetWayController() function calls
Init() to make sure that the reference is set up and ready for it to use, as it reposi-
tions _TR at the first waypoint (when the Boolean variable startAtFirstWaypoint is
set to true).
The next two functions in the class are interface functions for other scripts to
change values, if needed:
if( totalWaypoints==0 )
{
// grab total waypoints
totalWaypoints= _waypointsController.
GetTotal();
return;
}
To keep this function safe (in case it is somehow called before the waypoints
have been correctly set up in the WaypointsController class) there is a check to
make sure that more than one waypoint was found. If totalWaypoints is zero,
another attempt is made to get the total from _waypointsController before it
drops out of the UpdateWaypoints() function.
if( _ currentWaypointTransform==null )
{
// grab our transform reference from the
waypoint controller
_currentWaypointTransform= _
waypointsController.GetWaypoint( currentWaypointNum );
}
myPosition= _TR.position;
myPosition.y= 0;
currentWayDist= Vector3.Distance(
nodePosition,myPosition );
The distance between the AI bot and next waypoint is calculated using the
two cached position variables, below. When the resulting currentWayDist value
is less than the value of waypointDistance, it means that the bot is close enough
to the current waypoint to advance to the next one.
if( shouldReversePathFollowing )
{
currentWaypointNum--;
} else {
currentWaypointNum++;
// now check to see if we have been all the way
around
if( currentWaypointNum>=totalWaypoints ){
// completed the route!
reachedLastWaypoint= true;
// if we are set to loop, reset the
currentWaypointNum to 0
if(loopPath)
{
currentWaypointNum= 0;
The shouldReversePathFollowing Boolean must have been set to false for the
following code to be executed, meaning that the currentWaypointNum has gone
past the final waypoint in the list held by the waypoints controller and that
currentWaypointNum needs to be reset to zero, if the variable loopPath is set to true.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Base/Enemy Controller")]
enemyScore = 0;
enemyHealth = defaultHealthAmount;
}
Above, the script is self-explanatory. The Init() function sets default values for
our enemy stats and each function provides a way to change or enemy stat set values.
using UnityEngine;
using AIStates;
namespace GPC
{
public class AIBotController : BaseEnemyStatsController
{
private Vector3 moveVec;
private Vector3 targetMoveVec;
private float distanceToChaseTarget;
Skipping through the variables, the only thing to note above is the class dec-
laration deriving from BaseEnemyStatsController as mentioned at the start of
the section.
obstacleFinderResult = IsObstacleAhead();
switch (currentAIState)
{
// -----------------------------
case AIState.move_looking_for_target:
LookForTarget();
break;
case AIState.chasing_target:
ChasingTarget();
break;
// -----------------------------
This function is called every frame, which means the logic we place in this
case statement will be called each frame too. This is, essentially, the main loop for
our AI states.
The code above is relatively straightforward – in each case, we check to see if
currentAIState matches our case and, if it does, call a function to perform every-
thing we want to do every frame in that state. For example, we call LookForTarget()
when the currentAIState is AIState.move_looking_for_target. When the state is
AIState.chasing_target, we call ChasingTarget().
The code for each state has been split out into an individual function, which
makes it easier to debug later. Having big chunks of code inside a massive case state-
ment is a recipe for disaster, so it is better to split them out like this to save headaches!
case AIState.
paused_looking_for_target:
Paused(true);
break;
case AIState.
move_along_waypoint_path:
MoveAlongWaypointPath();
break;
case AIState.paused_no_target:
break;
default:
// idle (do nothing)
break;
}
}
Above, we go through all the AIStates we want the bot to react to and
call functions to deal with each one. We only use the states chasing_target,
move_looking_for_target, stopped_turning_right, stopped_turning_left and
backing_up_looking_for_target.
When we have a target, the state is chasing_target unless an obstacle is found –
if there is an obstacle, we change to either stopped_turning_left, stopped_turn-
ing_right or backing_up_looking_for_target to avoid it. Otherwise, chasing_
target aims for the target and moves forward constantly.
If we are in any of the turning or backing up states, we remain in those until
the obstacleFinderResult – populated by a call to IsObstacleAhead() – reports
that the path forward is clear. When there are no obstacles in the backing_up_
looking_for_target state, we choose a random direction to turn in, to reduce the
risk of getting stuck whenever the path directly in front is blocked.
Next, the functions themselves:
void LookForTarget()
{
// look for chase target
LookAroundFor();
case 2: // go left
SetAIState(AIState.
stopped_turning_left);
break;
TurnTowardTarget( _ followTarget);
distanceToChaseTarget =
Vector3.Distance(_TR.position, _followTarget.position);
When the bot is chasing, the code above calls TurnTowardTarget() to turn
the bot toward _followTarget. TurnTowardTarget() is in BaseAIController.
The next step is to find the distance between our bot (_TR.position) and the
target (_followTarget.position) to go into the variable distanceToChaseTarget.
We then compare distanceToChaseTarget to our minChaseDistance to make sure
that we are not too close to the target. If the distance is higher than minChase-
Distance, we continue to call MoveForward() which will set the vert input vari-
able to continue to move the bot forward toward the target.
void BackUpLookingForTarget()
{
LookAroundFor();
MoveBack();
if (obstacleFinderResult == 0)
{
if (Random.Range(0, 100) > 50)
SetAIState(AIState.
stopped_turning_left);
else
SetAIState(AIState.
stopped_turning_right);
}
}
void StoppedTurnLeft()
{
// look for chase target
LookAroundFor();
if (obstacleFinderResult == 0)
SetAIState(AIState.move_looking_for_target);
}
void StoppedTurnRight()
{
// look for chase target
LookAroundFor();
void MoveAlongWaypointPath()
{
// make sure we have been initialized before
trying to access waypoints
if (!didInit && !reachedLastWaypoint)
return;
UpdateWaypoints();
if (!isStationary)
{
targetMoveVec =
Vector3.Normalize(_currentWaypointTransform.position - _TR.position);
moveVec =
Vector3.Lerp(moveVec, targetMoveVec, Time.deltaTime * pathSmoothing);
_TR.Translate(moveVec * moveSpeed *
Time.deltaTime);
Although none of the examples in this book use it, the function
MoveAlongWaypointPath() is provided here should you need it. It uses the way-
points system from Chapter 8 to move the bot along a path set by waypoints. The
bool isStationary can be used to start or stop the bot along its path. You can also
set the bool faceWaypoints to decide whether or not to have the bot face the way-
point it is moving toward, or to just move, and keep its rotation.
using UnityEngine;
using AIStates;
namespace GPC
{
public class AISteeringController : BaseAIController
{
This class is derived from BaseAIController to use its state machine system.
Above, the few variables will become clearer as we go through the script. The
_inputController should reference a BaseInsputController or class deriving from
BaseInputController that this AI is using for its inputs to drive the vehicle.
switch (currentAIState)
{
case AIState.
move_along_waypoint_path:
SteerToWaypoint();
break;
case AIState.chasing_target:
SteerToTarget();
break;
case AIState.paused_no_target:
// do nothing
break;
default:
// idle (do nothing)
break;
}
}
void SteerToWaypoint()
{
if (!didInit)
return;
UpdateWaypoints();
if (_currentWaypointTransform == null)
return;
Above, we check that the class has initialized (didInit is true) then make a
call to UpdateWaypoints() which is an inherited function from the
BaseAIController class. We then check that __currentWaypointTransform has
a Transform in it. The UpdateWaypoints() function takes care of updating
our current waypoint, checking the distances and so on, including setting
_currentWaypointTransform to provide us with a target to steer toward.
relativeWaypointPosition = _
TR.InverseTransformPoint( _ currentWaypointTransform.position);
// by dividing the horz position by the
magnitude, we get a decimal percentage of the turn angle that we
can use to drive the wheels
horz = (relativeWaypointPosition.x /
relativeWaypointPosition.magnitude);
Now that horz contains the amount required to turn toward the next way-
point, this value may be used to slow down the bot if it is moving too fast.
The absolute (never negative) value of horz is checked against 0.5 to decide
whether acceleration should be used. If the turn is more than 0.5, vert is unmodi-
fied at this stage, leaving it at its default value of 0. If the turn is less than 0.5, the
acceleration put into the variable vert is calculated by taking the relative
WaypointPosition z value divided by its magnitude, with the horizontal turn
amount subtracted from it.
The idea is, the more the turn the less the acceleration. In many cases, this
should help the AI to get around corners without crashing.
if (obstacleFinderResult == 1)
TurnRight();
if (obstacleFinderResult == 2)
TurnLeft();
horz *= turnMultiplier;
SteerToTarget() is not used by any of the game examples in this book, but it
is provided for you to have the functionality of steering at a target rather than
waypoints. This may be useful in a game where you want vehicles to follow other
vehicles.
We start the function by checking for initialization (didInit is true) and then
we make sure that there is a target Transform to follow held in the _followTarget
variable.
Vector3 relativeTargetPosition = transform.
InverseTransformPoint( _ followTarget.position);
horz = (relativeTargetPosition.x /
relativeTargetPosition.magnitude);
if (Vector3.Distance( _ followTarget.position,
_ TR.position) > minChaseDistance)
MoveForward();
else
NoMove();
Above, a quick test to check our distance to the target and stop moving if we
are too close or keep going – calling MoveForward() – if we are further than
minChaseDistance.
LookAroundFor();
if (obstacleFinderResult == 1)
TurnRight();
if (obstacleFinderResult == 2)
TurnLeft();
if (obstacleFinderResult == 3)
MoveBack();
}
_ inputController.vert = vert;
_inputController.horz = horz;
}
}
}
10.6.1 Attack States
The AIAttackStates.cs script stores an enumerated list of the possible states for
the armed enemy to take:
namespace AIAttackStates
{
public enum AIAttackState
{
random_fire,
look_and_destroy,
no_attack,
}
}
10.6.2 BaseEnemyWeaponController
BaseEnemyWeaponController will call a StandardSlotWeaponController Component
attached to the same GameObject. More specifically, it will call the Fire() func-
tion randomly so that the enemy fires at random times.
using UnityEngine;
using AIAttackStates;
As with most of the Init() functions in this book, the code above kicks off the
function above, by caching references to the GameObject and Transform that
this script Component is applied to.
if ( _ slotWeaponController == null)
{
_slotWeaponController =
_GO.GetComponent<StandardSlotWeaponController>();
}
The prime objective of this class is to manage the weapons system for the AI
players, which means it is going to need a reference to the weapon controller
script. If one has not been set in the Unity editor Inspector window on this scripts
GameObject, the code above finds out and try to find it with a call to GameObject.
GetComponent():
if ( _ rendererToTestAgainst == null)
{
_rendererToTestAgainst =
_GO.GetComponentInChildren<Renderer>();
}
The option is provided with this class to only fire the weapon when a speci-
fied Renderer is on screen by setting the Boolean variable onlyFireWhenOnScreen
to true in the Inspector. The intention is that the main enemy mesh Renderer is
canFire = true;
didInit = true;
}
Finally, in Init() above, we set didInit to true and canFire to true, too.
canFire is used to stop firing when we want to have the firing pause between
shots. canFire is used by this script to help control the delay between shots, but
if you ever need to stop the AI from firing you can set canControl to false at
any time.
if (!canControl)
return;
Firing();
}
Most of the core logic takes place in the Update() function, which is auto-
matically called by the Unity engine each frame. It is declared here as a virtual
function so that it can be overridden should you choose to inherit another class
from this one.
Above, if canControl is false, no weapon control is allowed so this function
drops out early before calling the Firing() function.
void Firing()
{
if (!didInit)
Init();
if(thisGameObjectShouldFire)
{
if( canFire )
{
doFire=true;
}
}
} else {
When an object is found in front of this Transform with the correct tag
applied, doFire is set to true and firing can happen later in the function.
if (doFire)
{
// we only want to fire if we are on-screen,
visible on the main camera
Above, the state of doFire is a condition for executing the next chunk of
code. The Boolean variable onlyFireWhenOnScreen states whether this enemy
should be allowed to fire when not being drawn by the Camera. The Renderer
Component referenced by rendererToTestAgainst is checked, using the Renderer.
IsVisibleFrom() function provided by Unity. IsVisibleFrom() takes a Camera as a
parameter and will return true if the Renderer is visible from that Camera (and,
of course, false when it is not). Above, we use Camera.main to find the Camera
with. For Camera.main to work, a Camera in the Scene must have the MainCamera
tag applied to it in the Inspector. This is the easiest way to find the Camera in a
Scene, but if for whatever reason this does not work for you, add a new Camera
typed variable to the variable declarations and use that variable here, instead of
Camera.main.
The code sets doFire to false when onlyFireWhenOnScreen is true and the
Renderer is not visible on screen. If firing is cancelled in this way, it also drops out
with a return statement since there is no value left in executing the script further
on this pass.
After the call to fire the weapon has been made, canFire is set to false to delay
firing until the function ResetFire() is called to set it back to true again. An
Invoke call sets up the call to ResetFire() at a time set by fireDelayTime and the
CancelInvoke just before that line ensures that any existing call to ResetFire() is
wiped out before we add this new one.
In this chapter, first up is a full breakdown of the main menu used for all of the
example games. The main menu structure should provide a perfect starting place
for any main menu system.
The second part of this chapter will look at in-game UI.
187
CanvasManager.cs:
using System.Collections;
using UnityEngine;
namespace GPC
{
public class CanvasManager : MonoBehaviour
{
public CanvasGroup[] _UICanvasGroups;
public float fadeDuration = 0.25f;
public float waitBetweenFadeTime = 0.1f;
[Space]
public CanvasGroup _FadeCanvasGroup;
void Awake()
{
HideAll();
}
As you can see above, we also set the alpha of each Canvas to 0 as well as set-
ting the CanvasGroup’s interactable and blocksRaycasts properties to false. This
is so that the Canvases will not just be hidden, but they will also not interfere
with other UI in the Scene.
We can also hide an individual Canvas with the HideCanvas() function, next:
StartCoroutine(FadeCanvasOut(_UICanvasGroups[indexNum],
fadeDuration));
}
The fading of our Canvases relies heavily on coroutines. There are three
different coroutines in this class: FadeCanvasOut, FadeCanvasIn and
Above, if doFade is true we use the coroutine but if it is false then the alpha,
interactable and blocksRaycasts properties of the Canvas are set directly from the
reference held in the _UICanvasGroups array.
Now that we can hide Canvases, we get to showing them:
lastGroup = currentGroup;
currentGroup = indexNum;
StartCoroutine(FadeCanvasIn(_UICanvasGroups[indexNum],
fadeDuration));
}
if (doFade)
{
StartCoroutine(FadeCanvasIn(_UICanvasGroups[indexNum],
fadeDuration));
}
else
{
_UICanvasGroups[indexNum].alpha = 1;
_UICanvasGroups[indexNum].
interactable = true;
_UICanvasGroups[indexNum].
blocksRaycasts = true;
}
}
Often you need to provide a method to get from a sub-menu back to the
main menu, or back to the previous screen. To speed up the process of developing
menus with this behavior, we have the LastCanvas() function, which changes the
Canvas currently being displayed to the last one. It uses the variable lastGroup to
keep tabs on what was previously open. As this is only a single level system, if you
need to get back from multiple levels of menu you will need to extend this further
or manually set which menu should display next.
In LastCanvas(), notice that it is not using a coroutine this time, but instead a
function named TimedSwitchCanvas() that will coordinate the timing to switch
Canvases. TimedSwitchCanvas() takes two array index values – the first, the
Canvas to hide and the second value is the Canvas to show. TimedSwitchCanvas()
will carry out a fade out, a pause (specified by WaitBetweenFadeTime variable,
accessible via the Inspector) and then fade in the new Canvas. This means to fade
out a UI Canvas and to fade in another one, we can use TimedSwitchCanvas()
without having to deal with the timing between fades.
The FadeIn() and FadeOut() functions are next:
Above, the fade Canvas can be hidden or displayed instantly by setting its
alpha value to 0 or 1 depending on whether it’s on or off.
The last function of CanvasManager is the TimedSwitchCanvas() function
mentioned above:
StartCoroutine(StartFadeCanvasSwitch(_UICanvasGroups[indexFrom],
_UICanvasGroups[indexTo], fadeDuration, waitBetweenFadeTime));
}
theGroup.interactable = true;
theGroup.blocksRaycasts = true;
Now we have everything set, the next chunk of code deals with the fade:
while (currentTime < fadeDuration)
{
currentTime += Time.deltaTime;
Above, Unity’s Mathf.Lerp function takes the current alpha value as a start
value, 1 as a destination and we calculate how much of a portion of that to put
into newAlpha by using currentTime divided by fadeDuration – this calculation
gives us a time value between 0 and 1, which in turn means Math.Lerp will return
a value between currentAlpha and 1 at the percentage represented by that value.
The alpha property of the Canvas gets set next (to newAlpha) and the corou-
tine ends with:
Essentially, yield return null says to Unity that we are done running code here,
for now, so wait for the next frame update and continue execution from this line
after that. The next line is the curly bracket wrapping our while loop, so when the
next frame starts, we go back to the top of the loop and go again until currentTime
is more than fadeDuration. Once the while loop is done, the coroutine finishes.
Next, the FadeCanvasOut coroutine works the same as the coroutine we just
discussed, except we set interactable and blocksRaycasts to false and the target
value for the Mathf.Lerp is 0 instead of 1:
theGroup.interactable = false;
theGroup.blocksRaycasts = false;
Above, that’s the fading out dealt with for the first Canvas. Between fades,
we now need a delay – the timing for which is passed in by the function calling
for this coroutine to start (in this script, this coroutine is called by
StartFadeCanvasSwitch and this delay time is set in the Wait Between Fade Time
field on the Component, in the Inspector):
endGroup.interactable = true;
endGroup.blocksRaycasts = true;
UI
The main menu script in this example uses a script Component named
MenuWithProfiles, which uses another Component named Base Profile Manager
for the saving and loading of music volumes. An empty GameObject named
MenuManager holds the menu script along with CanvasManager and the
Component Base Profile Manager. The CanvasManager contains a list of all the
canvases so that we can easily call on it to show or hide them as we need to.
To kick off this chapter, we take a look at the code behind the
BaseMainMenuManager script and then how it is overridden to make a menu
with profiles.
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace GPC
{
public class BaseMainMenuManager : MonoBehaviour
{
void Start()
{
Init();
}
Invoke("ShowMainMenu", 0.5f);
}
void SetupSliders()
{
// now set the UI sliders to match the values
in the mixer
float value;
_theAudioMixer.GetFloat("SoundVol", out
value);
_ sfxSlider.value = DecibelToLinear(value);
The next part of the code handles music volume in a similar way, except of course
it uses a different slider and a different exposed property on the AudioMixer (MusicVol):
Next, two functions used to set the volume from values in our sliders:
_ theAudioMixer.SetFloat("SoundVol", dbVol);
}
Above, the music volume is set the same way we did the sounds volume above.
private float DecibelToLinear(float dB)
{
float linear = Mathf.Pow(10.0f, dB / 20.0f);
return linear;
}
As mentioned earlier in this section, the user interface sliders work at a value
between 0 and 1 in this example. The utility function DecibelToLinear above will
take a value in decibels and return a number from 0 to 1 depending on how loud
or quiet the volume should be – between the range of –80 to 20db.
void ShowMainMenu()
{
_canvasManager.TimedSwitchCanvas(2,0);
}
Invoke("LoadGameScene", 1f);
}
ExitGame() uses Application.Quit() to close the game, which will only hap-
pen in a standalone build. It will do nothing in the editor.
public virtual void OptionsBackToMainMenu()
{
// return to main menu from options
_canvasManager.LastCanvas();
}
The last two functions above deal with going back to the main menu, using
the CanvasManager.LastCanvas() function which hides the current Canvas and
shows the previous one.
[RequireComponent(typeof(BaseProfileManager))]
As this class derives from BaseMainMenuManager, once it has done its own
additional initialization in the Init() function we need to call Init() on the base
class so that the main menu runs through its initialization for the main menu too.
void LoadProfileVolumes()
{
// get volume values from profile manager
_theAudioMixer.SetFloat("SoundVol",
_profileManager.GetSFXVolume());
Above, the process for setting the music volume is the same as we set the
sound effects only the volume comes in from GetMusicVolume() and we set the
MusicVol property on the AudioMixer this time to affect the music levels.
Saving and loading user data can be handled in many ways. For example, you might
just store everything in a static class and dump it out to a .JSON file whenever you
need to. Perhaps you want to use .xml format files, or even a database? In this chap-
ter, we will look at a method for saving and loading user data that uses a
ScriptableObject combined with .JSON for its format. The ScriptableObject contains
an array of instances of a data class named ProfileData. The ProfileData class holds
everything we want to save and load to Profiles. By using an array of ProfileData in
the ScriptableObject, we can easily load, and save to more than one save profile.
The system in this chapter works with save slots, although there is no exam-
ple user interface to choose a different slot. The example games simply use the
first profile, but if you decide to take this further and build out an interface for
multiple slots, you can call ChooseProfile(<slot number>) to select whichever slot
ties up with your user interface.
201
The namespaces this script refers to are UnityEngine and System:
using UnityEngine;
using System;
namespace GPC
{
The class exists within the GPC namespace, which is used by this book for its
framework. You could just as easily change or remove the namespace the scripts
in this chapter reside in, but I find it helps to keep everything in a namespace like
this so that you a) know where the script came from originally and b) scripts
don’t overlap each other with variable names or function names and so forth.
Filename – This will be used as the default name for a ScriptableObject when
you use the menu to add it to the project.
Menu name – This defines how the item should appear in Unity’s menus and
where it should be located. By default, the item will appear in the Assets/Create
menu. You can then specify sub-folders within that folder to categorize and
organize your menu assets. In the example code above, the asset will appear in
Assets/Create/CSharpFramework/ProfileScriptableObject.
[Serializable]
public class ProfileScriptableObject : ScriptableObject
{
public Profiles theProfileData;
}
Above, the chunk of code starts out with [Serializable]. This is very impor-
tant, particularly as we are saving and loading data. Serialization, put simply, is
the act of taking a class like this and being able to read or write it as a sequence of
data suitable for transport such as saving and loading to or from disk. If we do not
tell Unity what we want to serialize, our profile save, and load code will not work.
The class declaration makes sure we derive from ScriptableObject, then
inside the class there is a single variable declaration for the ProfileData. The
ProfileScriptableObject ScriptableObject only needs this single variable named
theProfileData. Its type, Profiles, refers to a class named Profiles declared next, in
the same C# file:
[Serializable]
public class Profiles
The variable Profiles, above, can contain multiple instances of the ProfileData
class so that we can store more than one user profile in the same system/file. The
ProfileData class is next in the script and it contains everything we want to store
from the game:
[Serializable]
public class ProfileData
{
[SerializeField]
public int myID;
[SerializeField]
public bool inUse;
[SerializeField]
public string profileName = "EMPTY";
[SerializeField]
public string playerName = "Anonymous";
[SerializeField]
public int highScore;
[SerializeField]
public float sfxVolume;
[SerializeField]
public float musicVolume;
}
}
Above, first note that [Serializable] is used on the class whereas [SerializeField]
is used on each variable. Having these tags for your user data variables is impor-
tant, again, for saving, and loading to work correctly.
ProfileData in its current state offers the most basic of settings and game
information a game might need. When you develop your own games, should you
use this system, you will need to add in more variables for whatever you need to
store. Keep in mind that certain types like Vector3 are not directly serializable and
you may have to do some work to store them in imaginative ways. For example,
whenever I store a Vector3 for saving and loading I store them as an instance of my
own ‘clone’ of the Vector3 type containing three separate variables for the x, y, and
z coordinates.
I do not know if there is an easy way to tell which types are serializable, or if
there is a list anywhere you can refer to, so Google search is going to be your best
option for this.
using UnityEngine;
using System;
namespace GPC
{
public class BaseProfileManager : MonoBehaviour
{
[System.NonSerialized]
public bool didInit;
public ProfileScriptableObject profileObject;
public static ProfileData loadedProfile;
public string profileSaveName = "Game_ProfileData";
Above, theFileName is a string that makes up the file path and file name of
our save profile .JSON file. Using Application.persistentDataPath gives you a safe
place to store profile data. Although it can still be deleted by the user, you can be
confident that your game will have access to this location and that it will not be
wiped out by updates to the main game files. Added to this is the string held in
profileSaveName and we add the file extension .JSON to the end. You could
change the file type to whatever you wanted, by the way. It will not make any dif-
ference to the loading or saving of the file – it is just there to tell other users what
type of file this is. If this was used in my game, I would probably save it as a .jef file!
Before we can do anything with Profiles, we either load them, or create new
ones. The code above uses System.IO.File.Exists() to see if our file exists before
trying to access it (you should always do this!). If the file exists, we know we
can load it so the LoadAllProfiles() function is called. If not, we call
CreateEmptyProfiles() to make new ones.
didInit = true;
}
GetProfileName() is used to get the name of the save profile. In many games,
this would be something like the name of the user followed by the data and time
that the profile was last accessed. Making the profile names easy to identify like
that will help your players to figure out which profile to load.
The profile name is stored as a variable in the profile data, but unfortunately
this means that the path to get to it is a little long-winded: profileObject (the
ScriptableObject) .theProfileData (the data) .profiles[whichSlot] (item whichslot
in the array of profile data) .profileName – finally, the profile name!
public void ResetProfile(int whichSlot)
{
// here, we reset all of the variables in our
profile to their default values
profileObject.theProfileData.profiles[whichSlot].inUse = false;
profileObject.theProfileData.profiles[whichSlot].musicVolume =
0; // default music and sound to full volume (NOTE: In decibels!)
profileObject.theProfileData.profiles[whichSlot].sfxVolume = 0;
profileObject.theProfileData.profiles[whichSlot].profileName =
"EMPTY PROFILE";
profileObject.theProfileData.profiles[whichSlot].highScore = 0;
}
Most profile systems include a method for players to delete old Profiles or
overwrite them with new ones. Above, the ResetProfile() script takes the profile
index number as a parameter and will set up the default values for that particular
profile. If you extend the profile system to include additional data, you will need
to add additional default states to this function.
The first thing we set is the .inUse Boolean. This is used by the profile man-
ager to tell which Profiles are active and which are not. One example use for the
inUse flag might be, since we are using multiple Profiles in a single file, display
code checking inUse to know whether to show details of the save profile or
whether to show a ‘create new profile’ button instead.
The rest of the default values are: musicVolume, sfxVolume, profileName,
and highScore. Above, they are set to whatever values I think make good defaults.
Back in the Init() function, when a profile save file was not found by System.
IO.File.Exists(), we called the CreateEmptyProfiles() function above. At this
point, we can assume that the ScriptableObject used for holding profile data is
empty. This function will set up a new array to go into the ScriptableObject’s
array, and then new ProfileData instances to populate the array with.
After the array is populated, a call to SaveProfiles() goes out to save the file to
disk. Next time Init() runs, rather than calling this function it should find the
new file and load it, instead.
if (profileObject.theProfileData.
profiles[whichSlot].inUse == false)
{
profileObject.theProfileData.profiles[whichSlot].inUse = true;
The profileName is set above to a string containing the date and time, cour-
tesy of C Sharp’s DateTime class. This will make it easy to identify the latest saved
game when there are multiple Profiles in play.
Next, the inUse flag is set to true for this profile so that we can easily tell it is
active.
profileObject.theProfileData.profiles[whichSlot].playerName =
"Anonymous";
profileObject.theProfileData.profiles[whichSlot].musicVolume =
0; // default music and sound to full volume (NOTE: In decibels!)
profileObject.theProfileData.profiles[whichSlot].sfxVolume = 0;
profileObject.theProfileData.profiles[whichSlot].highScore = 0;
}
The code above sets up default values for the player name, audio volumes,
and high score.
The variable loadedProfile is set to the chosen profile above, with a quick
debug log message sent out so that we can easily see which profile is active from
inside the Unity editor.
string theFileName =
Application.persistentDataPath + "/" + profileSaveName + ".JSON";
Remember that the save game profile system is essentially an array of objects.
As long as those objects are serializable, we can use save time by using built-in
functions for saving and loading. And this is exactly what we are doing in this
next section of code. Above, we set the file path and file name. This is made up of
Application.persistentDataPath followed by a forward slash (very important) and
them the profileSaveName variable and our file extension. If you change any of
the code in this class you will need to be absolutely sure that whenever file names
are used, they match exactly. For example, if you changed the file extension ear-
lier to .jef you would need to do so here, too.
string jsonString =
System.IO.File.ReadAllText(theFileName);
The file already on disk should be formatted in JSON, since it was created by
our BaseProfileManager class. To load it back in, we use System.IO.File.
ReadAllText() to load the entire file into one big string. Now that we have our
JSON formatted data in a string, it can be parsed in using the JsonUtility class:
Profiles tempProfiles =
JsonUtility.FromJson<Profiles>(jsonString);
profileObject.theProfileData = tempProfiles;
}
You may recall from Section 12.1 inside the ScriptableObject we declared
only one variable named theProfileData. It was typed Profiles, which is a small
class holding an array of profile data. Above, we use JsonUtility.FromJson to
translate the data inside the string we just loaded, back to a Profiles object like the
one it started out as, when the file was last saved. tempProfiles holds the loaded
data, so on the next line it is just a case of setting our profileObject’s variable
theProfileData to tempProfiles. In summary, what we did in those two lines above
was to parse the data out from the string into a local (temporary) variable, then
copy the data inside that local variable into our ScriptableObject profileObject.
string jsonString =
JsonUtility.ToJson(profileObject.theProfileData);
We use Unity’s JsonUtility class and its ToJson() function to convert our pro-
file data into a JSON formatted string. Wait, what? This requires a little more
explanation. Right now, all the profile data is held in the ScriptableObject prof-
ileObject. All we do to get this converted into JSON is to pass in the instance of
the class containing the data. Just as long as the data is serializable, JsonUtility.
ToJson() will iterate through the class and all of its variables and format it into a
string for us. Pretty neat, right?
Above, we set up the file path and filename for saving the file to (again, mak-
ing sure that this exactly matches the file path and filenames used elsewhere in
the class).
The profile data is now JSON formatted in the local string variable named
jsonString, so we can just dump it all out as a text file using System.IO.File.
WriteAllText(), which takes two parameters – the file name and the string data.
The remaining functions in this class are used to get and set individual vari-
ables on the profile data, such as setting audio volumes or getting the high score:
return false;
}
Loading and saving Scenes in Unity is done with the built in SceneManager class.
SceneManager offers a range of utilities for Scenes – everything from straight
loading to combining and manipulating multiple Scenes at the same time.
The RunMan game example from Chapter 2 used SceneManager to load the
game from the main menu using SceneManager.LoadScene(). It is a function
that will load a new Scene and unload the current one without you having to do
anything further. Later in this book, the Blaster and Racing Game examples
both use an alternative method that loads two Scenes, combines them together
into one and then manually unloads the current Scene. This is useful in cases
where the game logic remains the same throughout, but you want to load in dif-
ferent environments and/or enemies and so forth. Rather than copying the
game logic to every Scene, you can make a core Scene and separate individual
level Scenes which are combined at runtime into one. Traditionally, in Unity,
this has been achieved by using a single GameObject containing the game logic
and making it persist across Scenes with the DoNotDestroyOnLoad tag. Having
to maintain that object and make sure that all references are correctly disposed
of, states reset, and new objects created each time, can be messy, and difficult to
manage.
When you just use SceneManager’s LoadScene() function in Unity, loading
locks everything up until the new Scene has finished loading. This may be fine if
the delay is less than a few seconds, perhaps loading a simple Scene like the
RunMan game from Chapter 2. On the other hand, long delays or lockups nega-
tively impact the perceived polish level of your game and it can look as though
something has gone wrong as the user waits for the Scene to update. The
LevelLoader class from this chapter loads levels asynchronously, meaning we can
211
do cool things as the loading takes place such as showing a percentage complete
amount, a progress bar, or a spinning image. It is, after all, the small touches like
these that help your game appear more polished to its players.
In the Unity editor, you can work on multiple Scenes at the same time by
dragging Scenes from the Project browser directly into the Hierarchy panel.
Check out the example Blaster and Racing games in this book for more on setting
up and editing separated core and level Scenes like this.
All of the scripts for this chapter can be found in the folder Assets/GPC_
Framework/Scripts/COMMON/LEVEL LOADING.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Utility/Level Loader")]
Variable declarations are basic for this script. If you want to start the loading
process automatically when the Scene that this script runs in, check the loadOn-
SceneStart checkbox in the Inspector on this Component and set our first
declared variable, a Boolean, to true.
Next above, there are two strings. One for the coreSceneName and one for
the levelSceneToLoad. Remember when we said that we use two independent
Scenes? You need to populate these strings with the names of your core and level
Scenes if you want to start the loading process automatically. Of course, if your
players are going to be moving from level to level, the level name will need to be
changed and updated as they progress through the game.
The LoadLevel() function comes in two flavors. The first, which takes no
parameters, will use the string in the variable levelSceneToLoad as the Scene to
load, and load it. This is provided mostly so that we can automatically initiate a
load in the Start() function, but you may find other uses for it. The first LoadLevel()
calls the second LoadLevel() function in our script below, requiring a Scene name
string as a parameter:
The second LoadLevel above calls StartCoroutine() to start the loading pro-
cess, starting the co-routine named LoadAsyncLevels.
AsyncOperation asyncLoad =
SceneManager.LoadSceneAsync(whichLevel, LoadSceneMode.Additive);
AsyncOperation asyncLoad2 =
SceneManager.LoadSceneAsync(coreSceneName, LoadSceneMode.Additive);
Now that the level Scene has finished loading, above we start the next load
with another call to SceneManager.LoadSceneAsync. This time, we pass in the
coreSceneName to tell it to load the core Scene. Again, the LoadSceneMode is
additive so that it will not interfere with the level already in memory.
Above, the same procedure as for level loading in that we poll isDone and
yield each frame until the Scene is ready to go. Now that our two Scenes are
loaded, we can move on to getting them set up and running:
A little diversion above, as we need to deal with a little problem that occurs
when Unity detects more than one Camera in a Scene – it reports an error to the
debug log console. At the time of writing, a bug in the engine causes the error to
continue being reported even after any additional Cameras have been removed.
Although it may be possible there is a way to rearrange this sequence of events so
that Unity does not do this, the only solution I could find was to set the current
main Camera’s active state to false – effectively disabling the Camera before
Scene activation happens.
SceneManager.MergeScenes(SceneManager.
GetSceneByName(whichLevel),
SceneManager.GetSceneByName(coreSceneName));
SceneManager.SetActiveScene(SceneManager.
GetSceneByName(coreSceneName));
AsyncOperation asyncLoad3 =
SceneManager.UnloadSceneAsync(loaderScene);
With the current Scene set to our new, merged Scene, the SceneManager.
UnloadSceneAsync() function is used to unload the Scene we were in whenever
the loading started.
Before we reach the end of the co-routine, the code chunk above checks
isDone to monitor the unloading process. Once the previous Scene has been fully
unloaded, its AsyncOperation.isDone Boolean will be true and we can move on
to the end of the co-routine:
yield return new WaitForSeconds(1);
}
}
}
The last piece of the puzzle is to wait for one second before the co-routine
shuts down.
using UnityEngine;
namespace GPC
{
[AddComponentMenu("CSharpBookCode/Utility/Level Vars")]
namespace GPC
{
public class MultiLevelLoader : LevelLoader
{
Above, our script starts with the usual namespaces and a class declaration
that states our new class derives from LevelLoader.
2. Where you want to start the game, add the following code:
3. Make sure that the core Scene name above matches with whatever you
name your core Scene and the same with the levelNamePrefix variable –
make sure it matches exactly (including case) with how you name your
level Scenes.
In this chapter, we will be building out a racing game (Figure 14.1) and it will
have almost everything a no-nonsense racer might need – AI opponents, a
respawning system, wrong way detection, and modern video game car physics
using Unity’s Wheel Colliders for suspension and car control.
The focus for this chapter is on the scripts, which means it will not be the
same step by step tutorial format used in Chapter 2 for the RunMan example.
There may be some work on the Unity side that will not be covered. You are
expected to open up the example project and use this chapter as a guide to find
out how it all works, but there will be full script breakdowns and enough infor-
mation for you to be able to make your own game – this is just a heads up that in
this chapter we focus on the programming a lot more than steps on how to use
Unity.
The overall structure of our racing game is shown in Figure 14.2. At its
core is a Game Manager to deal with overall game states, which will be talking
to a Global Race Controller script. The Global Race Controller acts to wrangle
all the player information into coherent racing info such as current lap, who
has finished the race and so on. The structure for vehicles (Figure 14.3) uses a
Race Player Controller Component to provide information to, and communi-
cate with, the Global Race Controller. The scripts used to calculate and monitor
race information (current lap, race position and so on) work independently,
meaning it does not matter what type of vehicle or avatar the player is – it could
be a vehicle, a humanoid running, a space ship, or a rolling ball – as long as
Race Player Controller is provided with what it needs, the racing specific logic
will work.
This game may be found in the example games project. The files for this
game can be found in the Assets/Games/Racing Game folder. If you want to play
219
Figure 14.1 The Racing Game example.
GLOBAL RACE
MANAGER
RACE RACE
CONTROLLER CONTROLLER
(IN THIS CASE, (IN THIS CASE,
PLAYER PLAYER
CONTROLLER CONTROLLER
DERIVES FROM DERIVES FROM
THIS CLASS) THIS CLASS)
PLAYER PLAYER
CONTROLLER CONTROLLER
MOVEMENT MOVEMENT
INPUT CONTROLLER INPUT CONTROLLER
(VEHICLE, USERDATA (VEHICLE, USERDATA
CONTROLLER CONTROLLER
HUMANOID, SHIP HUMANOID, SHIP
ETC.) ETC.)
PHYSICS PHYSICS
(TRANSFORM / (TRANSFORM /
RIGIDBODY/ RIGIDBODY/
COLLIDER ETC.) COLLIDER ETC.)
A.I. PLAYERS
USER CONTROLLED PLAYER
(USES EXACTLY THE SAME COMPONENTS)
the game, the controls are the arrows – up and down to accelerate or brake, with
left and right to turn left or right respectively.
The game works over six Scenes: a main menu, a level select, a Scene loader,
a core Scene containing the Game Manager and GameObjects shared across lev-
els and then two further track Scenes.
PLAYER
CONTROLLER
BASE USER
AI CONTROLLER
MANAGER
MOVEMENT
CONTROLLER
INPUT
(VEHICLE, USERDATA
CONTROLLER
HUMANOID, SHIP
ETC.)
PHYSICS
(TRANSFORM /
RIGIDBODY/
COLLIDER ETC.)
Figure 14.3 The player structure for the racing game example.
14.1 Ingredients
The racing game example uses these ingredients:
2. Level select – The level select relies on a Scene loader using LevelVars.cs.
▪ Fake Friction – a helper script to increase friction and make the vehi-
cle easier to drive
7. User Interface – The user interface for in game will use a basic script to
display the current lap and to show or hide other GameObjects and/or
Canvas as needed.
8. Sound Controller – The BaseSoundController class from Chapter 9,
Section 9.2
using GPC;
public class RaceMainMenu : MenuWithProfiles
{
public override void LoadGameScene()
{
AddPlayers();
base.LoadGameScene();
}
void AddPlayers()
{
BaseUserManager _baseUserManager =
GetComponent<BaseUserManager>();
_baseUserManager.ResetUsers();
using UnityEngine;
using UnityEngine.SceneManagement;
using GPC;
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using GPC;
[Header("Game Specific")]
public string mainMenuSceneName = "mainMenu";
public int totalLaps = 3;
public Transform _playerParent;
public IsoCamera _cameraScript;
public GameObject _playerGO1;
public GameObject[] _playerPrefabList;
public RaceUIManager _uiManager;
public BaseSoundManager _soundController;
public WaypointsController _WaypointController;
public bool didInit;
// user / player data
public BaseUserManager _baseUserManager;
public List<UserData> _playerInfo;
We will skip past the variable declarations, as they should become clear as we
make our way through the script. Instead, jump ahead to:
public RaceGameManager()
{
instance = this;
}
Above, technically yes that is a variable declaration, so we did not skip them
all. The variable instance is used to create a simple Singleton pattern. It is a static
variable that will refer to this instance of the script. That is, the one instance in
our Scene. By making this variable static, it is accessible from anywhere. Since
it is a reference to an instance of this class, we can call functions on it like
RaceGameManager.instance.SomeFunction().
The RaceGameManager() function above is known as a Constructor. It will
be called when this class is instanced automatically, making it the ideal place to
populate our static instance variable with a reference to ‘this’ instance.
Next, a call to Init() is made:
void Start ()
{
Init();
}
void Init()
{
// tell race manager to prepare for the race
GlobalRaceManager.instance.InitNewRace( totalLaps );
This game uses a global race manager script. As discussed at the beginning of
this chapter, the race manager tracks state for the race overall. The game control-
ler needs to tell the race manager that the race is starting, and a new race should
be initialized. As GlobalRaceManager follows the Singleton pattern again, we can
call functions on it using GlobalRaceManager.instance. GlobalRaceManager will
also need to know the total laps for the race so that it can tell when players finish
the race, so we pass that into its InitNewRace() function.
The code above first calls out to SetupPlayers() to get our player prefabs into
the Scene, then sets the lap counter to 1 through the UpdateLapCounter()
function.
The 3,2,1 count in is about to begin. It will take 4 seconds to go through it, so
the Game Manager uses Unity’s Invoke function to schedule a call to StartRace
in 4 seconds’ time.
There is no need for the user interface to update every frame or every step. It
would be unnecessary load on the CPU to update positions like that, so instead
the Game Manager schedules a repeating call to the UI updating function
UpdatePositions() every half second. The InvokeRepeating function takes three
parameters; the name of the function to call, the time from the call to the first
function call and the last one is the time between each function call after that and
ongoing until either this script instance is destroyed or CancelInvoke is called to
stop the continuing calls.
All three of the 3,2,1 count in numbers should be hidden at the start of the
game (otherwise we’ll just have a big blob of all three numbers on the screen at
the same time!). A call to the game controller’s HideCount() function takes care
of that.
The code to make the 3,2,1 count in happen is made simple by Unity’s Invoke
function. The functions our Game Manager uses to display each number will
hide each previously displayed number, so all that needs to be done is some stag-
gered calling to each one followed by a final call to HideCount() to hide the final
number at the end of the count in sequence.
When the race is over, a message tells the user what their final race position
was. At the start of the race, this should be hidden. We call HideFinalMessage()
on the _uiManager (an instance of RaceUIManager in the Scene) to hide it.
didInit=true;
}
The Init() function is done, so didInit gets set to true so that we can easily
find out if the Game Manager has initialized.
Next in the class, the SetupPlayers() function:
void SetupPlayers()
{
// if we can't find a ref to user manager, try to
find one instead..
if (_baseUserManager == null)
_baseUserManager =
GetComponent<BaseUserManager>();
The BaseUserManager class holds information about our players. Before the
game started, on the main menu, one or more players were added through
BaseUserManager. Not physical players with vehicles and so on – just representa-
tions of them in data. Placeholders, if you like. What happens in SetupPlayers() is
we go through the List of players in _baseUserManager and spawn vehicles for
each one. Right now, the infrastructure for more than one human player is not in
place – that would need two lap counter displays and a split screen camera system
along with some code to set those up when the game starts – but this system
would simplify the process of adding more players. The player List in
BaseUserManager is made up of instances of the UserData class. UserData con-
tains a type integer that could easily represent human players or AI players but
for the purposes of this example game we do not go so far and we only allow one
human player vs. AI.
In the code above, we grabbed a reference to BaseUserManager using Unity’s
built in function GetComponent(). _baseUserManager contains the reference we
need to talk to it further in this function.
// get player list from user manager
_playerInfo = _baseUserManager.GetPlayerList();
if ( _ playerInfo.Count < 1)
{
Debug.Log("USING DEBUG PLAYERS..");
The code above is only there to make testing your racing game easier. It looks
at the length of the _playerInfo List to see if there is anything in it. As an example,
if you were running the game from the main menu our main menu code adds
players to BaseUserManager before the game starts and so those players that were
added there will appear in our _playerInfo List since we got it from a static List
inside BaseUserManager. If you are running the game inside Unity and you just
want to test out the game without going out to the menu and back in again, the
List of players will be empty. So, we check to see if the List is empty and populate
it with some default user data if we need to. This way, you can play the game from
the core Scene and it will spawn players that otherwise would only appear if you
had run the menu Scene first.
One way or another, we are now at a stage where there is data in our player
List. The game does not know how many players there will be in the game yet and
we get this number by looking at the count value of _playerInfo – giving us the
length of the List.
GetStartPoints();
GetWaypointsController();
The level Scenes need to contain at least enough start points for the num-
ber of players in a race. A start point is just an empty GameObject placed in a
safe position for one of the racers to spawn into when the race begins. In the
example game, these points are near to the start line like a regular racing
game.
GetWaypointsController() is called to find a reference to a waypoints con-
troller. We will need to pass the waypoints controller into each player for the AI
to be able to find their way around the track as well as for position calculation.
The way we calculate race position relies on being able to compare how far around
the track the different racers are, and for this we use the waypoints.
Next, above, we create two new Lists. One to store all of the player Transforms
in so that we can get direct access to their positions later on, and another List to
store references to our racer’s RacePlayerController Components. Having refer-
ences to the RacePlayerControllers gives us access to their race positions and lap
information.
This part of the code is going to spawn all the vehicles for the race. Most of
what happens in this function takes place inside the loop above, which goes from
0 to however many players we need to spawn – representing by the variable
numberOfRacers.
A function named Spawn() is used to deal with the instantiation of our pre-
fabs, rather than calling out to Instantiate here. By splitting out spawning into its
own function, we can easily replace out the spawning system with something
more performance friendly should the game grow to a stage where it might need
it or if the target platform is limited by performance. If you need to improve per-
formance, I would recommend replacing the Instantiate call inside with Spawn()
function with some kind of pool management, whereby objects are recycled
rather than destroyed and replaced with new ones.
Our Spawn() function takes a prefab (Transform) followed by a Vector3
position and a Quaternion rotation value to apply immediately after spawn.
As we only ever use one prefab in this game, for both AI and users, the
_playerPrefabList array only needs to point to one prefab. This will be the first
item in the array (note that this array is set up via the Inspector on this Component
in the editor) and we get the prefab with _playerPrefabList[0] since Lists always
start at position 0. We pass in the prefab from _playerPrefabList followed by a
startpoint position and rotation from the _startPoints List that was populated by
a call to GetStartPoints() earlier in the code.
RacePlayerController _ raceController =
_ newPlayer.GetComponent<RacePlayerController>();
_ newPlayer.parent = _ playerParent;
The line above is important. We need to tell Unity to parent these new
GameObjects to something, otherwise they will be parented to nothing and dis-
appear into the Unity void! I can only assume this is a bug that occurs when you
load and merge Scenes together, so to work around it we just have to make sure
anything made at Start() has a parent set.
In order to tie the players in the Scene to the players in our player List
together, we use an id system. Above, the RacePlayerController Component
attached to the player we just spawned will have its id value set to the same id
held by the player from BaseUserManager’s player List that we are creating the
vehicle for.
As this is a single player game, we need a player to observe during the game
(for the camera to follow) and to use to populate the user interface’s lap counter
with. In this function, we assume that the first user-controller player we find will
be the one to follow. Above, the If statement checks the type of our player to see
if it is user controlled (its type will be 0) and then sees that we do not already have
a player in the _playerGO1 variable – in effect, making sure we follow the first
player found.
// focus on the first vehicle
_playerGO1 = _newPlayer.gameObject;
_playerGO1.AddComponent<AudioListener>();
The _playerGO1 variable has two uses here. One is to hold which GameObject
we will be following and the other is to add an AudioListener to. By default, Unity
automatically adds an AudioListener to the Main Camera GameObject in a Scene
but here we put the AudioListener on our vehicle instead.
Note: If you have more than one listener in a Scene, Unity will produce a
warning in the console repeatedly.
focusPlayerID = i;
}
if (_baseUserManager.GetType(_playerInfo[i].id) == 0) // <-
- get the type of this player from baseusermanager
{
_newPlayer.GetComponent<RaceInputController>().SetInputType
( RaceInputController.InputTypes.player1 );
There are just two more properties to affect the player above, when it is user
controlled. The first is to set AIControlled on the RacePlayerController class to
false so that the AI does not try to drive for us. We want the player to have full
control at this point.
A Component named MaterialSetter is used to set the material of the car
body. The MaterialSetter Component is attached to the Vehicle_Player prefab
and it holds an array of materials that can be applied to the vehicle. Using
SetMaterial, we pass in the array index of the material to set and it will take care
of the rest. In this game, we have two materials – one for the user controlled
player and the other for AI so that it will be easy for the user to determine which
car they are controlling.
}
else if (_baseUserManager.GetType(_playerInfo[i].id) == 2)
{
If the player type is not user controlled (0), in the line above we check to see
if it is AI (2).
_ newPlayer.GetComponent<RaceInputController>().SetInputType
( RaceInputController.InputTypes.noInput );
The code above is very similar to how we set up the user controlled player,
except instead of setting AIControlled to false we set it to true. Below that,
_tempAI (a reference to this player’s BaseAIController Component) gets its
AIControlled variable set to true, too. Finally, we set the material to a material
from the position 1 in MaterialSetter’s array.
At this stage, I understand that having to set AIControlled on two separate
Components is not ideal. The only alternative would be to have either one of the
BaseAIController or the RacePlayerController Components look at the other one
and copy its AIControlled value across. As the goal of this book is to make stand-
alone scripts wherever we can, making one script reference another goes against
this principle. You may not always decide to pair up the BaseAIController with
RacePlayerController, perhaps replacing BaseAIController entirely with different
AI code, so having to reference two scripts AIControlled variables here is a trad-
eoff that ultimately gives us more freedom later on.
// look at the main camera and see if it has an audio
listener attached
AudioListener tempListener =
Camera.main.GetComponent<AudioListener>();
void StartRace()
{
// play start race sound
_soundController.PlaySoundByIndex(1);
StartRace() is called when the countdown finishes, and we are ready to actu-
ally get the race going.
Above, _soundController plays a nice sound effect to tell the player to go.
The variable _soundController points to a BaseSoundController script attached
to the GameManager in our core Scene, which is used to play incidental sounds.
When we call it with PlaySoundByIndex(), the BaseSoundController script will
play a sound at the world position 0,0,0 and with a very wide drop-off area so that
our Audio Listener will ‘hear’ it.
We unlock all of the players next, passing false as a parameter into the func-
tion LockPlayers(). After that, we need to tell the GlobalRaceManager class that we
are ready to race too – so that it will start tracking positions and so on – so there’s
a quick call to GlobalRaceManager’s instance (reminder: GlobalRaceManager is a
Singleton!) to StartRace().
Next up is the LockPlayers() function:
Called a few times by this class already, the LockPlayers() function tells
players whether or not they should be allowed to move. As discussed in Chapter 6
(Section 6.2), locking the vehicles will constrain them along their x and z axis but
not on the y axis. This is so that the vehicle will still be affected by gravity. If the
vehicle’s y axis were locked too, it would sit exactly at its start position in the game
world, possibly floating, and not in its natural rested suspension state.
The code to lock our vehicles in place is in the RacePlayerController class.
What we do here is to iterate through all the RacePlayerController instances held
in the List _playerList[] – which was populated back in the SetupPlayers()
function – and we call SetLock() on each one. SetLock() takes a Boolean as its
parameter, stating whether or not to lock the vehicle.
Above, UpdatePositions() gets the lap currently being run by the player we are
focusing on, whose ID is in focusPlayerID. This was another variable set in the
SetupPlayers() function when we first added all of the vehicles to the Scene. The
GlobalRaceManager class takes care of tracking positions and laps, and to find out
how many laps the player has completed we can call GlobalRaceManager.instance.
GetLapsDone(). Passing the player ID as a parameter means that it will return the
current lap count of that player as an integer. We store that into the variable theLap,
which is used in the next line above to pass into the UpdateLapCounter() function
as a parameter. This will update the user interface lap display.
To update the user interface’s lap counter, we have the function
UpdateLapCounter():
void UpdateLapCounter(int theLap)
{
// if we've finished all the laps we need to finish,
let's cap the number so that we can
// have the AI cars continue going around the track
without any negative implications
if (theLap > totalLaps)
theLap = totalLaps;
void ShowCount1()
{
_uiManager.ShowCount(1);
_soundController.PlaySoundByIndex(0);
}
void ShowCount2()
{
_uiManager.ShowCount(2);
_soundController.PlaySoundByIndex(0);
}
void HideCount()
{
_uiManager.ShowCount(0);
}
The next part of the Race Game Manager class is a function to find the player
starting points in the level Scene:
void GetStartPoints()
{
_startPoints = new List<Transform>();
As you may recall, this game is split into two Scenes per level – the core Scene
containing the game logic and the level Scene containing level specifics like the
environment itself and start positions. As we load levels in at runtime, there is no
way of setting start positions via the Inspector in the editor (as they will change
from level to level) so an automated system is necessary.
Above, GetStartPoints() kicks off by making a new List in the variable _
startPoints. This is a variable used by the SetupPlayers() function as it creates the
players, adds them to the Scene and requires a start position for each one.
GameObject _ startParent =
GameObject.Find("StartPoints");
The starting points for our players are parented to an object named
StartPoints. It does not matter so much what the start point GameObjects them-
selves are called, just as long as they are parented to a GameObject named
StartPoints. The reason being that we use GameObject.Find() to look for a
GameObject named StartPoints in the Scene. Once we have that, we iterate
through all of its child objects like this:
foreach (Transform sp in _ startParent.transform)
{
_startPoints.Add(sp);
}
}
The foreach loop above will go through all Transforms found under our
StartPoints GameObject – but it is important to note that this method is not
recursive and if any of the StartPoints are at a level below this (i.e. children of
children of StartPoints) they will be left out.
As the loop goes through each child object, we use _startPoints.Add() to add
them to our List of start points. At the end of this function, _startPoints should
be a List populated with all the starting points from any open Scene.
void GetWaypointsController()
{
_WaypointController =
FindObjectOfType<WaypointsController>();
}
if (isWrongWay)
{
_uiManager.ShowWrongWay();
}
else
{
_uiManager.HideWrongWay();
}
oldIsWrongWay = isWrongWay;
}
Above, after dealing with the display state we set oldIsWrongWay to isWrong-
Way so we can keep track of state changes. At the bottom of the RaceGameManager
class, the Spawn() function:
We talked a bit earlier about having our Spawn() function separated like this
so that it would be easy to switch out the call to Instantiate() with something
more performance-friendly such as an object pool manager. All that happens in
this function is that we add a GameObject to the Scene based on the parameters
passed into it and then return a reference to the new GameObject.
namespace GPC
{
public class GlobalRaceManager : MonoBehaviour
{
public int totalLaps;
public bool raceAllDone;
public int racersFinished;
private int currentID;
private Hashtable raceControllers;
private Hashtable racePositions;
private Hashtable raceLaps;
private Hashtable raceFinished;
private int numberOfRacers;
private int myPos;
private bool isAhead;
private RaceController _tempRC;
Its Awake() function ensures that only one instance of the script exists at any
time, by checking to see if its static variable named instance is null. If instance is
null, we know that an instance of this script has not yet been made. If instance is
not null and already has a reference to an existing Component, we destroy it here.
instance = this;
Above, several hashtables are used to store and access the data this script
needs to use. Hashtables have a key and a value, which makes them ideal for stor-
ing player related info or objects. Instead of using an index number, as you do
with an array, you can provide a key and get access to its associated value. In this
case, each player’s unique ID is used to store data about the state of each player,
and we can use a player ID to quickly get data back out again.
In this class, we use hashtables for:
Race finished – When a player crosses the finish line after completing the set
number of laps (held in the variable totalLaps), an entry into the raceFinished
Hashtable will be set to true. Otherwise, it will be false.
GetUniqueID() does more than provide a unique ID for each player of the
game. When the RaceController script (RaceController.cs – discussed earlier in
this chapter) calls in, it passes in a reference to itself (its instance) as a parameter.
This function then adds a reference to the new player’s RaceController script
Component to the raceControllers Hashtable, using the player’s unique ID as a key.
currentID++;
At the start of the race, when the RaceController is first registered with
GlobalRaceManager, the raceLaps Hashtable adds an entry for this player start-
ing at 1 to indicate that this is the first lap.
As the race is just beginning, not ending, the raceFinished state of this player
is defaulted to false above.
May as well keep a count of how many players there are here, since all new
players will have to be fed through this function. It will save having to count
anything later to find out how many players there are, as the integer num-
berOfRacers will hold the count for us.
The entry for anID in the raceLaps Hashtable is incremented by adding one
to its current value. Note that we do not shortcut with something like
raceLaps[anID]++ because this refers to a hashtable, at this stage, not a number
to increment. Instead, the first part of the statement says we want to set the entry
in the hashtable, and the second part of the statement gets a value out of the
hashtable and increases it.
Above, we check to see if this player has completed enough laps (by check-
ing its entry in raceLaps against the variable totalLaps) and not yet finished the
race (its entry in raceFinished should be false right now) then, if these condi-
tions are met, this player’s entry in the raceFinished Hashtable is updated
to true.
Closing up the CompletedLap() function now, we see how many racers have
done their laps by comparing racersFinished to raceFinished.Count. If all the
racers have finished, we set raceAllDone to true. This will be used by our player
controllers as a way for them to know when the entire race is finished.
public void ResetLapCount(int anID)
{
// if there's ever a need to restart the race and
reset laps for this player, we reset its entry
// in the raceLaps hashtable here
raceLaps[anID]=0;
}
If there is ever a need to restart the race for a specific player, ResetLapCount()
takes a player ID number and resets its entry in the raceLaps Hashtable to zero.
Next, the GetPosition() function:
public int GetPosition(RaceController focusPlayerScript)
{
myPos is an integer which starts out set to the maximum value – last place in
the race – then it will be decremented whenever we find out that the current
player is in front of another.
A loop (b) goes from 0 to the value of numberOfRacers. The value of b will be
used to retrieve RaceController references from the raceController Hashtable. As
we step through the loop, we grab one other player and compare properties to the
player passed in as a parameter (such as current waypoint, current lap and so on)
to see which one is in front. At the start of the position calculation, we assume that
the player passed in will be behind the player being checked – isAhead is false. If
we find otherwise, isAhead will be set to true and we will decrement this player’s
race position at the end of the loop – bringing one closer to 1st place.
To keep the function safe, _tempRC is null checked just in case one of the
players has somehow been removed from the game. If it is null, the continue
command will move the loop on to the next pass without continuing to execute
the code in subsequent lines.
if ( focusPlayerScript.GetID() != _ tempRC )
{ // <-- make sure we're not trying to compare same objects!
There are several conditions we test against to find out whether the player
within focusPlayerScript is ahead of the one that the loop is currently looking at.
Above, the first condition is whether the current lap of the focused player is
higher than the lap of the race controller in _tempRC – if it is, we know right
away that focusPlayerScript is ahead of _tempRC and we can set isAhead to true.
The above condition checks to see whether the current lap of the focused
player is the same as the lap of the race controller in _tempRC. If they are both on
the same lap, we look to the waypoints to see if the focusPlayerScript waypoint
index number is greater than that of _tempRC. If it is, we know that focusPlayer-
Script is ahead of _tempRC because its waypoint is further along the track: in
which case, isAhead gets set to true. There is also a check to make sure that the
player in _tempRC has not yet finished its current lap.
The fourth condition checks that both players are on the same lap and the
same waypoint, but this time looks to see whether the focus player has registered
a lap done before _tempRC has. If focusPlayerScript.IsLapDone() is true and _
tempRC.IsLapDone() is false, we know the passed in player has completed its lap
before the one we are checking against, and isAhead is set to true.
if (focusPlayerScript.GetCurrentLap() ==
_ tempRC.GetCurrentLap() && focusPlayerScript.GetCurrentWaypointNum() ==
_ tempRC.GetCurrentWaypointNum() && (focusPlayerScript.IsLapDone()
== true && _ tempRC.IsLapDone() == false))
isAhead = true;
Above, a fifth condition does a similar check to the last one, only this time
omitting any waypoint distance checking. Here, the code focuses on whether the
two players are on the same lap and, when they are on the same lap, whether the
focusPlayerScript has finished its lap or not. When focusPlayerScript.IsLapDone()
is true and tempRT.IsLapDone() is false, we know that the focused player is ahead
and can go ahead and set isAhead to true.
if (focusPlayerScript.GetCurrentLap() ==
_ tempRC.GetCurrentLap() && (focusPlayerScript.IsLapDone() ==
true && ! _ tempRC.IsLapDone()))
isAhead = true;
The final condition (phew!) checks to see if both players are on the same lap
and that our passed in player has finished a lap before the one in _tempRC.
if ( isAhead )
{
myPos--;
}
}
}
By the time this loop has iterated all the way through the players, myPos
shows exactly which race position the focused player is in. The function ends by
using myPos as a return value.
We skip through down past the variable declarations to bring us to the first
function, Awake():
public void Awake()
{
raceID =
GlobalRaceManager.instance.GetUniqueID(this);
}
Above, we start this class in the Awake() function, where the first task is to
get a unique ID for this player from the GlobalRaceManager. The ID number will
be returned as an integer that we store in the variable raceID.
There may be occasions where other scripts need to get this player’s unique
ID number, which is why the GetID() function, above, returns the value of myID.
When the player has passed by all the waypoints around the track, the vari-
able isLapDone is set to true. This value can then be used when the player hits the
trigger on the start/finish line to see whether the lap should be increased.
Increasing laps in this way (rather than just incrementing the current lap counter
at the start/finish line) ensures that the player has driven all the way around the
track and crosses the finish line.
Above, other scripts may need to know if this player has finished racing. When
it is called, the IsFinished() function will return our isFinished state Boolean.
When other scripts need to find out what the player’s current lap is, the
GetCurrentLap() function gets the latest information to be calculated by the
GlobalRaceManager instance. It passes in this player’s ID as a parameter (so that
the global race manager knows who is asking for their lap count) and adds one
to the return value because the return value from GlobalRaceManager.Instance.
GetLapsDone() will be how many laps have been completed rather than the value
we want to return, which is the current lap number.
The game could be reset by another script, calling for the lap counter for this
player to be reset too. Remember that lap counting is handled entirely by the
GlobalRaceManager class, so we need to ask GlobalRaceManager to reset our lap
counter as we do not store the current lap here. GlobalRaceManager provides a
ResetLapCount() function for this, with its parameter being the player’s unique
ID we kept in the variable myID.
This class keeps a track of the where the player is on the track by monitoring
its progress along a set of waypoints. In the example game here, those waypoints
are the same ones used to drive the AI players around. The GlobalRaceManager
class will need to know which is this player’s current waypoint index number
to compare it to other players and calculate race positions. Above,
GetCurrentWaypointNum() returns the player’s currentWaypointNum integer.
As this script uses a waypoint controller to track the player’s progress around
the track, its _waypointsController variable (of type WaypointsController) needs
to be set by another script. In the case of this game, it is set by the RaceGameManager
when players are first spawned into the game Scene.
if (_waypointsController == null)
return;
Above, before any waypoint checking can happen, the class must have run
its initialization Init() function and set doneInit to true. If doneInit is false, we
call Init() above. Next, the variable _waypointsController is null checked, just in
case this function is called before a reference to the WaypointsController instance
has not yet been set.
if(totalWaypoints==0)
{
// grab total waypoints
totalWaypoints =
_waypointsController.GetTotal();
return;
}
Waypoint checking works by looking at the distance between the player and
the waypoint and moving on to the next waypoint when that distance gets too
low. For this type of waypoint following, the y axis may be discounted. It will
serve only to make the distance checking more complicated since it would be on
currentWaypointDist = Vector3.Distance(nodePosition,
myPosition);
Above, the built in Unity function Vector3.Distance() takes two Vector3 positions
and returns the distance between them. We put our distance into currentWaypointDist.
When the distance in currentWaypointDist drops below waypointDistance, it incre-
ments currentWaypointNum; in effect, advancing to the next waypoint.
// now check to see if we have been all the
way around the track and need to start again
if (currentWaypointNum >= totalWaypoints)
{
// reset our current waypoint to the
first
currentWaypointNum = 0;
isLapDone = true;
}
Vector3 relativeTarget =
myTransform.InverseTransformPoint
(_currentWaypointTransform.position);
To ensure the player is heading in the right direction around the track, the
CheckWrongWay() function uses the waypoints system to look ahead to the next
waypoint and check the angle of the vehicle vs. the ideal angle to reach the way-
point. When the angle goes over a certain threshold (here, it is set to 90 degrees)
then the wrong way message is displayed, and a timer will count. When the timer
reaches a certain amount, we automatically respawn the player back on to the
track.
First, the function checks that _currentWaypointTransform has something
in it. If this reference was at null, it would break the code, so we drop out here, if
we need to.
The script uses the waypoint from _currentWaypointTransform to check
that the vehicle is traveling in the right direction. The first step in doing that is to
calculate a vector, relative to the vehicle, with Transform.InverseTransformPoint().
Essentially, what happens here is that the world vector provided by
_currentWaypointTransform.position is converted to the local space belonging
to _TR (the vehicle), providing relativeTarget with a vector.
if(targetAngle<-90 || targetAngle>90){
goingWrongWay=true;
} else {
goingWrongWay=false;
If a vehicle is going the wrong way for a set amount of time, it will be auto-
matically respawned by the game code. The variable timeWrongWayStarted is, in
effect, a timestamp, which is used to hold the time at which the wrong direction
was first detected. When the code no longer detects the vehicle going in the
wrong direction, timeWrongWayStarted is set to –1 which has the effect of can-
celling a respawn and restarting the timer.
Next, we set timeWrongWayStarted when the wrong way first happens:
if(oldWrongWay!=goingWrongWay)
{
// store the current time
timeWrongWayStarted= Time.time;
}
oldWrongWay=goingWrongWay;
}
It’s the end of the CheckWrongWay() function and oldWrongWay may now
be set to the current value of goingWrongWay so that we can track it for changes
the next time this function runs.
As discussed earlier in this section, the current lap will not be increased as
soon as the player reaches the end of its waypoints. The player also needs to cross
the start/finish line before the lap counter will be updated and a simple trigger
Collider is used on an invisible box crossing the track. When the player hits the
trigger, the OnTriggerEnter function is automatically called by the Unity engine:
As the lap is incremented, the end of this lap has been reached so we need to
use the isLapDone variable to track the next lap. It is reset to false above, then we
call GlobalRaceManager to let it know that a lap completed. We make a call out
to the GlobalRaceManager’s CompletedLap() function, passing in this vehicle’s
unique ID to identify itself.
ResetLapCounter();
_myController = GetComponent<BaseWheeledVehicle>();
didInit = true;
}
When timedif (how long the player has been facing the wrong way) is greater
that stuckResetTime, above, we call the Respawn() function.
}
else if (!AIControlled)
{
Above, we deal with what happens when goingWrongWay is not set to true.
If this is not an AI controlled vehicle, RaceGameManager gets another call to its
UpdateWrongWay() function to tell it that we are no longer going the wrong way.
// stuck timer
if(_myController.mySpeed<0.1f &&
_myController.canControl)
{
stuckTimer += Time.deltaTime;
When stuckTimer goes over the value of stuckResetTime, we know that the
vehicle has been extremely slow or stopped for that amount of time and now it is
time to call Respawn() in the hopes of getting the player moving again. Above, we
call Respawn() and reset the stuckTimer to zero.
} else
{
stuckTimer = 0;
}
On the other hand, if the vehicle speed is above our minimum 0.1f amount,
we just set stuckTimer to 0 so that the code above does not respawn the vehicle.
UpdateWaypoints();
If the race finishes, above we tell the RaceGameManager about it. I chose to
put this call here (rather than in the GlobalRaceManager class) because
GlobalRaceManager is intended to be a generic script that can be used anywhere
without ties to any other non-Unity class than RaceController. This means that
RaceComplete() will likely be called more than once, but the way
} else
{
// we only update the position during the
game. once the race is done, we stop calculating it
UpdateRacePosition();
}
}
Above, since we now know that the race is not finished (this is the else that
comes after checking if GlobalRaceManager.raceAllDone is true) we can do
whatever needs to be done during the race. Above, this means we call out to
UpdateRacePosition() to keep the race position indicator up to date. Which leads
us to the UpdateRacePosition() function, next:
From the point of view of the RacePlayerController class, there is not much
to do to update the race position. We just get the return value from an instance
of GlobalRaceManager.GetPosition() and store it in myRacePosition. The
RaceGameManager class deals with updating the user interface to show on screen.
void Respawn()
{
if (blockRespawn)
return;
// reset our velocities so that we don't reposition
a spinning vehicle
_myController._RB.velocity = Vector3.zero;
_myController._RB.angularVelocity = Vector3.zero;
As it is quite common for the waypoints to be either off the ground or too
close to the ground for our respawn to work correctly, the code above deals with
ray casting down to find the ground Collider. We project a ray from tempVEC (the
waypoint’s position) add 10 units upward. The ray goes straight down at -Vector3.
up and we put raycast hit information into the locally scoped variable, hit.
If the raycast was successful, it will return true, and we run the code between
the graphics. What happens there is that tempVEC has its y position set to the
ground position we just found through the raycast (hit.point) add 2.5 units. This
number (2.5f) may be adjusted to respawn the vehicle closer or further from the
ground as required. By the way, tempVEC will be used as the final position for the
vehicle during repositioning:
_ myController. _ TR.rotation = _ tempTR.rotation;
_myController._TR.position = tempVEC;
blockRespawn = true;
Invoke("ResetRespawnBlock", 5f);
}
Above, Respawn() ends by setting our vehicles rotation to that of the way-
point Transform in _tempTR. Then on the next line, its position is set to the
Vector3 in tempVEC. We then set blockRespawn to true and in the last line of
the function, a call to ResetRespawnBlock() is scheduled for 5 seconds’ time.
This means that the respawn function will be disabled for 5 seconds after a
respawn.
void ResetRespawnBlock()
{
blockRespawn = false;
}
When a player has completed all of the laps in the race, GlobalRaceManager
calls the PlayerFinishedRace() function in the RaceController class. Above, we
override RaceController’s version of PlayerFinishedRace() to do something spe-
cific to this game. When our player has finished all of their race laps, we have the
AI take control of the vehicle and drive it around until the entire race finishes
when all players have completed their laps, too.
if (!AIControlled)
{
// make sure we give control over to AI if
it's not already
AIControlled = true;
We begin by making sure that the player is not already being controlled by
AI with a check on the AIControlled Boolean variable. If we are not yet AI, the
code inside the curly brackets for this condition will change that.
AIControlled gets set to true, so that we now know our player is AI con-
trolled. Next, it is a rather long line that uses Unity’s GetComponent() function
to find our RaceInputController Component, then it calls SetInputType on it
and sets the input type for the player to be RaceInputController.InputTypes.
noInput, to disable user input. Next, GetComponent goes off to find an
AISteeringController Component (or, remember this could be a class that
derives from AISteeringController and it would still find it) and then tells the
steering controller that we are now AI controlled by setting its AIControlled
variable to true.
To complete the AI takeover, we tell _AIController that it can control this
player by setting its canControl variable to true.
At the end of the race, if you ever wanted to do something different like have
the cars keep going, the PlayerFinishedRace() function would be the place to
start.
Above, SetLock is used to tell our vehicle that it cannot be controlled. First,
we make sure that Init() has completed (as this will be called when the player first
spawns to hold them in place and it may be that this script has not yet initialized)
but if has not, we call to Init() from here so that _myController will be populated
and we can access it in the next line.
_myController.canControl is a Boolean we can set to decide whether the
vehicle will be allowed to receive input and act on it. Above, this is not an ideal
situation. We actually set canControl to the opposite of the state being passed in
because of the fact that the function is named SetLock (i.e. set whether a lock
should be in place) whereas canControl is whether the vehicle can be controlled.
They are opposites. SetLock() is named so for the sake of continuity elsewhere in
the framework, but it is always better to avoid any naming contradiction like this,
if you can.
public override void OnTriggerEnter(Collider other)
{
base.OnTriggerEnter(other);
if (IsInLayerMask(other.gameObject.layer,
respawnerLayerMask))
{
Respawn();
}
}
}
14.4.5 Audio
For the vehicle audio in this game, we use a set of GameObjects with AudioSource
Components attached to them. These GameObjects are child objects of the vehicle,
moving around with the vehicle wherever it goes and activated to play by other
scripts. For incidental sounds, a BaseSoundController Component holds an array of
sounds such as bleeps during the countdown that can be played from other scripts.
14.4.5.1 Vehicle Audio
Vehicle audio is controlled by the following classes:
14.4.5.2 Incidental Sounds
Incidental sounds are controlled by a single BaseSoundController Component
attached to the GameManager GameObject in the core Scene. You can find a full
breakdown of the BaseSoundController in Chapter 9, Section 9.2.
In-Game UI – The user interface contains a Text object to show the current lap
number, a Text object to show the wrong way message and a third Text object
to show when the race is over.
Intro_UI – The intro Canvas contains three Text objects for the three numbers
we show during the countdown at the start of the race.
All the game’s user interface is managed from a single class named
RaceUIManager. It is added as a script Component to the GameManager
GameObject in the Scene.
The RaceUIManager class derives from MonoBehaviour so that it can be
added to a GameObject as a Component:
using UnityEngine;
using UnityEngine.UI;
Above, the SetLaps() function takes the current lap, followed by the total race
laps as its parameters. The Text Component references in _lapText has its text set
to the word Lap and then we do something very cool with the lap number. Instead
of simply converting it into a string with the ToString() function, we add "D2"
inside the brackets. What this does is tell ToString() how to format the number
when it converts it. D2 will format the lap string to have two numbers. For exam-
ple; if the current lap is less than 10, it will be displayed with a zero in front of it.
We add the word "of" to the string next, then at the end of the line we take
totalLaps and convert it to a string in the same way we did the current lap. That is,
we use D2 to format the conversion. Note that there is no technical reason to do
this – I think it just looks nicer to have the extra number in the user interface.
public void HideFinalMessage()
{
_finalText.SetActive(false);
}
In the late 1970s came a turn-based game called Robots for the Berkeley Software
Distribution (BSD). In Robots, the player must escape from a swarm of enemies
that move in closer toward the player each turn. In the original version of the
game, the player moves around the arena to avoid contact between enemies and
player, which would end the game.
Robots is often referred to as the beginning of the arena shooter – a term used
to describe a top-down shoot ‘em up game with the action locked into a limited
area like an arena. The Blaster game example from this chapter is a basic template
for an arena shooter, which borrows heavily from its predecessors; the action has
a single player moving around an arena fighting off hordes of killer robots.
In this chapter, we look at the scripts used to build an arena shooter
(Figure 15.1) where bots will spawn and the goal is to destroy them all to be able
to progress to the next level. This chapter is less of a tutorial and more of a com-
panion guide to the example project. It is expected that you will open the exam-
ple project and dig around to see how it works, using this chapter as a guide to
what everything does in the game and how it all fits together. This chapter is
mostly focused on the C# scripts, what they do, and how they work.
Additions to the core framework are minimal and you can see the structure
of the game in Figure 15.2.
You can find the Blaster game in the example project folder Assets/Games/
Blaster. Scenes are in the Assets/Games/Blaster/Scenes folder.
263
Figure 15.1 The Blaster Game Example.
AI BOT
CONTROLLER (IN
THIS CASE, ENEMY
BOT BELOW
DERIVES FROM
PLAYER THIS CLASS)
CONTROLLER
PHYSICS
STANDARD SLOT
(TRANSFORM / AI WEAPON
WEAPON USERDATA
RIGIDBODY/ CONTROLLER
CONTROLLER
COLLIDER ETC.)
STANDARD SLOT
WEAPONS WEAPON
CONTROLLER
WEAPON
15.1 Ingredients
The Blaster game uses the following ingredients:
5. User Interface – The user interface for in game will derive from the
CanvasManager script from Chapter 11, Section 11.1, to fade UI ele-
ments in or out.
6. The BlasterPlayer class (the main class for the player) derives from
BasePlayerStatsController, which also uses BaseUserManager to provide
stats for the player for lives and score values.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using GPC;
public Awake()
{
instance = this;
}
As with any other Singleton style classes we produce in this book, above we
declare a variable named instance that gets set to this instance in its Awake()
function.
SetTargetState(Game.State.loaded);
}
When the Start() function runs, automatically called by Unity, the first thing
we need to do is to make sure that the screen starts blacked out so that we can
have a nice fade in. To pull this off, we call out to the ScreenAndAudioFader class
reference in _fader and run its SetFadedOut() function. This will set the state of
the fader to black right away.
The next line is an Invoke statement that will call the StartFadeIn() function
in one second. We delay this to make it work nicely – it appears better to a user if
void StartFadeIn()
{
_fader.FadeIn();
}
void Init()
{
// if we're running this in the editor, LevelVars.
currentLevel will not have been set to 1 (as it should be at the
start)
// so we just do a little check here so that the
game will run OK from anywhere..
if (LevelVars.currentLevel == 0)
{
LevelVars.coreSceneName = "blaster_core";
LevelVars.levelNamePrefix = "blaster_level_";
LevelVars.currentLevel = 1;
}
There is a little set up required before the game can begin. Above, the first
thing we do is make it easier to test the game in the editor by setting up some
default values in LevelVars, if none have already been set up. To find out if
LevelVars have already been set, we look to see if LevelVars.currentLevel is 0. We
want the level numbering to start from 1, so if it sits at 0, we know that the default
value of 1 has not yet been set up.
Normally, these variables would have been filled in by the main menu but if
you are running in the editor it will need to know what values to use when you
get to the end of the level and the game tries to load in the next one.
CreatePlayers();
_cameraManager.SetTarget(_thePlayer);
The next task in Init() is to call CreatePlayers() to add the main player to the
Scene. The current game is only single player, but creating players in this manner
would make it easier to add multiple players in the future.
By the time we reach the next line of code, the players have been created so we
can go ahead and tell the Camera script which player to follow around. A direct
reference to the Camera script can be found in _cameraManager – this was set via
the Inspector on the Component. The CreatePlayers() function set a variable
named _thePlayer to the first player it created, and we can pass this over to the
SetTarget() function of the Camera script for it to follow our user-controlled
player.
At the end of Init(), the time has come again to update the game state. This
time, we set it to Game.State.levelStarting.
List<UserData> _playerInfo =
_baseUserManager.GetPlayerList();
_playerSpawnpoint =
GameObject.Find(spawnPointString).transform;
The next step is to iterate through the player List we grabbed out of
BaseUserManager and spawn a player for each one. Before we can spawn a player,
though, we need to figure out where to place it. For that, the level Scene should
contain at least one spawn point named PlayerSpawnPoint_ with a number after
it. In the example Scene, the spawn point is named PlayerSpawnPoint_1.
Above, we construct a string named spawnPointString to find the spawn
position with. Once we have the string, GameObject.Find is used to populate
_playerSpawnpoint (a Transform type variable) with the Transform of the
GameObject it finds.
_thePlayer = Instantiate(_playerPrefab, _
playerSpawnpoint.position, _playerSpawnpoint.rotation, transform);
Above, the player is instantiated, using the playerPrefab prefab (set in the
Inspector) and the position and rotation of the _playerSpawnpoint Transform.
Notice that we also set the parent of the object to be instantiated here. If you do
not parent a newly instantiated object like this, due to how Unity deals with
initializing our merged core and level Scenes the new GameObject will be
_playerScripts.Add(_aPlayerScript);
if(i==0)
_thePlayerScript = _aPlayerScript;
}
BaseCameraController theCam =
FindObjectOfType<BaseCameraController>();
_theCam.SetTarget(_thePlayerScript.transform);
Above, we update the score, and live user interface displays by calling their
respective functions in the BlasterUIManager class (see Section 12.2.6 for a
breakdown of that script). To get the values we use GetScore() and GetLives()
functions on the _thePlayerScript (BlastPlayer class) object.
We need to tell the Camera script what to follow here, too, and we get to it by
setting up a locally scoped variable theCam of type BaseCameraController and
then use FindObjectsOfType() to get Unity to go off and search for it. With a
reference set up in theCam, we can call BaseCameraController.SetTarget() and
pass in the target of our player’s Transform.
When an enemy is added to the game, the BlasterEnemyBot script will call
the above RegisterEnemy() function. We keep a count of how many enemies
there are, in the variable enemyCounter, so we can tell when they have all been
destroyed and it is time to move on to the next level.
enemyCounter--;
_thePlayerScript.AddScore(150);
UpdateUIScore(_thePlayerScript.GetScore());
if (enemyCounter <= 0)
SetTargetState(Game.State.levelEnding);
}
if ( _ thePlayerScript.GetLives() <= 0)
SetTargetState(Game.State.gameEnding);
else
SetTargetState(Game.State.restartingLevel);}
}
When the player has been destroyed, there are two ways it can go. If they
have any lives left, the level will be restarted. If they have no lives left, the game
needs to end.
Above, we use the GetLives() function to see if there are lives left, then
SetTargetState() sets the target game states to either Game.State.gameEnding or
Game.State.restartingLevel.
public void NextLevel()
{
// increase the current level counter in the
LevelVars static variable, so that the levelloader can use it later
LevelVars.currentLevel++;
When all of the enemies have been destroyed, the LevelEnding() function
calls the function above, NextLevel() to progress the player and initiate loading
the next level.
The first thing it does is to increase the currentLevel value held in our LevelVars
class. This will be used to form the filename of the next level, so we need to be sure
that the value of currentLevel never goes above the number of levels we have made
in Scenes otherwise it will produce an error. We can currentLevel at 4, for this
game. If you use this project as a template for your own games you will need to
adjust this number accordingly, otherwise your game will always end after level 3!
If the currentLevel is 4, the code above makes an Invoke() call to
LoadGameCompleteScene one second from now. In the example game,
LoadGameCompleteScene simply loads the main menu and ends the game but if
void CallFadeOut()
{
_fader.FadeOut();
}
//--------------------------------------------------------------------------------------
——————————————————————————
// GAME STATE FUNCTIONS (CALLED BY THE UPDATETARGETSTATE /
UPDATECURRENTSTATE FUNCTIONS)
The functions below all override virtual functions from the base
BaseGameManager class. They are called by the BaseGameManager class at the
beginning of game states. We looked at the BaseGameManager class in Chapter 3,
Section 3.2.1., so look at that if you need more information on where these are
called exactly.
Above, when the core Scene has loaded, we call the Init() function and invoke
a UnityEvent named OnLoaded. The UnityEvents are there so that you can easily
hook in other scripts to react to game state changes without dependencies. In the
example game, any UnityEvents we do use are used to tell the user interface to
hide or show Canvases such as the get ready message or game over.
One by one, each spawner has a message sent to it in the loop above, which
runs the StartSpawnTimer() function in the Spawner class.
The last line of LevelStarted() sets the target game state to Game.State.game-
Playing – signifying that the game is afoot.
Invoke("CallFadeOut", 4f);
Invoke("NextLevel", 6f);
}
Invoke("CallFadeOut", 2f);
Invoke("GameEnded", 3f);
}
void LoadSceneLoaderToRestart()
{
SceneManager.LoadScene("blaster_sceneLoader");
}
Since the LevelVars class still has the values set up for loading the same level
we are already on, all we do to have it restart is to reload the Scene loader.
LoadeSceneLoaderToRestart(), above, uses SceneManager.LoadScene() to tell
Unity to load blaster_sceneLoader.
void LoadMainMenu()
{
SceneManager.LoadScene("blaster_menu");
}
void LoadGameCompleteScene()
{
SceneManager.LoadScene("blaster_menu");
}
}
using UnityEngine;
using GPC;
public class BlasterPlayer : BasePlayerStatsController
{ public LayerMask projectileLayerMask;
public Transform explosionPrefab;
[Header("Respawn")]
public GameObject playerAvatarParent;
public float respawnTime = 2f;
public BasePlayerCharacterController _controller;
[Header("Weapon")]
public StandardSlotWeaponController _weaponControl;
public bool allowedToFire = true;
_TR = transform;
_controller =
GetComponent<BasePlayerCharacterController>();
}
void Update()
{
if (_controller._inputController.Fire1)
_weaponControl.Fire();
}
ReduceHealth(1);
if (GetHealth() <= 0)
{
Instantiate(explosionPrefab, _TR.position,
_TR.rotation);
If we know the Collider that hit the player is a projectile and we are not
respawning, we go ahead and reduce the player’s health stat by 1. Below that, we
take a look at GetHealth() to see if it is less or equal to 0. Since the player only has
one health in this game, one hit has the effect of killing the player instantly. If you
want to make the player have more than one chance before getting destroyed by
a projectile, you could change the SetHealth() statement in the Init() function
near the top of the class.
If GetHealth() says the health level is less or equal to 0, Instantiate creates an
explosion effect in the Scene, courtesy of the explosionPrefab prefab and
positioned at the player’s Transform position and rotation. We then call on the
PlayerDestroyed() function to deal with the next steps:
void PlayerDestroyed()
{
_controller.isRespawning = true;
Above, we override BaseAIController’s Start() function with this one. It calls the
Init() function to get set up. Init() calls its base class equivalent then sets the AI’s state
to AI.States.AIState.chasing_target so that our bots will start moving right away.
public override void Update()
{
base.Update();
}
15.3.5 User Interface
The UI script derives from BaseUIDataManager, the standard UI script shown in
Chapter 10. The BaseUIDataManager declares several common variables and
provides some basic functions to deal with them:
void Start()
{
// hide all ui canvas
HideAll();
}
void ShowGetReady()
{
ShowCanvas(0);
}
public void HideGetReadyUI()
{
HideCanvas(0);
}
public void ShowGameOverUI()
{
ShowCanvas(1);
}
public void ShowGameUI()
{
ShowCanvas(2);
}
public void HideGameUI()
{
HideCanvas(2);
}
public void ShowLevelComplete()
{
ShowCanvas(3);
}
public void HideLevelComplete()
{
HideCanvas(3);
}
public void UpdateScoreUI(int anAmount)
{
scoreText.text = "SCORE " + anAmount.ToString("D6");
}
public void UpdateHighScoreUI(int anAmount)
{
highText.text = "HIGHSCORE " + anAmount.ToString("D6");
}
public void UpdateLivesUI(int anAmount)
{
livesText.text = "LIVES " + anAmount.ToString();
}
}
15.4.1 Spawning Enemies
The Spawner class derives from BaseTimedSpawner, which is a class used for
automatically spawning another prefab and deleting itself once the prefab has
been added to the Scene. The idea is that you add Spawner (or a derivative of it) as
a Component to a cube primitive object in your Scene. The cube acts as a visual
guide, just so you can easily gauge where your prefab will be spawning. At run-
time, the cubes Renderer is disabled by the script, making it invisible, before the
prefab is spawned and the original spawner GameObject destroyed.
Spawner also allows you to set a randomized delay time so that spawning
may be staggered to avoid everything spawning in at the exact same time.
Spawner.cs was covered in Chapter 5, Section 5.8.1, in this book but in this chap-
ter, we will look at the derived version of it used specifically for this game. That is,
the Spawner class.
BlasterTimedSpawner gets its behavior from BaseTimedSpawner. Essentially,
the function of this script to register an enemy whenever one is spawned by the
regular BaseTimedSpawner code.
using GPC;
public class BlasterTimedSpawner : BaseTimedSpawner
{
public override void Start()
{
// do everything the base script does
base.Start();
// tell gamemanager about this enemy
RegisterEnemy();
}
void RegisterEnemy()
{
// make sure we have a game manager before doing
anything here
if (BlasterGameManager.instance == null)
{
Invoke("RegisterEnemy", 0.1f);
return;
}
284 Index
BlasterPlayer class, 274–277 CanvasGroup, 18, 39–40, 187, 189, 193
enemy bots, 277–278 CanvasManager, 39–40, 91, 187–197, 200,
user interface, 278–279 265
weapons and projectiles, 278 CanvasManager.LastCanvas, 198
BlasterEnemyBot, 270, 277–278 CanvasManager.
BlasterGameManager, 265–274, 277, TimedSwitchCanvas, 198
278, 280–281 Canvases, 42, 222, 272
BlasterPlayer, 265, 269–271, Capsule Collider, 23, 24
274–277 car (as C# script), 4
BlasterPlayer.GetScore, 270 Car Audio, 222
BlasterPlayerController, 62 car controller code, 138
BlasterUIManager, 269, 279 Char.ConvertToUtf32, 134–135
BlasterUIManager. CharacterController.SimpleMove, 106
UpdateScoreUI, 270 ChasingTarget, 172–173, 174
blocksRaycasts properties, 187–193 cheaters, 203
Boolean parameter, 127, 128, 233 CheckGround, 111, 113
Boolean variables, 8, 53, 56, 57, 64, 82, 85, CheckInput, 81–82
97, 104, 108, 109, 113, 120, CheckLock, 111, 114
131–132, 134, 140, 141, 158, 159, CheckWrongWay, 251, 252, 254
166, 169, 175, 182–185, 189–190, ChooseProfile, 201, 206–207
206, 212, 215, 235, 239, 246–248, class declaration, 4
251, 258, 275 class names, 62
bots, 155, 159, 171, 263, 264, 270, 277–278, CloseExitConfirm, 198
281 coroutines, 5–6, 148, 188–193, 212–214,
Box Collider, 36, 37–38, 88 215
brake (float variable), 114 code libraries, 6
brakeMax, 112 codes, 5–6
brakeTorque, 112 Collider, 36, 62, 89, 91, 92, 162, 252, 257,
braking, 115 259, 276
Build Settings panel, 46, 47 collision Components, Unity-specific,
61, 62
collision layers, 119
C collisionFlags bitmask, 106
C#, 99, 202 collisions, 23, 24
built-in system for iterator blocks, 6 communication between classes, 2
C# GPC framework, 9 CompletedLap, 241–242
C# script, 4, 15, 19, 263, 274 Component, 37, 39, 40
CSharpBookCode>Base>Input Component Base Profile Manager, 194
Controller, 26 Component menu, 26
CallFadeOut, 272–274 componentized methodology, 137
camera, 84–85, 137, 185, 194, 214, 222, 269 Compression level, 13
folders, 75, 76 Constraints field, 23
camera scripts, 76–80, 267 Constructor, 225
third-person camera, 77–80 controllers, 1–2
top-down camera, 80 main class, 50
camera system, 227 core Scene, 220, 228, 232–234, 236, 260
Camera.main, 185, 232 coreSceneName, 212–214–215, 216
CameraParent GameObject, 232 correspondingGroundHit, 117,
cameraScript.SetTarget, 232 118
CameraTarget, 58–59, 222, 232 CPU, 226
CameraThirdPerson, 265 CrashSound, 259
canAirSteer, 120 Create Empty, 22
canControl, 103–104, 108, 111, 183, CreateAssetMenu, 202
258 CreateEmptyProfiles, 205, 207
canFire, 130–131, 183–186 CreatePlayers, 267–268, 270
canSee, 161–162, 165, 175 Cube, 36, 37–38
canSeePlayer, 175 cube primitive, 88–89
CancelInvoke, 32, 33, 87, 88, 132, 185, curSmooth, 105–106, 107
226 curSpeed, 102, 106–107
Canvas buttons, 194 currentAIState, 150, 160, 172
Canvas GameObjects, 187 currentAlpha, 191–193
Canvas index numbers, 40 currentGameState, 17, 52–53
Index 285
currentID, 238–240 enemy controller, 50
currentLevel, 271, 272 enemy stats, 170–171
currentObjectNum, 86–87, 88 EnemyBot prefab, 278
currentRotationAngle, 78 enemyCounter, 270, 273
currentTime, 96–97–99, 191–193 EnemyDestroyed, 273
currentWayDist value, 168 EnemyHit, 270
currentWaypointDist, 247, 250, 251 engine AudioSource object, 115
currentWaypointNum, 168–169, 247, engine function, 4, 5
249–250 engineSoundSource, 110–111
currentWaypointTransform, 167, Euler rotation values, 79
169, 178, 180 exceptThis (parameter), 141, 143
custom states, 51 Exit_Confirm_Canvas, 194
cutscene, 51 ExitGame, 198
explosionPrefab, 276
ExtendedCustomMonoBehaviour,
D 51, 56–58, 70, 84, 88–90, 102,
data format (.json), 201, 203, 205, 208 103, 109, 116, 118, 130, 133, 156,
data storage, 61 157, 182, 259, 276, 277
DateTime class, 207 folder, 57
deathmatch-style scoring, 75 ExtendedMonoBehaviour class, 8, 91,
debug log console, 214 245
Debug.DrawRay calls, 163
debugging, 4–8, 11, 30, 80, 162–163, 165, F
172, 195, 201, 208, 209, 227, 271
DecibelToLinear, 196–197 FaceCamera, 94
default functions, 4, 5 faceWaypoints, 177
Destroy, 91, 232, 278 fade effects, 5
destroyAtEndOfWaypoints, 167, FadeCanvasGroup, 187, 190
169 fadeDuration, 191–193
DestroyGO [Game Object], 93 fading in and out, 187–193, 265, 266–267,
didInit, 8, 57, 67–71, 119–120, 159, 178, 272
180, 183, 204, 210, 227, 253–254, Fake Friction, 222
258 fakeBrakeDivider, 113
direct referencing scripts, 2 file path, 205, 208, 209
Disable, 129, 131 filename, 202, 204, 205, 208, 209, 215–217,
DisableCurrentWeapon, 126–129 223, 271
distance checking, 249 Filename parameter, 202
distanceBasedSpawnStart, 85, 86 filtering, 13
distanceCounter, 32, 34, 35, 46 final score, 45, 46
distanceFromCameraToSpawnAt, FinalScoreTextLabel, 41
86 FindGameObjectsWithTag, 273
distanceToChaseTarget, 175 FindNearestWaypoint, 141–143
distanceToSpawnPlatform, 17, 34, FindObjectOfType, 237, 269
35, 35, 46 FinishRace, 235
doFade, 189 finite state machines (FSM), 7
DoFakeBrake, 111, 113 Fire, 82, 129–133, 181, 275
doFire, 183–185 Fire1 (as jump key), 121
DoMovement, 172, 173 fireDelayTime, 185–186
doneFinalMessage, 235 fireDirection, 132
DoNotDestroyOnLoad tag, 211 FireProjectile, 132
DoSomething, 2 Firing, 183, 186
doSpawn, 87, 88 FixedUpdate, 4, 5, 52, 56, 77, 88, 95, 111
flow diagrams, 8
focusPlayerID, 231, 234
E focusPlayerScript, 243–244
Edge Collider 2D Component, 18–19, 19 follow camera, 77
Enable, 131 followTarget, 159, 161, 162, 174–175,
EnableCurrentWeapon, 127–129 180
encryption, 203 followTargetMaxAngle, 165
EndGame, 33, 43, 45 format strings, 98–99
enemy bot controller, 171–177 forward vector, 138, 163
enemy bots, 277–278 framework scripts, 50–59
286 Index
BaseCameraController, 51, 58–59 in Scene as Components, 2
BaseGameManager, 51–56 Script Component, 194
ExtendedCustomMonoBehaviour, user interface, 260–261
51, 56–58 GameOverMessageCanvas, 40, 41
Freeze Rotation, 23 GamePaused, 51, 53, 55–56
fromPos, 141 GamePlaying, 28, 30
GameSceneName, 47, 197
Game Sounds, 27, 27
G GameSounds, 148, 150, 151
game code, 2 GameStarted, 30, 42, 44, 45
game control, 80 GameStarting, 7, 28–31, 44
scripts, 5, 65 GameUICanvas, 40, 41
sub-folder, 75–76, 80 GameUnPaused, 56
Game Events label, 41 GetComponent, 58, 103, 110, 111,
game framework, 49–59 180–181, 199, 227, 230–232,
controllers, 50 235, 258, 269
diagram, 50 GetCurrentLap, 247
managers, 50 GetCurrentWaypointDist, 247
scripts, 50–59 GetCurrentWaypointNum, 247
game loading, 51 GetDirection, 108
game logic, 15, 18, 28, 53, 55, 56, 75, 200, GetFire, 64
211, 236, 274 GetFormattedTime, 96, 98
game loop, 28–36 GetGroundHit, 118
Game Manager, 3, 8, 28–36, 41, 49, 50, 53, GetHealth, 68, 72, 73, 171, 276, 278
66, 96, 200, 219–221 GetID, 246
GameObject, 37, 40, 234 GetInput, 103–104, 109, 111, 113–115
infinite runner game, 15–18 GetIsFinished, 248
Game Manager script, 2, 10, 51, 52, 54 GetLapsDone, 241, 247, 248
game over message, 46, 108 GetLives, 69, 72, 269, 271
Game panel, 11, 21, 23 GetMusicVolume, 199
game state, 28–29, 49, 51, 53, 75, 267 GetPlayerList, 67, 268
Game.State, 28, 30, 44, 52–55 GetPosition, 242
Game.State.gameEnding, 271 GetProfileName, 206
Game.State.gamePlaying, 53, 270, GetRacePosition, 241
271, 273 GetReadyMessageCanvas, 40
Game.State.levelEnding, 270, 273 GetRespawn, 64–65
Game.State.levelStarting, 267 GetRespawnWaypointTrans
Game.State.loaded, 53–55 form, 248, 256
Game.State.restartingLevel, 271 GetSceneByName, 214
Game.State.Starting, 53–55 GetScore, 269
GameEnded, 28, 30, 33, 44, 51, 274 GetSFXVolume, 199
GameEnding, 28, 30, 33, 44, 45 GetSpeed, 108
GameLoaded state, 7, 44 GetStartPoints, 229, 236
GameObject, 8, 57, 58, 163 GetTime, 99–100
GameObject referencing using GetTotal, 141, 249
SendMessage, 2 GetTransforms, 138, 141, 143, 144
GameObject.Find, 236, 268 GetUniqueID, 239
GameObject. GetVertical values, 115
FindGameObjectWithTag, GetWaypoint, 141, 144, 166, 167,
161, 162 169
GameObject.GetComponent, 182 GetWaypointsController, 228
GameObject.GetComponentInChildren, GetWaypointTransform, 248
182–183 Github, 9
GameObject.SetActive, 129 Gizmos, 11, 89, 163
GameObjects, 10–12, 15, 17–19, 21–22, definition, 85
27, 34–36, 41, 61, 65, 70, 76, Gizmos.color, 85, 139–141
83–94 passim, 103, 109–111, 113, Gizmos.DrawLine, 140
116, 117, 119, 125, 129, 130, 140, Gizmos.DrawSphere, 85, 140
141, 148–151, 159, 165, 181, 211, Global Race Controller script, 219
216–230 passim, 236–238, 246, global race manager script, 1–2, 225
259, 273 ‘global_userDatas’, 65–70
cached, 57, 157, 182 GlobalBattleManager, 75
Index 287
GlobalRaceManager, 50, 76, 222, Chapter2_UnComplete_ProjectFiles,
225, 233, 234, 238–246, 255 10
GlobalRaceManager. demise of RunMan, 36, 37–38
GetPosition, 256 example project, 10–13 (notes 12)
GlobalRaceManager. game loop, 28–36
raceAllDone, 255–256 Game Manager, 15–18, 28–36
goingWrongWay, 251–252, 254 inspector, 12
GPC (Game Programming Cookbook), 19, open main game scene, 14–15
37 platforms, 15
GPC namespace, 16, 24, 43, 52, 58, 63, 77, platforms to run on, 18–22
80, 83, 84, 89, 91, 95, 96, 102, Player, RunMan, 22–28
109, 116, 118, 124, 130, 134, 138, scene, game, project, hierarchy, 11
148, 156, 170, 171, 177, 188, 195, sprites, 12–13
198–199, 202, 204, 212, 215, 216, Unity editor as seen in 2 by 3 layout, 11,
223, 224, 238, 245, 253, 266, 275, 11
277, 279, 280 user Interface, 14, 37–47
GPC_Framework folders, 14, 47, 66, 70 inheritance, 3–5, 16, 22, 62, 178, 181, 212,
graphics, 9, 139, 194, 222, 257 253
Gravity Scale, 23 Init, 67, 102–103, 110, 130, 157–159,
green arrow, 18 166, 170–171, 181, 183, 195–196,
GroundLayerMask, 26, 119, 121 199, 204–207, 210, 225, 227, 246,
249, 253–254, 258, 267, 272,
275–276, 278
H InitNewRace, 225, 239
input, 63–65
Hand Tool, 12, 12 horizontal and vertical, 64–65, 102–108,
hashtables, 239–243 120, 160, 169–170, 172–174,
health, 65–69, 71 177–179, 181
HideAll, 187, 188 recipe component, 81–82
HideCanvas, 39, 40 sub-folder, 76, 81
HideCanvas(index), 187–190, 197 input Controller, 26, 61, 62
HideCount, 226, 236 Input Manager, 121
HideFinalMessage, 226, 260–261 input scripts, 81, 104
HideGameUI, 42 InputTypes, 63, 181
HideGetReadyUI, 41 Input.GetAxis, 64, 94
HideWrongWay, 261 Input.inputString, 134
Hierarchy, 21–24, 26, 27, 29, 35–37, Inspector, 15–19, 21, 22–26, 35, 36, 38, 40,
40–41, 47, 141, 149, 212, 216, 41, 47, 53–54, 58, 78, 85, 87, 89,
260, 277 90, 92–94, 109, 112–113, 119,
Hierarchy panel, 11, 12, 15, 18 146, 150, 151, 179, 185, 190, 196,
high score, 37, 39, 43–46, 65, 68, 83, 203, 197, 204–205, 212, 216, 217, 232,
206, 207, 209–210, 279 259, 267
HighScoreTextLabel, 41 Inspector window, 2, 12, 78, 93, 94, 107,
howManyLaps, 239 109, 111, 125, 126, 130–132, 140,
humanoids, 61, 62, 101–108, 219, 220–221, 148, 158, 163, 182–183
264 instance is null, 238
instance variable, 17
Instantiate, 34, 45, 58, 83, 125, 133, 229,
I 237
IEnumerator, 5, 6, 212, 213 integers, 57, 99, 126, 131–132, 141, 144,
In-Game UI (GameObject), 260 151, 162, 227, 234, 238, 239–242,
in-game user interface, 193, 200 245, 247, 261, 270
inUse, 206, 207 interpolation function, 78
incidental sounds, 250 Intro_UI (GameObject), 260
index numbers, 26, 40, 137, 141, 144, 148, Invoke, 32, 54, 87, 88, 90, 93, 132, 186, 197,
151–152, 239, 242 226, 271
infinite runner game, 9–47, 118, 122, 211, InvokeRepeating, 32, 33, 87, 88, 226
219, 264 isAhead, 242–244
Add Component panel, 16 isDone, 213–215
All_Completed_ProjectFiles, 11 isFinished, 246–248
anatomy of Unity project, 10 isGround, 113–114
animation, 13 isInfiniteAmmo, 131–132
288 Index
IsInLayerMask, 57, 58, 84, 89, 119, LoadAllProfiles, 205
165, 258–259, 276 LoadAsyncLevels, 213
isLapDone, 243–244, 246, 248, 252–253 LoadGameCompleteScene, 271
isLoaded, 131–132 LoadGameScene, 197
isLocked, 109, 111, 114 LoadLevel, 212–213, 216
IsMoving, 108 LoadMainMenu, 274
IsObstacleAhead, 162, 172–174 loadOnSceneStart, 212–213
isOnGround, 121 LoadProfileVolumes, 199
isRunning, 17, 32, 34–35, 46 LoadScene, 211
isStationary, 176–177 LoadSceneAsync, 213
isTimerRunning, 97–98 LoadSceneLoaderToRestart,
IsTrigger, 36, 38, 88, 89 274
isWrongWay, 237 LoadSceneMode, 213, 214
IsoCamController, 222, 232 Loaded, 31, 42
loadedProfile, 204, 208
loading level scenes, 211–217
J
building multi-scene level loader,
joystick axis, 115 216–217
JSON formatted string, 209 folder, 212
JsonUtility class, 208 multiple level loading, 215–216
JsonUtility.FromJson, 208 script, 212–215
JsonUtility.ToJson, 209 Loading_Canvas, 194
Jump parameter, 13, 13 lock state, 111
jumping, 9, 118, 121–122 LockPlayers, 233
LookAroundFor, 161, 172, 174, 176,
180
L LookAt, 79, 132, 140
laps, 225, 239 LookAtCamera, 94
lapsComplete, 252 LookForTarget, 172, 174
lasers, 145 loopPath, 157, 168–169
LastCanvas, 190
lastGameState, 17, 52 M
lastGroup, 190
lastPos, 139, 140 magnitudeToPlay, 259
lastSelectedWeaponSlot, 124, Main Camera, 232
126–128 Main Camera GameObject, 230
lastTime, 97–98 main game scene, 14–15
LateUpdate, 5, 80, 82, 88, 104, 111, 254, main menu, 45, 193–200, 220, 221
255 BaseMainMenuManager script,
Layer dropdown menu, 20, 21 195–200
LayerMask, 92, 119, 121, 259, 276 blaster game, 264
LayerMask playerLayer, 165 blaster game, 265
level loading sub-folder, 76 design, 194
level scenes, 211–217 ProfileManager script for
spawning enemies, 280–281 audio settings, 198–200
level select, 220, 221 main menu Scene, 14, 33
level select screen, 223 main menu setup, infinite runner
LevelEnding, 271, 273 game, 37, 47
LevelLoader, 211–212, 215–217 MakeProjectile, 132–133
LevelLoader.cs, 212, 264 manager and controller scripts, 1–3
levelNamePrefix, 217 communication between classes, 2
levelSceneToLoad, 212–213, 216 controllers, 1–2
LevelStarted, 272–273 managers, 1
LevelStarting, 7, 272 private static, 3
LevelVars, 223, 264, 267, 271–272, public static, 2
274 managers, 1
LevelVars.cs, 215–216, 221 main class, 50
LevelVars.currentLevel, 216, 217 managing players, 73
library C# code, 52 MaterialSetter, 231
List of UserData instances, 227 Mathf.Abs, 115, 118, 142, 143, 179
List of UserData objects, 269 Mathf.Atan2, 164, 251
List<UserData>, 65 Mathf.Clamp, 115, 153, 164
Index 289
Mathf.Infinity, 141–143, 165 N
Mathf.Lerp, 78–79, 105–107, 153,
192–193 namespaces, 6–7, 16, 52
Mathf.Min, 105, 107 naming conventions, 7–8
Mathf.Pow, 153 new GameObject, 149
Mathf.Rad2Deg, 164, 251 New Script, 24
maxChaseDistance, 158, 162, 165, newAlpha, 192–193
175 NewHighScoreTextLabel, 41
maxRange (distance limit), 142 NextLevel, 271, 273
maxSpeed value, 179 NextWeaponSlot, 127, 128, 134
MenuManager, 194 nodePosition, 168, 250–251
MenuManager GameObject, 47, 197 null check, 77, 243, 249
MenuTemplate Scene, 47 number keys, 64, 134
MenuWithProfiles, 47, 194, 198 numberOfProfileSlots, 204
MenuWithProfiles class, 200, 223 numberOfRacers, 229, 240, 242
MenuWithProfiles.cs, 221, 264
menus, 187–200 O
CanvasManager class, 187–193
in-game user interface, 200 object references (naming conventions),
main menu, 193–200 7–8
name parameter, 202 objectToSpawnOnTrigger, 89
Mesh Renderer Component, 89 obstacleAvoidDistance, 158, 163
Microsoft, format strings page, 99 obstacleAvoidLayers, 163, 179
minChaseDistance, 158, 175, 180 obstacleFinderResult, 172–176,
mind-mapping software, 8 178, 179
minimumSpawnTimeGap, 87 obstacleHitType, 162, 163–164
mixing audio, 145–147 OctoCar, 5
modelRotateSpeed, 165, 173–174 oldIsWrongWay, 237
modular game-making, 1–8 oldWrongWay, 252
naming conventions, 7–8 OnCollisionEnter, 276, 278
next steps, 8 OnDrawGizmos, 85, 139, 140
programming concepts, 1–7 OnDrawGizmosSelected, 139
MonoBehaviour, 4, 19, 52, 56–57, 62, OnEnabled, 225
63, 66, 93–96, 100, 124, 150, 170, OnGameEnded, 41
188, 195, 204, 212, 215, 238, 260, OnGameEnding event, 273–274
266 OnGameStarted event, 41
motor (float variable), 114 OnLevelEnding UnityEvent, 273
motorTorque, 112 OnLevelStarting UnityEvent, 272
mouse input, 81–82 OnLoaded, 41, 272
MoveAlongWaypointPath, 176–177 OnPause and OnUnPause UnityEvents, 56
MoveBack, 161, 175–176 OnTriggerEnter, 252, 258–259
moveDirection, 102–106, 108 1_prefabToSpawn field, 90
moveDirectionally, 101, 104, 108 onlyFireWhenOnScreen, 182, 185
MoveForward, 161, 174, 175, 180 options menu, 187, 194
moveSpeed, 105–108, 173 OptionsBackToMainMenu, 200
moveVel, 120 override, 4, 5, 25, 30, 31, 44, 54, 64, 70, 90,
movement codes, 61 122, 160, 172, 183, 194, 200, 217,
movement controller, 61–63 253, 277–278, 280
multi-scene level loader, 216–217 owner ID, 130, 131, 133
MultiLevelLoader.cs, 216, 217, 264 ownerNum, 124, 126, 130
Multiple, 13
multiple level loading, 215–216 P
music, 145, 146, 147
music volume, 194, 196–197, 199, 206, 207, Package Manager, 12
209 parameters, 13
myDataManager, 70–72 Pause button, 12, 12
myParent, 149 pause code inline, 6
myPos, 242, 244–245 pausing Unity games, 97
myPosition, 168 physics, 23, 24, 61, 62, 103, 159–160
myRacePosition, 253, 256 Unity updates, 97
mySpeed, 112, 115 physics simulations, 108, 110
myWayControl, 166, 169 physics systems, 179
290 Index
Physics.IgnoreLayerCollision, 133 PretendFriction, 94–96
Physics.Raycast, 92, 116, 117, PrevWeaponSlot, 128, 134
162–163, 165, 175, 184, 257 private static, 3
Physics2D.Raycast, 121 ProfileData class, 201–203, 207
pickups, 93 ProfileManager, 199–200
placeholders, 160, 172, 227 ProfileManager script, audio settings,
platforms, 9, 10, 26, 27, 32, 46, 118 198–200
platforms to run on, 18–22 profileName, 206, 207
Play button, 11, 12, 27, 33, 36, 47, 150 profileObject, 204–205
playAreaTopY and playAreaBottomY, 34, 35 profileSaveName, 204, 205, 208
PlayOnAwake, 153 ProfileScriptableObject, 202,
PlayOneShot, 148, 150 204, 205
PlaySound, 150–152 ProfileScriptableObject.cs, 83, 201
PlaySoundByIndex, 151–152, 233, 274 profiles
playback tools, 12 BaseProfileManager class,
Player (RunMan), 22–28 203–210
adding physics and collisions, 23, 24 folder, 201
adding sprite, 22, 22–23 saving and loading, 201–210
animation, 27 ScriptableObject, 201–203
player scripts, 24–26 programming concepts, 1–7
scoring, 28 coroutines, 5–6
sounds, 26–27, 27 finite state machines, 7
player controller, 50, 53, 61–63, 62 inheritance, 3–5
player data, 61 manager and controller scripts, 1–3
getting and setting, 68–70 namespaces, 6–7
player health level, 275 singleton, 3
player ID, 70, 239–241, 246, 247 Project browser, 212
player information, 50 Project folder, 42
player List, 227, 229–230, 233, 235 Project panel, 11, 14, 15, 21, 22, 27, 36, 47
player lives, 2, 69 projectileLayerMask, 276
player manager, 66, 71 projectileSpeed, 132–133
player movement controllers, 101–122 projectiles, 5, 123, 126, 278
folders, 101 public class, 4, 5
humanoid character, 101–108 public static, 2, 17
two-dimensional platform character
controller, 101, 118–122
wheel alignment, 115–118 Q
wheeled vehicle, 101, 108–115
player name, 65, 67 Quaternion rotation value, 229
player prefabs, 222, 226, 229, 231, 232, 268 Quaternion spawnRotation, 58
player scripts, 3, 269 Quaternion.Euler, 79
infinite runner game, 24–26 Quaternion.FromToRotation,
player stats, 65 92–93
player structure, 61–73 Quaternion.Lerp, 91, 93
BasePlayerStatsController Quaternion.LookRotation, 106
class, 70–73 Quaternions, 34, 45
dealing with input, 63–65
managing players, 73
player controller, 61–63, 62 R
user data, 65–70 race control, 238–259
playerAvatarParent, 277 Race Management, 221
PlayerDestroyed, 271, 276–277 raceAllDone, 239, 242
PlayerFell, 33 RaceComplete, 234, 255–256
PlayerFinishedRace, 246, 257, 258 RaceController, 1–2, 50, 76, 240,
playerGO1, 230 245–253
PlayerSpawnPoint, 268 hashtable, 241–243
Players, 49, 50 RaceController.cs, 239, 248
point scoring, 2 RaceFinished, 239–241, 246
pooling, 84 raceFinished.Count, 242
Position Graphic Setter, 222 RaceGameManager, 221, 222, 224–237,
Prefabs, 10, 21, 34, 35, 36, 84–91, 123, 222, 239, 254–256
224, 229, 230, 259 raceID, 245
Index 291
RaceInputController, 222, 231, 235, relativeWaypointPosition,
258 178–179
RaceInputController. reloadTime, 132
InputTypes, 258 Reloaded, 130–132
raceLaps (hashtable), 240, 241 Renderer, 182–183, 280
RaceLevelSelect.cs, 223 Renderer.IsVisibleFrom, 185
RaceMainMenu.cs, 223 rendererToTestAgainst, 182–183,
RacePlayerController, 219, 222, 185
228–233, 238, 253–259 RequireComponent, 109
RacePlayerController.cs, 245, 246 ResetFire, 185–186
racePositions (hashtable), ResetLapCount, 242, 247
240 ResetLapCounter, 234, 247, 253–254
RaceStart, 246 ResetProfile script, 206
RaceUIManager, 222, 226, 234, 260 ResetRespawnBlock, 257
racing game, 1–2, 73, 137–138, 155, 159, ResetTimer, 98
160, 172, 181, 211, 212, 219–261 ResetUsers, 67
controls, 220 Resources.Load, 205
folders, 219, 222–223, 238, 245 respawn points, 140
ingredients, 221–222 respawnerLayerMask, 258–259
level select scene overview, 223–224 respawning, 138, 248, 251–252, 254–259,
main menu scene overview, 223 276–277
overall structure, 219, 220 RestartLevel, 274
player structure, 221 ReturnToMenu, 33
scenes, 220 revolutions per minute (RPM), 117
racing game (core scene overview), Rewired (Guaveman Enterprises), 81
224–261 Rigidbody, 23, 62, 70, 89, 109, 110,
audio, 259 112–114, 118, 133, 179, 256
Game Manager, 224–237 cached, 57, 157
GlobalRaceManager class, _RB. _inputController, 103
238–245 2D ~ references, 8, 57
race control, 238–253 Rigidbody.AddForce, 94–95
RaceController class, 245–253 Robots (turn-based game), 263
RaceGameManager Class, 224–237 Rotate tool, 12
RacePlayerController class, rotateSpeed, 102, 104–106
253–259 rotateTransform parameter, 158, 164
user interface, 260–261 RotationValue, 117
Random function, 34 Run parameter, 13, 13
Random.Range, 88, 90 runAfterSeconds, 105
randomDelayMax, 90 RunMan: demise, 36, 37–38
raycasting, 116–117, 121, 163–165, 175, RunMan GameObject, 23, 24, 36
183, 184, 187–193, 257 RunMan sprite, 22, 22–23
definition, 91 RunMan_Animator, 13, 13
re-usability design, 8 RunMan_CharacterController,
reachedLastWaypoint, 167–169 24–25, 32, 118
realtimeSinceStartup, 97 RunMan_core Scene, 14, 15, 21
recipe, audio, 145–153 RunMan_GameManager, 15, 17, 20,
recipe components, 75–100 31–33
AI scripts, 76 completed script, 43–46
camera, 76–80 RunMan_GameManager Component,
folder and sub-folders, 75–76 35–36
game control, 80 RunMan_GameManager script, 28, 42
input, 81–82 main loop, 29–35
scriptable objects, 83 runMan_menu Scene, 46, 47
spawning, 83–91 RunMan_Platform, 21
user interface, 91 RunMan_Platform GameObject, 18
utility, 91–100 RunMan_Platform Prefab, 36
Rect Transform, 12 RunMan_Platform sprite, 18
red arrow, 18, 21–22 RunMan_Run-Sheet, 22–23, 27
ReduceHealth, 69, 71, 171, 276, 278 RunMan_UIManager, 37, 40, 40–42
ReduceLives, 277 RunMan_UIManager.ShowGetReadyUI,
ReduceScore, 71 54
RegisterEnemy, 270, 280–281 RunManCharacter object, 32
292 Index
runPower, 120 SetLock, 233, 258
runSpeed, 17, 20, 31, 33, 34, 35, 45, 46, SetMaterial, 231
105, 107 SetMoveSpeed, 177
Runtime Only box, 41 SetMusicVolume, 200
SetOwner, 126, 130
SetPlayerDetails, 269
S SetReverseMode, 141–142
save slots, 201 SetScore, 31, 39, 43, 44, 46, 68, 72, 171
SaveProfiles, 200, 208–209 SetSFXVolumeLevel, 196, 200
Scale Transform button, 36 SetTarget, 59, 267
Scene, 21, 21, 22, 32, 35, 36, 58, 84, 88, 91, SetTargetState, 29, 33, 44, 45, 52–54,
118, 148, 149, 151, 194, 196, 197, 267, 270, 271
225, 226, 230, 234, 235, 276 SetUserInput, 103, 104, 111
Scene folder, 14 SetWayController, 166–167
Scene loader, 220, 221 SetWeaponSlot, 125–126, 133–135
blaster game, 264 SetupPlayers, 226, 227, 229, 233–234,
Scene loading, 65–67, 264 236
Scene panel, 11, 12, 19 SetupSliders, 195–196
Scene view, 18–19 sfxVolume, 206, 207, 209
SceneManager, 33, 195, 211–213, 224 ShootingPlayerCharacter, 62
SceneManager.LoadScene, 197, 211, shouldRandomizeSpawnTime, 87–90
272, 274 shouldReverse, 141, 144
SceneManager.LoadSceneAsync, shouldReversePathFollowing,
214 166, 168–169
SceneManager.MergeScenes, ShowCanvas, 39, 40
214–215 ShowCanvas(index), 187, 190, 197
SceneManager.SetActiveScene, ShowCount, 235–236, 261
215 ShowExitConfirm, 198
Scenes, 10 ShowGameOverUI, 42
score display, 49, 200, 270 ShowGameUI, 41
ScoreTextLabel, 41 ShowGetReadyUI, 41
scores, 65, 66, 68 ShowMainMenu, 195, 197
scoring, infinite runner game, 28 ShowOptions, 197–198
ScreenAndAudioFader class, 91, 265, ShowWrongWay, 261
272 sidewaysSlip value, 118
script components, 2 SimpleMove, 106
script editor, 19 Singleton, 3, 17, 20, 225, 233, 266
ScriptableObjects, 83, 204–205, Size field, 40
208–209 Slider Component, 195
folders folder, 76, 204 slipAmountForTireSmoke, 118
user data, 201–203 slipPrefab object, 118
ScriptableObjects: slotNum, 126–127
ProfileData parameter, 205 SmoothDampAngle, 78, 79
Scripts, BASE folder, 50 sound, 194
selectedWeaponSlot, 124, 126–129 Sound Controller, 222
SendMessage, 2 blaster game, 265
Serializable class, 203 sound effects, 27, 36, 145, 146
serialization, 66 Sound Effects Mixer, 151
SerializeField, 203 Sound Manger, 49, 50
session data, 50 sound playback, 49
SetActive, 227, 260–261, 277 SoundObject class, 148–151
SetActiveScene, 215 five parameters, 149, 151
SetAIControl, 158–159 soundObjectList, 150–151
SetAIState, 158–159, 162, 174–175 sounds, 10, 26–27, 27
SetChaseTarget, 159 sourceTR, 149, 150
SetCollider, 131 Space key, 27
SetFloat, 199 Spawn, 91, 116, 133, 229, 237
SetHealth, 69–72, 171, 275, 276 SpawnAndDestroy, 90–91
SetID, 57, 58 spawnCounter, 86–88
SetInputType, 231, 235, 258 spawnObject, 58
SetLaps, 234, 260 SpawnPlatform, 34–35, 43, 45–46
SetLives, 69, 71 spawnPointString, 268
Index 293
spawnPosition, 58 System.Collections, 91, 93, 96, 124,
spawnRotation, 58 188, 212
spawnTime, 90 System.Collections.Generic,
Spawner base class, 84–86, 89, 90 138, 148, 224, 266
Spawner class, 273 System.IO.File class, 203
Spawner Component, 84–88 System.IO.File.Exists code, 205
Spawner.cs, 280 System.IO.File.ReadAllText,
spawning, 83–91, 265 208
folder, 76, 84 System.IO.File.WriteAllText,
spawning platforms, 33–34 209
speedSmoothing, 107
Sphere Collider, 118
Sprite Mode, 13 T
Sprite Renderer, 27 tagOfTargetsToShootAt, 184
sprite sheets, 12–13 targetAIState, 160
sprites, 12–13, 122 targetAngle, 251
folder, 12, 13, 18, 22 targetDirection, 104, 105, 107
Standard_SlotWeaponController.cs script, targetGameState, 17, 52, 53
181 targetLayer, 175
StandardSlotController, 278 targetSpeed, 105–107
StandardSlotWeaponController, tempDirVec, 157, 165
124, 181, 275 tempIndex, 141–143
StandardSlotWeaponController. tempListener, 232
Fire, 275 tempProfiles, 208
Start, 4, 29–30, 81, 90, 92, 94, 95, 102, 110, tempRC, 242–244
116, 125, 130, 138, 150, 157, tempSoundObj, 151
177–178, 195–196, 213, 216, 217, tempTR, 8, 57
225, 229, 253–254, 266, 280 tempVEC, 8, 256–257
start button, 66 TEMPVec3, 63, 65
start-finish line, 241, 246, 250, 252 TEMPWeapon, 128, 129
startAtFirstWaypoint, 166 template scripts, 4
StartCoroutine, 6, 213 temporary variables, 8
StartFadeCanvasSwitch temporary vector actions (tempVEC), 57
coroutine, 191 Text Component references, 260
StartGame, 31–32, 47 text fields, 40
StartPoints, 236 Texture Setter Component, 222
StartRace, 226, 233 The RunMan_Platform Components, 19
StartRunAnimation, 32 theDist, 161
StartRunning, 31–33 theFileName, 205
StartSpawn, 86 theLap, 234
StartSpawnTimer, 90, 273 theListenerTransform, 151–152
StartTimer, 97 theProfileData, 208
StartWave, 86, 87 third-person camera, 77–80
starting points, 224 this (keyword), 3, 17
state machine AI, 155 thisGameObjectShouldFire, 183
static variables, 2, 225, 238 three-dimensional
steer (float variable), 114 games, 155
SteerToTarget, 179–180 primitives, 36, 37
SteerToWaypoint, 180 units, 158
Steering AI Glue, 222 Time (Unity helper class), 20
Step button, 12, 12 Time Before Object Destroy, 19
StopTimer, 98 time-based actions, 5
StoppedTurnLeft, 176 Time.deltaTime, 20, 34–35, 46,
StoppedTurnRight, 176 78–80, 93, 94, 105, 106, 117,
string data, 209 153, 165, 173–174, 191
stuckResetTime, 254–255 Time.fixedDeltaTime, 56
stuckTimer, 255 Time.realtimeSinceStartup,
sub-folders, 202 96–98
sub-menus, 190, 194 Time.time, 105–107, 252, 254
switching between weapons, 124, Time.timeScale, 105
133–135 timeBeforeObjectDestroys, 93
system functions, 52 timeBetweenSpawns, 87, 88
294 Index
timedif, 254 get ready message, 37
timeElapsed, 96–98 in-game, 200
timeScale, 56 in-game (to tell players current score),
timeScaleFactor, 96, 97 37
timeString, 99 infinite runner game, 37–47
timeWrongWayStarted, 252, 254 main menu setup, 37, 47
TimedSpawner Component, 89–91 racing game, 260–261
TimedSwitchCanvas, 190–191 recipe component, 91
TimerClass, 76, 96–100 sub-folder, 76
automatic updating, 100 updating, 226
timestamp, 97 UI Canvas, 39, 50, 187
tire slip calculations, 117–118 UI Canvas Groups, 40, 40, 189, 193, 197
ToString, 99, 260 UI Manager, 49, 50
top-down camera, 80 UI Panel GameObject, 187
totalAmountToSpawn, 86–88 UI_Canvases GameObject, 40, 260
totalLaps, 239, 241 UI_Canvases object, 18
totalSpawnObjects, 85 Unicode, 134
totalTransforms, 144 uniqueNum, 3
totalWaypoints, 166, 168–169, 250 Unity, 2, 10, 13, 22, 27, 29, 32, 35, 36, 39,
track, 224, 228 47, 58, 83, 159, 181, 182, 185,
tracking states systems, 7 192, 211, 225
Transform, 2, 8, 10, 19, 20, 62, 70, 91–93, AddTorque function, 112–113
96, 103, 116, 125, 130, 132, 133, Build Settings panel, 46, 47
149, 151, 159, 162, 229, 232, 236, building multi-scene level loader in,
268, 275 216–217
cached version, 56–59, 85, 157, 182, 246 character controller, 101
temporary references (_tempTR), 57 collision systems, 91
Transform box, 22, 23 default audible volume, 199
Transform tools, 12 gizmos, 139–140
Transform.CompareTag, 184 input system, 63, 64
Transform. JsonUtility, 203
InverseTransformPoint, mixing audio, 145–147
179, 251 raycasting function, 113–114
Transform.Rotate, 165, 173 RequireComponent, 199
Transform.targetAngle, 165 Resources system, 205
Transform.TransformPoint, 117 use of singleton, 3
Transform.Translate, 173 Wheel Colliders, 108, 219
Transforms, 142, 144 Unity Collider, 33
Translate, 20 Unity documentation, 4, 5–6, 9, 56, 64, 78,
Trigger Spawning, 88–89 92, 109, 140
triggers, 36 Unity editor, 9, 11, 40, 52, 81, 82, 84, 165,
turnHelp, 112 201, 202, 208, 212, 260
turnHelpAmount, 112–113 RunMan_GameManager
TurnLeft, 160 Component, 35–36; see also
turnMultiplier, 179 Inspector window
TurnRight, 161 Unity GUI, 194
TurnTowardTarget, 164, 165, 175 Unity packages, 12
two-dimensional Unity Project anatomy, 10
Collider, 118 UnityEngine, 16, 57, 58, 63, 66, 70, 77,
player movement controller, 101, 80, 84, 89, 91, 93, 95, 96, 102,
118–122 109, 116, 118, 124, 130, 134, 138,
primitives, 36, 38 150, 156, 170, 171, 177, 181, 183,
Sprite Package, 12 188, 198, 202, 204, 215, 245, 252,
253, 275, 277
CanvasGroup, 187
U UnityEngine.Audio, 148, 195
UI (user interface), 18, 194, 195–196, 222, UnityEngine.SceneManagement,
256 195, 212, 217, 223, 224, 266
blaster game, 265, 272, 278–279 UnityEngine.UI, 195, 260, 279
Game Events on Game Manager UnityEvent OnLoaded, 31
Component in Inspector, 42 UnityEvents, 31, 41, 52–54, 272,
game over message, 37, 39 274
Index 295
UnloadSceneAsync, 215 W
Update, 4, 5, 19–20, 34–35, 43, 46, 77, 86,
88, 92, 94, 100, 107, 117, 134, WaitBetweenFadeTime, 190, 193
159, 172, 181, 183, 266, 278 waitForCall, 90
UpdateCurrentState, 29, 55, 159, WaitForSeconds, 6, 193, 215
160, 172, 174–176, 178 waitTime, 193
UpdateDirectionalMovement, walkSpeed, 105–107
104, 106, 107 walkTimeStart, 105–107
UpdateEngineAudio, 111 waypoint distance checking, 244
UpdateLapCounter, 226, 234 waypoint management, 277
UpdateMovement, 120–121 Waypoint_Controller.cs, 169
UpdatePhysics, 111, 115 waypointDistance, 158, 168
UpdatePositions, 226, 234 waypointManager.
UpdateRacePosition, 256 GetWaypoint, 249
UpdateRotationalMovement, 106 waypoints, 224, 241–244, 247–249,
UpdateSmoothedMovement 251–252, 255, 257
Direction, 104, 108 usefulness, 137
UpdateTargetState, 29–31, 33, 45, waypoints controller script, 138
54, 55, 158–160 waypoints system, 137–144
UpdateTimer, 96–100 FindNearest
UpdateUIScore, 270 Waypoint, 141–143
UpdateWaypoints, 166–167, 178, functions, 137–138
248–250, 255 GetTotal, 141
UpdateWrongWay, 237, 254, 255 GetWaypoint, 141, 144
useForceVectorDirection, 130 SetReverseMode, 141–142
user controlled vehicles, 222 utility functions (publicly-accessible),
user data, 50, 65–70, 73 141–144
saving and loading, 201–210 WaypointsController, 139, 141, 155,
user data manager, 61, 62 157, 166–167, 230, 236–237, 248,
UserData class, 67, 227 249
folder, 66 weapon control, 181–186
utility (recipe component), 91–100 attack states, 181
AlignToGround, 91–93 BaseEnemyWeaponController,
AutomaticDestroyObject, 181–186
93 WeaponController, 123, 185
AutoSpinObject, 93–94 weapons, 61–64, 278
FaceCamera, 94 disabled, 125–129
LookAtCamera, 94 sound, 153
PretendFriction, 94–96 sub-folder, 76
TimerClass, 96–100 weapons list, 124–125
utility folder, 76, 91 weapons system scripts, 123–135
BaseWeaponController class,
123–130
V BaseWeaponScript, 123, 130–133
folder, 124
Vector2, 34, 45 switching between weapons, 124,
Vector3, 8, 20, 57, 58, 63, 79, 92, 94, 103, 133–135
108, 130, 131, 150, 164, 229 weaponScripts, 125, 128, 129
‘not directly serializable’, 203 weaponSlots, 125–129
Vector3.Distance, 161, 250 wheel alignment, 115–118
Vector3.Lerp, 80 WheelCollider.center property, 117
Vector3.RotateTowards, 104, WheelColliders, 108–109, 112–118
105 WheelHit object, 118
vehicle ID, 253 WheelHit structure, 117–118
vehicle prefabs, 231, 253 WheelToGroundCheckHeight, 114
vehicles, 61, 62, 112, 219, 222, 259 wheeled vehicle, 108–115
virtual functions, 4, 30, 52, 54, 55 WheeledVehicleController, 222,
visual representation, 137 259
Visual Studio, 15 wheels function, 4, 5
volume levels, 195–197 whichSlot, 206
volume sliders, 194 Windows Explorer, 11
Vroom, 4, 5 wrongWaySign, 226–227
296 Index
X yield return null, 5, 6, 92, 152–153,
192–193, 213–215
x axis, 18, 21–22, 36, 65, 112, 117, 167–168, yield statements, 6
173, 203, 233
Y Z
y axis, 18, 65, 78, 79, 104, 106, 111, 114, z axis, 79, 104, 106, 112, 114, 132, 167, 173,
117, 121, 167–168, 173, 203, 233, 203, 233
249–250
Index 297