Learning C# by Developing Games With Unity
Learning C# by Developing Games With Unity
B u i l d , c u s t o m i z e , a n d o p t i m i z e
p r o f e s s i o n a l g a m e s u s i n g U n i t y
Claudia Alves
For information contact :
(alabamamond@gmail.com, memlnc)
http://www.memlnc.com
INTRODUCTION_____________________________________________________12
2D OR 3D PROJECTS----------------------------------------------------------------------------13
Full 3D-----------------------------------------------------------------------------------------------13
Orthographic 3D------------------------------------------------------------------------------------14
Full 2D-----------------------------------------------------------------------------------------------15
2D gameplay with 3D graphics-------------------------------------------------------------------15
Why do we use game engines? -------------------------------------------------------------------17
Quick steps to get started with Unity Engine---------------------------------------------------18
CHAPTER 1__________________________________________________________23
The language C #-----------------------------------------------------------------------------------23
syntax-------------------------------------------------------------------------------------------------25
Comments--------------------------------------------------------------------------------------------25
variables----------------------------------------------------------------------------------------------26
Naming conventions--------------------------------------------------------------------------------27
Data types--------------------------------------------------------------------------------------------27
Variable declaration with initialization----------------------------------------------------------29
Boolean variable------------------------------------------------------------------------------------30
Keyword var-----------------------------------------------------------------------------------------30
Data fields / array-----------------------------------------------------------------------------------30
Create arrays-----------------------------------------------------------------------------------------31
Access to an array element-----------------------------------------------------------------------32
Determine the number of all array items-------------------------------------------------------32
Multidimensional arrays-------------------------------------------------------------------------33
Constants-------------------------------------------------------------------------------------------34
Enumeration---------------------------------------------------------------------------------------34
Type conversion-----------------------------------------------------------------------------------35
Ramifications--------------------------------------------------------------------------------------37
if statements----------------------------------------------------------------------------------------37
switch statement-----------------------------------------------------------------------------------38
grind------------------------------------------------------------------------------------------------40
for loop---------------------------------------------------------------------------------------------40
Negative step size---------------------------------------------------------------------------------41
Break-----------------------------------------------------------------------------------------------41
Foreach loop---------------------------------------------------------------------------------------43
while loop------------------------------------------------------------------------------------------43
do loop---------------------------------------------------------------------------------------------44
Classes---------------------------------------------------------------------------------------------45
Assign components by code--------------------------------------------------------------------46
Instantiation of non-components---------------------------------------------------------------47
Methods / functions------------------------------------------------------------------------------48
Don't repeat yourself-----------------------------------------------------------------------------50
Value types and reference types------------------------------------------------------------------50
Overloaded methods--------------------------------------------------------------------------------51
Local and global variables-------------------------------------------------------------------------52
Prevent confusion with this------------------------------------------------------------------------52
Access and visibility--------------------------------------------------------------------------------53
Static classes and class members-----------------------------------------------------------------54
Parameter modifier out / ref-----------------------------------------------------------------------56
Array passing with params------------------------------------------------------------------------58
Properties and property methods-----------------------------------------------------------------60
Inheritance-------------------------------------------------------------------------------------------63
Base class and derived classes--------------------------------------------------------------------64
Inheritance and visibility---------------------------------------------------------------------------65
Override inherited method-------------------------------------------------------------------------66
Access to the base class----------------------------------------------------------------------------68
Seal classes------------------------------------------------------------------------------------------69
Polymorphism---------------------------------------------------------------------------------------69
Interfaces---------------------------------------------------------------------------------------------71
Define interface-------------------------------------------------------------------------------------71
Implement interfaces-------------------------------------------------------------------------------72
Support from MonoDevelop----------------------------------------------------------------------74
Access via an interface-----------------------------------------------------------------------------74
Namespaces------------------------------------------------------------------------------------------75
Generic classes and methods----------------------------------------------------------------------77
Cunning----------------------------------------------------------------------------------------------78
Sort list objects--------------------------------------------------------------------------------------80
Dictionary--------------------------------------------------------------------------------------------80
CHAPTER 2-----------------------------------------------------------------------------------------84
Script programming--------------------------------------------------------------------------------85
Script programming--------------------------------------------------------------------------------85
MonoDevelop---------------------------------------------------------------------------------------85
Help in MonoDevelop------------------------------------------------------------------------------86
Syntax error------------------------------------------------------------------------------------------86
Forwarding of error messages---------------------------------------------------------------------87
Usable programming languages------------------------------------------------------------------87
Why C #?--------------------------------------------------------------------------------------------88
Unity's inheritance structure----------------------------------------------------------------------88
Object------------------------------------------------------------------------------------------------89
GameObject-----------------------------------------------------------------------------------------90
ScriptableObject------------------------------------------------------------------------------------90
Component-------------------------------------------------------------------------------------------90
Transform--------------------------------------------------------------------------------------------91
Behavior----------------------------------------------------------------------------------------------91
MonoBehaviour-------------------------------------------------------------------------------------92
CHAPTER 3__________________________________________________________137
Special attacks for projectiles--------------------------------------------------------------------152
Ejector industry------------------------------------------------------------------------------------159
INTRODUCTION
Many 2D games use flat graphics, sometimes called sprites, which have no
three-dimensional geometry at all. They are drawn to the screen as flat
images, and the game’s camera has no perspective. For this type of game,
you should start the editor in 2D mode.
2D gameplay with 3D graphics
On the other hand, there are features of this industry that make
survival in it possible. On the technical side, for example, the vast
majority of games are not free of similar functions and repetitive
patterns of data processing, which makes the reuse of the software
modules of previous games in order to create new games possible.
This, in turn, contributes to overcoming technical obstacles and
shortening time and effort.
When you talk about making a game, you are here to mention the
big process that involves dozens and possibly hundreds of tasks to
accomplish in many areas. Making a game means producing,
marketing, and publishing it, and all the administrative, technical,
technical, financial, and legal procedures and steps involved in
these operations. However, what is important for us in this series of
lessons is the technical aspect which is game development, which
is the process of building the final software product with all its
components. This process does not necessarily include game
design, as the design process has a broader perspective and focuses
on such things as the story, the general characteristic of the game,
the shapes of the stages and the nature of the opponents, as well as
the rules of the game, its goals and terms of winning and losing.
1.The name defaults to New Unity Project but you can change it to
whatever you want. Type the name you want to call your project
into theProject namefield.
Scene Window: It you use to build the game scene, add different objects to
it and distribute it in 2D space. Initially this window contains only one object
which is the camera.
Hierarchy: contains a tree arrangement that contains all the objects that
have been added to the scene and helps you in organizing the relationships
between them, as it is possible to add objects as children to other beings so
that the son being is affected by the parent being as we will see. Initially this
window contains only one object which is the camera.
Project Browser: Displays all files inside the project folder, whether they
were added to the scene or not added. The project initially contains one folder
called Assets, and inside it we will add all other files and folders.
Inspector Properties Window: When selecting any object from the
scene hierarchy, scene window, or project browser, its properties will appear
in this window and you can change it from there.
Console :The console tab is used to display error and warning messages. If
you have made programming errors, for example, Unity displays them here.
But you, as a developer, can also issue reports about this.
Messages about errors that must be rectified (errors) are shown in red in the
window. In contrast, warnings, i.e. non-critical errors, appear yellow. Normal
information messages are displayed in white. You can also filter the
messages according to these ratings using the three symbols on the right. In
addition to the three filter options, the upper console bar also has a few
buttons. These have the following meanings:
-Clear on Play deletes all entries at the start of the game via the play button.
HealthController
using UnityEngine;
using System.Collections;
public class HealthController : MonoBehaviour {
public float health = 5;
private bool isDead = false;
void ApplyDamage(float damage) {
health -= damage;
if(health <= 0 && !isDead) {
isDead = true;
Dying();
}
else {
Damaging();
}
}
public virtual void Damaging() {
}
public virtual void Dying () {
}
}
syntax
First of all, a few words about basic programming: Program code (also called
source code) consists of lines of code that are processed one after the other.
So that the computer also understands these lines, they are translated into
machine language with a program called compiler, the so-called compiling.
So that the compiler also knows when the end of a single line of code is
reached, a semicolon is added to the end in C #; written. Other important
characters in C # are the curly braces {}. These are used to identify coherent
blocks of code.
Comments
In addition to the actual code, you may also want to add annotations to your
code that are not meant to be run by the computer. Such lines that are not to
be executed are also called comments. Mark a comment with a double slash
//. Anything behind it until the next line break is considered non-executable
code. What is in this line before the double slash is considered executable!
Multi-line comment
/ * This is old Codeint life
Points;
lifePoints = 2;
*/
int lifePoints = 2;
// This is the new code
Variable declaration
int lifePoints;
float height;
string name;
Assign values
lifePoints = 2;
name = "Carsten";
height = 1.5F;
As you can see, in addition to the text itself, I use quotes to tell the compiler
where the assigned text begins and where it ends. When I assign commas (I
need the float data type), I write a period instead of the comma. I also write
an F after the value for float variables so that the compiler knows that the
value is actually a floating point number of the float type. Alternatively, there
is also the double type in C #, which, however, covers a larger number range
than float and therefore does not "fit" into a float variable:
float gravity;
gravity = 9.81F;
double gravity;
double = 9.81;
In Unity, however, float is more common than double, which is why I will
only speak of float values in the future. To shorten the code above, you can
also assign a value to the variable when you declare it (also called
initialization):
Variable declaration with
initialization
float gravity = 9.81F;
Another type that is used a lot in game programming is the data type Boolean
(bool). This type can only assume two states: TRUE and FALSE, meaning
true or false. A typical example of this is a checkbox, i.e. a checkmark in the
GUI, for example to activate or deactivate a function. Depending on the state
of this checkbox, the Boolean variable that stores the value in the program
code has the value TRUE or FALSE.
Boolean variable
bool isAttacking = false;
Keyword var
In addition to specifying a data type with int, string, etc., there is also the
option in C # to define a variable with var . In this case, the data type of this
variable is only determined when the first value is assigned. Then the
compiler decides which data type is the right one and defines it. Any future
assignment that would require a different type then leads to an error.
int [] speedLimits;
In contrast to a normal variable, the array does not yet exist through the sole
definition of the variable. You have to do this again with the keyword new .
This process is also called instantiation. In addition, when you instantiate the
array, you must also specify how large it should be, i.e. how many integer
variables the array should consist of. It looks like this:
Or a little shorter:
When instantiating, you can also give each element of the array a start value.
This can look like this:
Arrays may also be created automatically. If you want to know how many
items the array now consists of, you can determine this using the Length
property :
Again, keep in mind that each index in the array begins with a 0. If you now
encrypt the figures with numbers (0 means no figure, 1 represents a pawn, 2
is a tower), you can easily save the positions of the figures in this array:
You can then create a variable of this enumeration type and assign one of the
three values to it.
This function is very useful, because in this way the integer variable can be
treated like a string:
if (lifePoints == 2) {
else {
message = "I don't know how many lives you have." + "But there are
definitely no 2.";
If the code block or else branch consists of only one line, the curly brackets
can also be omitted.
int damage = 1;
string title;
switch (damage) {
case 0:
message = "Beside!";
case 1:
break;
case 2:
default:
message = "Arrrgh!";
break;
It is important for the switch statement to end each case section with the
break command . This ensures that the switch statement is exited after the
code has been executed. If none of the defined case conditions apply, you can
finally use the default signal word to define a standard case that is selected as
soon as none of the previous cases matched. In Listing 3.34 this would
always be the case if the damage was higher than two ( we simply ignore a
negative damage value).
grind
In addition to the branches, loops are another important element in
programming. These ensure that certain code sections are repeated until a
specified termination condition is met.
for loop
The classic loop is the for loop. It is used when it is known how often a code
area should be repeated. With this type of loop, a count variable (loop
counter) is assigned a start value and incremented as long as a specified
running condition is fulfilled. In C # the first expression is the loop counter
with the start value, the second represents the running condition, the third
expression describes the counter increment.
int counter = 0;
//Code...
In the example above, the counter counter was set to the start value 0 and
runs through the following code ten times. This is due to the counter increase
++ , which increases the value of counter by one after each run.
Negative step size
The next example shows a step size of –2 and runs through the code as long
as the counter is greater than 0. In addition, here the loop counter is only
defined in the for loop, which is also quite common in practice:
//Code...
}
Break
A for loop does not always have to be run through to the end. It can also be
canceled using the break keyword :
int currentGear;
//Code...
}
Foreach loop
The foreach loop is used to run through arrays. Here an additional run
variable is used, which takes over the value of the current element of the
array. Since this is a copy of the original, the contents of the array cannot be
changed in this way. It is important to know that you only define the run
variable in the loop.
if (currentLimit == 80) {
// code ...}
}
while loop
The while loop continues until an abort condition is met. The termination
condition is queried at the beginning of the loop, so that the loop may not
even be able to be executed, namely if the termination condition is met from
the beginning.
int enemyIndex = 0;
if (enemyIndex == 3)
break; //Code...
enemyIndex ++;
This loop can also be simply ended with the break command without
fulfilling the actual termination condition. In addition, there is also the
continue command . This ensures that the loop behaves as if it had already
arrived at the end of the loop code when the continue command is reached,
and therefore begins with the next iteration of the loop.
do loop
In contrast to the while loop, where the termination condition is checked at
the beginning, the condition in the do loop is only checked at the end. The
result is that the loop makes at least one pass, regardless of whether the
condition at the end of the loop was met from the beginning or not
int enemyIndex = 0;
do {
//Code...
enemyIndex ++;
With the do loop it is also possible to leave the loop with the break command
.
Classes
As already mentioned at the beginning, C # is a so-called object-oriented
programming. The aim is to bundle and encapsulate coherent functionalities
and values in so-called objects (which are nothing more than variables). B.
Everything that concerns the player is in an object called player . Everything
that affects the enemy is saved by an object called enemy . What do we do if
we want to have two or more opponents, not just one opponent? Do we have
to program a completely new object for every opponent? No, of course not,
because this is where classes come in. A class is a template for objects. It
describes how objects in this class “look” and how they should work. A class
begins with the word class , followed by the name of the class. To better
distinguish it from normal variables, this begins with a capital letter in C #.
Then the code of the class follows, which is enclosed in curly brackets:
class enemy {
int health;
In Unity, a script represents exactly one class. The name of the script must
have the same name as the class in the script. If you now want to create an
object from a script or a class, this is very easy in Unity. Since scripts are
normally considered to be components (more on this in the chapter "Script
programming"), you can easily drag them onto a respective GameObject,
which automatically creates an object of this class. This process is also called
instantiation. The result, i.e. the object, is therefore often referred to as an
instance of this class.
Assign components by code
Instead of dragging such a component script onto a GameObject, you can
alternatively assign a script or an object of your class to a GameObject using
program code. For this there is the command AddComponent .
Enemy enemy;
The two brackets after new and the class name are important and must always
be written, since this is not the class itself, but the constructor of this class, a
method that is carried out when the object is created. You can find out more
about the topic of constructor e.g. B. in the previously mentioned books by
Wal-ter Doberenz and Thomas Profitus.
Methods / functions
Algorithms, i.e. lines of code that do not contain variable declarations or
similar. within a class can only be written in methods. Methods are code units
that are addressed by calling their method name and can have both transfer
parameters and return values. Note that the term “method” is not normally
used in JavaScript, instead the term “function” is used there . In both cases,
however, the same is meant. And because, as already mentioned, Unity does
not only support C # as a language, Unity developers often use both terms.
For methods / functions there is a similar naming convention as for variables,
except that they begin with a capital letter, e.g. B. SetPosition . A method
definition has two important parts:
Transfer parameters, they are defined after the method name within
parentheses with the variable type. If the method has no transfer parameters,
the brackets are still written. Please note that when a variable is passed, a
copy is created, which is then used in the method. So it is not the same
variable that is passed to the method and that is used in the method itself.
result = true;
return result;
Note that the example also accesses methods, ToLower () , which provides
each string variable. This returns the content of the variable stringB in lower
case letters, so that "Test" becomes a "test". The following line now
compares whether the two strings have the same letters and then gives a
TRUE or a FALSE accordingly via the result variable.
//Code...
bool equal;
Value types are data types that contain the value. Examples of this are the
simple types int, bool or enumerations. If you pass a value type to a method,
the value is also copied. In this case, you can change the variable within the
method as desired, without this affecting the externally transferred variable.
Reference types are data types that do not contain the value, but only a
pointer to another memory location (i.e. the memory address). The actual
content of the variables is only stored in this other storage location. If you
now pass a reference type of a method, it is not the content that is copied
here, but only the pointer. Changing the variable data within the method also
changes the data of the object that was transferred to the method from
outside. A typical example of reference types are instances of classes.
To finally confuse you, I want to tell you that string variables are also
reference types. But, and that's the good news, strings have a value semantics.
This means that strings behave more or less like value types. Therefore, you
can usually use strings in exactly the same way as int variables or Booleans.
For the previous example method, this means that you can change the value
of stringA within the method without affecting the passed value of the
myName variable .
Overloaded methods
In C # there is also the possibility to define two methods in a class with the
same name. This works if the transfer parameters differ. This technique is
also known as overloading a method.
Depending on where you declare your own variables in classes, they only
exist in one function or in the entire class. ƒ Local variables are declared
within a method and only exist in this one method. ƒ Global variables are
declared outside of methods and apply to the entire class.
Prevent confusion with this
Due to the different areas of validity, it can happen that a local variable and a
global variable have the same name. To avoid misunderstandings, C # offers
the keyword this , which refers to your own class instance. If you want to
access the local variable in this case , simply write the name of the variable.
If you want to access the global variable, write this first , then a period and
then the name.
int quantity = 0;
this.quantity + = quantity;
}
Access and visibility
Global variables as well as all methods can be expanded with additional
access modifiers. These determine whether they are only visible within the
class and can therefore also be used, or whether they can also be accessed by
another class or instance.
public enables access from anywhere. Public variables are also displayed
in Unity in the Inspector.
private limits access to your own class.
protected limits access to your own class and all other classes that inherit
from it.
internal limits access to all classes in your own assembly (important for
DLLs).
If you do not specify any of these keywords, variables and methods in C # are
automatically considered private. By the way, classes can only be awarded
public and internal . If the keyword is missing, it is considered internal.
//Code...
}
}
Static classes and class members
There are situations where you B. Programming methods or variables that
should work independently of a created instance, e.g. B. a counter of all
previously created instances of this class. In this case, of course, not every
instance may have its own counting mechanism, the results of which may
even differ. So-called static methods / variables / are used for such
applications. . . or also called general class members. (In contrast, the
conventional methods / variables /... Are referred to as instance members.)
They are declared with the keyword static and are not accessible via an
instance, but directly via the class name. Unity uses this e.g. B. in his math
class Mathf.
//Code...
The Mathf class goes even further. Here the whole class was declared static,
so that the compiler only allows class members. Member is the overall term
for all “things” that can be in a class, for example: B. methods, variables, etc.
A static class is then defined as follows:
//Code...
}
Parameter modifier out / ref
Usually, a variable that is passed to a function is copied, as mentioned above.
This means that a change in value type variables (or with a value type
semantics) within a function has no influence on the variable passed from
outside. However, this may be desirable in certain cases by using the
parameter modifiers ref and out , which pass the variables (regardless of
whether they are value types or reference types) to a method by reference and
not by copying the content. Both the transfer parameter in the method must
be identified with the modifier and the variable that is transferred to the
method.
return result;
In the above case, myName initially has the value "Player" and colliderName
has the value "player". After calling the method, both variables have the
value "player". out is used the same way as ref . The difference between these
two parameters is as follows:
A great advantage is that you now have the option of returning multiple
values from one method. For example, the Unity class Physics uses an out
parameter in the Raycast method . The method sends a test beam from a
starting point to check whether there are other objects in the direction. The
actual return value of the function is only a Boolean and only says whether
the virtual test beam has hit an object. If something has been hit, you can get
further information about the hit object via the out parameter.
RaycastHit hit;
int val = 0;
val + = current;
return val;
int sum;
int damage1 = 5;
int damage2 = 3;
damageArray [0] = 5;
damageArray [1] = 3;
return health;
set {
health = value;
if (health <0)
health = 0;
Pay attention to upper and lower case: The property methods are capitalized
and are public. The variables are in lower case and are private.
int h = 3;
npc.Health = 3; // sets the private variable health to 3
Because the Inspector only displays public variables in Unity, but no property
methods, property methods are often not used in Unity.
Inheritance
An important feature of object-oriented programming is the possibility of
inheritance. That means nothing more than that as a programmer you can
develop a new class that can use another class with all its methods, variables
etc. as a basis without having to rewrite the code. This new class is also
called the derived class. The class from which it inherits is called the base
class. If a class should now inherit from another, a colon is written after the
class name, followed by the name of the base class.
//Code
Even if you may not make much use of inheritance yourself, this is a very
important issue for you as a Unity programmer. Every class that is created in
Unity and is to be used as a component must inherit from the Unity class
MonoBehaviour . This includes everything a class needs to be able to be used
as a component. You can find out more about this in Chapter 4, “Script
Programming”.
Base class and derived classes
If a class is to inherit from another in C #, a colon must be written after the
class name, followed by the name of the base class. Suppose we have a class
person with a public variable name and a private variable points .
The derived class NPC , which should have an additional variable health ,
would then look like this:
Now you can create an object of the class NPC and use both public variables:
NPC npc = new NPC ();
npc.name = "Legolas";
npc.health = 10;
Inheritance and visibility
Even if the NPC class inherits from the base class Person in the example
above , the rules of visibility that I explained earlier still apply. You cannot
access a private variable or a private method of the base class in a derived
class. If you still want to allow access, you can define these members with
the keyword protected instead of private .
This means that the variable points is still not visible from the outside, but
can be accessed in the derived class NPC .
points = 100;
}
Override inherited method
Imagine that you have a basic class person who, in addition to various
variables and methods, also has a method called Shoot . To display this
functionality, a short text should first be output in the console (console tab).
You can do this with the Debug.Log command . While the class Person is
provided with a standard text "Peng!", The class NPC should now say
something else. For this purpose in C # there is the possibility to overwrite
methods. To do this, the method to be overwritten must first be declared as
overwritable in the base class. This is done with the keyword virtual .
Debug.Log ("Peng!");
The derived class can (but does not have to) override this method. The key
word here is override .
public class NPC: person {
Debug.Log ("Tie!");
}
Access to the base class
If you have now overwritten a method of the base class in the derived class,
but now want to access the method of the base class, the question naturally
arises how to do it now. This is made possible by the base keyword . You
access methods, variables etc. of the base class via base .
base.Shoot ();
Debug.Log ("Tie!");
}
Seal classes
Another way of inheritance is by sealing. This means preventing the
inheritance ability of a class. In other words, if you prepend the sealed
keyword in the class definition , no other class can inherit from it.
person.Shoot ();
LetsShoot (npc);
Interfaces
Another feature of the object orientation of C # are interfaces. With these you
define common features of different classes, via which you can address them
again. In this way you can treat different classes equally, which can be very
useful in certain situations. There are general interfaces that have already
been predefined by Microsoft, which are also important for certain
functionalities, and you can define your own interfaces.
Define interface
To define an interface, the keyword is interface used. By convention, the
name of an interface always begins with an I , so in the example above it
would be IShootable . The rest of the interface is similar to a class, with the
difference that the methods are not programmed. Because this only happens
in the classes that implement this interface. The IShootable interface could
then look like this:
using UnityEngine;
using System.Collections;
void Awake () {
weaponController = gameObject.GetComponent
<WeaponController> ();
weaponController.Switch (this);
}
Debug.Log ("Peng!");
The individual areas of the script are not so important at the moment and will
be dealt with in more detail in the other chapters. Nevertheless, I would like
to anticipate what is actually happening here. As soon as the Awake method
is called, the script component WeaponController (whose code will be treated
in a moment ) is searched for and assigned to a private variable. If the UseMe
method is now called, Gun's own script instance is transferred to the switch
method of the WeaponController . Finally, the Shoot method only writes the
text "Peng!" Into the Unity console for demonstration purposes. Later the
code would be programmed here, which would then e.g. B. fires a projectile.
Support from MonoDevelop
The programming environment Mono-Develop helps you to implement an
interface. If you have written the implementation IShootable behind
MonoBehaviour in the example above , move your mouse pointer to
IShootable and press the right mouse button. In the context menu that
appears, go to Refactor / Implement implicit . Then all properties, methods,
etc. are automatically added to your class. All you have to do now is fill it
with life. Of course, you can then remove the exception (error message) that
is inserted by default.
Access via an interface
To access an interface, you can use an interface like a variable type. In the
following script, the interface is therefore used for the parameter definition of
the Switch method as well as for the public variable definition. This means
that any class can be transferred to Switch as long as it has the IShootable
interface and thus also the shoot function.
using UnityEngine;
using System.Collections;
weapon = newWeapon;
}
Namespaces
If we develop the demo game at the end of this book, you will find that quite
a few classes are created during a Unity project. In addition, the framework
that Unity uses for programming in the background also provides many
classes. So that you do not lose the overview and there is confusion, there are
so-called namespaces.Namespaces are organizational structures that structure
related types (classes etc.) and group them into logical units. You can think of
this as a folder on a file system that contains various files. If you now want to
use a type in Unity, the namespace in the project must be referenced and
integrated into the class. Usually Unity takes care of all of this. You only
have to do it yourself if you want to use special types.
You include a namespace at the beginning of a script file with the signal
word using . The following example class includes the
System.Collections.Generic namespace in order to use the List type.
using System.Collections.Generic;
}
If you now look at the interface example again, you will notice that
namespaces have already been integrated in this way, namely the namespaces
UnityEngine and System.Collections . You can find out more about these two
namespaces in the "Script programming" chapter.
Generic classes and methods
The term generic in object-oriented programming actually only means that
e.g. B. Methods or classes in general, that is, be designed regardless of type.
The type (or method) to be used at the end is then passed to the class (or
method) using angle brackets. In the case of C #, this type to be assigned is
usually symbolized by the letter T in the generic class (or method).
using UnityEngine;
using System.Collections;
public T current;
Wherever the parameter T was used in the generic class, the transferred type
is used later. The variable current will therefore have the data type that you
specify when declaring the instance.
test2.current = 2;
Cunning
A generic type that is often used in Unity projects is the List <T> type . This
works similar to an array, except that objects of this type offer significantly
more functions than normal arrays. You can add as many new elements to
these objects using the Add method , search them with various commands, or
delete specific elements using Remove . You only have to enter the type that
the object should contain when creating a list object. In the following
example, you should therefore pay particular attention to the variable
declaration of myNumbers , but also the inclusion of the
System.Collections.Generic namespace , to which the List class belongs.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
void Start () {
myNumbers.Add (2);
myNumbers.Add (33);
myNumbers.Add (17);
if (myNumbers.IndexOf (33)> = 0)
myNumbers.Remove (33);
// prints a 2
}
Sort list objects
Another strength of the List class is the sorting of the different elements using
the Sort method . However, you should note that not every type can be sorted.
This will only work if the latter has implemented the IComparable interface
from the system namespace . You can find out more about this topic on the
Internet (keyword “IComparable interface”).
Dictionary
Another class from the generic namespace that you should know is the
Dictionary <TKey, TValue> class, or Dictionary for short. It even has two
generic parameters. A dictionary stores so-called key-value pairs. The first
value represents a unique identifier (key) and the second the value of this
key. Each key may only appear once in the dictionary. The next example
shows some basic functions of this class using a simple inventory
management. This shows how you can use ContainsKey to determine
whether this key already exists in the Dictionary, how you can use the Add
method to create a new key-value pair, and how you use the key to access the
associated value to change it .
using System.Collections;
using System.Collections.Generic;
private static dictionary <string, int> items = new dictionary <string, int>
();
if (items.ContainsKey (key))
items [key] + = val;
else
if (items.ContainsKey (key)) {
return true;
else
return false;
else
return false;
-
CHAPTER 2
Script programming
Script programming
After getting to know the most important C # basics for Unity in the previous
chapter, we now come to the actual programming of the scripts. Each script
usually corresponds to exactly one class, which is why the terms script and
class are often used synonymously in Unity.
MonoDevelop
Unity provides the programming environment MonoDevelopmit for
programming the scripts. MonoDevelop is an open source development
environment for software developers, which was developed as part of the
Mono project, an open source alternative to the Microsoft .NET framework.
Unity delivers a specially adapted version that, among other things, enables
easy debugging in conjunction with Unity . Double-clicking a script in Unity
automatically opens MonoDevelop with the respective script. If
MonoDevelop is already open with another script, an additional tab is created
with the respective script. The other script remains open in MonoDevelop, so
it is possible to switch between the scriptsAs a Unity developer, the main
window of MonoDevelop is particularly important, in whose tabs the code of
the various scripts is displayed. The Solution Explorer, which you can find on
the left, is also helpful. This shows you all code files of the Unity project
sorted according to programming languages and enables the opening of
further script files. The storage function of MonoDevelop is particularly
elementary, which you can use File / Save or the key combination [Ctrl] +
Reach [S] . Adjustments that should be available in Unity must always be
saved in MonoDevelop first. We will talk about the debugging
functionalities, which are of course also very important, in the chapter
"Troubleshooting and Performance". If you already have experience with
other development environments and prefer a special one (e.g. Visual
Studio), you can use this instead of MonoDeve-lop. In this case you can save
an alternative IDE in the External Tools area as External Script Editor under
Edit / Preferences .
Help in MonoDevelop
The version of MonoDevelop that comes with Unity has some special
modifications, including an additional help function that leads the developer
directly to the scripting reference of Unity. To do this, select the command
for which you want more information and press On the German PC keyboard
[Ctrl] + [Shift] + ['] . Alternatively, you can do this via the MonoDevelop
menu Help / Unity API Reference . If you only have one class name in
Mono-Develop, such as For example, entering the application and calling the
help will give you all the information about methods and variables that this
class provides.
Syntax error
If you have a syntax error in a script, e.g. For example, if you have forgotten
a semicolon ("parsing error" ) or use a non-instantiated variable, Unity will
display an error message after you save this incorrect script. This appears
both at the bottom of the Unity window and in the console (console tab). If
you click on this message, MonoDevelop will open and you will get to where
the error occurs.
Forwarding of error messages
Clicking on the error message in the console does not always lead to the
location where the developer made the programming error. Should you e.g.
For example, if you forget a semicolon at the end of a line, the compiler may
only recognize the next line as faulty, since the (previous) line of code has
not yet been completed and this new command must not appear at this point.
Especially for beginners, this sometimes leads to long error searches because
the error can be found somewhere else than it is displayed.
Usable programming languages
- You use the main menu Assets / Create and select the desired script type (in
our case C # Script ).
- You use the Create menu of the Project Browser (located above), where you
can also find the script types listed.
- You use the context menu of the Project Browser, which you can access
with the right mouse button. You can also find the script types there via the
Create menu branch .
- You use the New Script option of the Add Component button in the
Inspector. As soon as you select a GameObject in the hierarchy, you will find
it among all added components of the GameObject. The script is created by
default in the root of the project browser and attached to the selected
GameObject.
using UnityEngine;
using System.Collections;
void Start () {
void Update () {
}
}
In the first two lines, the command using two namespaces is integrated. The
UnityEngine namespace in particular is mandatory for a MonoBehaviour
script and must not be removed. If necessary, additional namespaces can of
course be added above. After the namespaces, the class definition follows
with the MonoBehaviour inheritance. By default, each script inherits from
this class, which also provides the start and update methods. You can also
delete these methods if necessary. You can also change or completely remove
the inheritance. Only then are these classes logically no longer usable as
components.
Unity's event methods
You now know how to script in Unity. Now the question remains when the
code in these scripts will actually be executed. The procedure of Unity is the
following: MonoBehaviour provides several methods that are triggered by
certain events or that are called at regular intervals by Unity. Here Unity runs
through all GameObjects and their script instances and executes the
corresponding methods there . I would like to briefly introduce some of these
important methods below. Keep in mind that every script can / may have
these methods, but they do not have to be implemented. On the contrary, it is
better to delete methods if they are not filled with code. Because each of the
following methods is called according to the definitions and accordingly
costs performance, even if they are empty.
Update
The most important method in Unity is the Update method . Most of the code
is programmed here. Update runs throughout the game, each time before a
frame is rendered, that is, before a new frame is drawn on the monitor. Note
that the rendering times of the individual frames can vary. If you want to
program in update code that should be independent of frames, the values
should be multiplied again with the time value deltaTime of the class Time .
This value represents the time between the last and the current frame. For
example, if you add motion directly to a GameObject using the Transform
component. Each time Update is called, the object is moved a little bit
further. You can find out more about Transform in Chapter 5, "Objects in the
Third Dimension" .
void Update ( ) {
}
FixedUpdate
The FixedUpdate method is called in defined time intervals and should be
used if you Rigidbodys want to access (Rigidbodys are responsible for Simu
lose physical behavior). The background is that the Unity physics engine
does not work frame-based, but works in discrete time intervals, i.e. in fixed
time periods. Therefore, before each calculation of the physics in Unity, all
FixedUpdate methods are executed in the MonoBehaviour scripts. You can
find out more about physics in the chapter “Physics in Unity”. The following
example adds a torque to the rigid body of the object in every FixedUpdate
call so that it rotates around the Y axis.
void FixedUpdate ( ) {
}
Change time interval
By default, the time interval in which FixedUpdate is called is 0.2 seconds.
You can change this via Edit / Project Settings / Time / Fixed Timestep .
Awake
This method is executed once, when the script instance is loaded. When
loading a scene, all objects in the scene are initially initialized. Then the
Awake methods of the individual GameObjects are called in a random order .
Since all GameObjects were initially initialized, it is already possible to find
other GameObjects and assign variables.
void Awake ( ) {
}
begin
The Start method behaves similarly to Awake . However, unlike Awake , this
method is only carried out on active instances. If a script instance is
deactivated at the beginning and is later set to enabled , Start is only called at
that moment. In addition, the Start methods are only called after all Awake
methods have been executed, which makes it easy to control the order of
initialization and to delay certain executions. The following example uses the
FindWithTag method to find the player character for theirs Assign a script
instance of the HealthController class to a temporary variable. The next step
is to query the current value of health and assign it to its own private variable.
void Start ( ) {
}
OnGUI
The OnGUI method is part of Unity's own programming- oriented GUI
system. In this method, the controls for this system are programmed. Since
the system does not work frame-based, but rather event-based, where the
events are triggered by user input or internal Unity rendering processes, this
method is also called accordingly. For this reason, OnGUI can also be called
several times per frame. You can find out more about GUI in the chapter of
the same name.
void OnGUI ( ) {
}
LateUpdate
Another method that is not used as often, but is still important, is the
LateUpdate method. This is carried out after all update methods have been
called, but before rendering. This method is therefore often used, especially
for camera scripts. If a camera tracks a target that is moved in the update
method, the camera can also be repositioned in LateUpdate before it is finally
rendered . The following example could be added to one camera so that it
always points in the direction of another Object is directed, more precisely in
the direction of its trans-form component. For example, this could be for the
control of a surveillance camera, which is permanently installed in one place,
but keeps track of the player in the room all the time.
void LateUpdate () {
transform.LookAt (target.position);
}
Component programming
An important principle of Unity is that every GameObject has its own
components. So you are not programming a life management system that
manages the life strength of all opponents, but a script that only manages the
health of an individual opponent. This script is then added to each opponent
again, so that each opponent has its own administration, and this principle
can (and should) sometimes not be adhered to in certain situations. But first
of all, you should make sure that each GameObject has its own components.
Incidentally, this also makes a big difference in access to the components,
since Unity often offers simplifications for access to its own components,
which you will also see below.
Access GameObjects
Accessing a component's own GameObject is very easy. Simply use the
lowercase variable gameObject for this . This makes it very easy to access
components of your own GameObjects, which I will talk about in a moment.
To access other GameObjects, there are several methods that you can use
both at development time and at runtime. The first method, which is also the
most gentle on performance, is the Inspector assignment, in which you create
a public variable to which you can drag and drop the other GameObject
during development.
void Update ( ) {
//Code...
if ( player! = null) {
//Code...
}
But you can also access other GameObjects at runtime. If you want to access
them more often, make sure that you save them in variables. This brings an
enormous performance advantage, since Unity has to run through all
GameObjects of a scene in the worst case to find the right one. If you then
also make this access in an update method, this would happen in every frame.
FindWithTag
You can find a GameObject by its tag. To this end, the class GameObjectdie
method FindWithTag available. If there are several GameObjects with this
tag in the scene, one is selected at random.
}
FindGameObjectsWithTag
If there are several objects with the same tag, but you also want to collect
them all in an array, you should use the FindGameObjectsWithTag method .
As a return value, you will receive a GameObject array of all objects found
with this tag.
GameObject [ ] enemies;
void Awake ( ) {
}
Find
You can find a GameObject by its name. The static method Find of the class
GameObject is suitable for this.
GameObject leftArm;
You can also further specify the search query by also entering the parent
objects of the GameObject you are looking for. Separate the different
GameObjects with a slash character [/] . To show that an object is the last one
and does not itself have a parent object (root object), you can do this with a
preceding slash.
leftArm = GameObject.Find ("Bob / Left Arm"); // Left Arm has a parent Bob
leftArm = GameObject.Find ("/ Bob / Left Arm"); // Bob is the root object
Activate and deactivate
GameObjects
Each GameObject offers the SetActive method . You can use this to activate
and deactivate a complete GameObject. Give the method the state you want
to achieve.
void Start ( ) {
gameObject.SetActive (false);
Note that you can only activate a deactivated object if you already have
access to it (e.g. via an Inspector assignment). With the find methods
presented above, you have no chance of finding a deactivated object
Destroy GameObjects
To completely destroy a GameObject, you have to pass the object to the
Destroy method .
void DestroyPlayer ( ) {
GameObject player;
Destroy (player);
You can also give the Destroy method a float parameter that delays the
destruction by the respective seconds.
void Start ( ) {
if ( player! = null)
player.position = Vector3.zero;
}
GetComponent
Each GameObject has the GetComponent method , with which you can
access all components of the respective GameObject. If you write this call
without a GameObject, the method looks for this component in your own
GameObject. The following example searches for the HealthController of the
player as well as its own and assigns two different variables in order to use
them later.
HealthController playerHc;
HealthController myHc;
void Awake ( ) {
GameObject player;
void Update () {
if (playerHc.health> 0) {
//Code...
}
Reduce traffic
Be sure to reduce GetComponent accesses as much as possible. For more
frequent access, save the components in variables instead.
SendMessage
A very interesting way to call a method is the SendMessage method .
SendMessage is provided by the GameObject class and runs through all
MonoBehaviour scripts of the GameObject, calling every method with this
name. So you don't even need to know which script it is in. Optionally, you
can also give SendMessage a transfer parameter as well as an option
parameter of the type SendMessageOptions . The latter determines whether a
recipient method is absolutely necessary or not. If one is necessary, but none
is found, an error is triggered.
The following example script could be attached to any weapon or trap with a
collider to cause damage on contact. The script tries to call a method called
ApplyDamage in the other GameObject and transfer the damage value 1 to it.
For this purpose, the GameObject of the passed collision parameter is
accessed and SendMessage is called.
using UnityEngine;
using System.Collections;
audio.Play ();
Destroy ( this, 2); // destroy your script instance with a 2 sec delay
Activate and deactivate components
Instead of removing a component completely straight away, it can also make
sense to just deactivate it. The advantage is that you can reactivate it later. A
classic example is light. The following example accesses the Light
component of your own GameObject and uses the logical negation operator
(the exclamation mark) to reverse the state of enabled. The effect is that the
switched on light is switched off and the switched off light is switched on.
void Switch ( ) {
light.enabled =! light.enabled;
}
Random values
Random values are an important element in making games interesting. The
application options range from assigning a random parameter value such as
strength to generating and placing GameObjects to creating entire levels. For
this, Unity offers the Random class , which offers several interesting methods
for generating random values. The most common is the Range method .
Please note that there are two different versions.
- Range for float values generates a random value that lies between the start
and the end value. The start and end values can also be returned as results.
- Range for int values generates a random value that ranges from the start
value, but must be smaller (!) Than the end value.
using UnityEngine;
using System.Collections;
void Start () {
transform.position = pos;
}
The class offers a few more variants of the random calculation. How to
achieve a random rotation of the type Quaternion via the static variable
rotation or with the help of insideUnitSphere you get a random value in
Vector3 format, in which x , y and z each have a random value between –1
and 1.
using UnityEngine;
using System.Collections;
void Start () {
}
Execute code in parallel
With Coroutines, you have the option of executing methods that are executed
in parallel to the actual Update and LateUpdate calls. While normal code,
which is in the update method, runs in every frame, you can program code
blocks with Coroutines that extend over any number of frames.
IEnumerator Countdown ( ) {
message = "3";
message = "2";
message = "1";
message = "Go!";
}
The example in Listing shows a countdown that waits four times a second
and then assigns new content to the message variable (which is then
visualized in the GUI, for example). To start such a coroutine, use the
command StartCoroutine .
void Start () {
IEnumerator FrameTest ( ) {
Debug.Log (Time.time);
yield return null;
The WaitForSeconds method used above extends the code suspension so that
it does not continue in the next frame, but only after a second.
WaitForSeconds
With yield return WaitForSeconds (2.5F) you can interrupt the execution of a
coroutine for a certain time. The passed float parameter specifies the waiting
time. Please note that an interruption with WaitForSeconds can only be as
precise as the frame duration. So if every frame lasts 0.02 seconds, the
accuracy of WaitForSeconds cannot be better.
Delayed and repeating function calls
with Invoke
Unity provides you with options so that you can delay other methods within a
MonoBehaviour class and call them repeatedly. For this Unity offers the so-
called Invoke commands.
Invoke
The Invoke method expects the name of a method to start and a second value
that indicates the delay. The code continues to run normally after the
command has been executed. The specified method is only triggered after the
time has been reached. The specified method must be in the same script. You
can use IsInvoking to find out whether a method has already been called with
a delay via Invoke (more on this in a moment ). The following example
executes the DelayedMessage method with a delay of 2.5 seconds after
execution of the Start method. This assigns a welcome text to a variable, the
content of which could in turn be displayed in the GUI.
void Start () {
void DelayedMessage ( ) {
}
InvokeRepeating, IsInvoking and
CancelInvoke
InvokeRepeat works in a similar way to Invoke , except that this method also
calls the specified method not just once, but as often as you like. You pass the
interval of the repetition as the third parameter. The second one also
determines the delay of the first execution. You can also use the IsInvoking
method to check whether a method has already been initiated with an Invoke
command. You can then cancel these statements with CancelInvoke . You
pass the name of the method you want to cancel. If you don't pass any, all
methods that you started with an Invoke command are aborted. The following
example could come from a Spawner script that creates a new object at a
random position every second three seconds after the game starts.
void Start () {
void Update () {
// Checks whether RandomInstantiation is still running
if (IsInvoking ("RandomInstantiation")) {
// RandomInstantiation is to be canceled
void RandomInstantiation ( ) {
Change the company name and product name in the Player Settings ( Edit /
Project Settings / Player ). By default, Company Name is set to "
DefaultCompany" and Product Name to the name of your project.
save data
PlayerPrefs provides three static methods to save values as int, float or string.
All three methods expect two parameters. Pass the first parameter a string
that represents a unique identifier, i.e. the key (the ID or the name of the
value). The second parameter is then the actual value in the particular data
type.
There are also three static methods for loading the data. You also have to
hand over the key here.
if (! PlayerPrefs.HasKey ("Lifepoints"))
PlayerPrefs.DeleteKey ("Lifepoints");
PlayerPrefs.DeleteAll ();
Save
On some platforms, such as the web player, Unity does not write the values
to the hard drive when calling the methods SetInt , SetFloat and SetString .
Instead, Unity temporarily saves them temporarily. Only when you exit the
application, the web player that would be the proper closing the tab or the
browser, these values are only to disk geschrieben.Da to Unity in the
background taking care of everything that makes this behavior for First no
difference. Only in the event of an error, e.g. For example, if the browser
crashes, the values may no longer be able to be written back in time. For this,
Unity now provides the Save command, which you can use to force the actual
saving.
PlayerPrefs.Save ();
Cross-scene data
GameObjects actually only exist within a scene. If a new scene is started, all
GameObjects of the previous scene are destroyed, loading a new scene is
done with the LoadLevel method of the Application class. You pass this
either the name or the level index of the respective scene. You define these in
the Build Settings, which you can access via File / Build Settings . You can
find out more about this in the chapter “Creating and publishing games ” .
Application.LoadLevel (1);
However, if all GameObjects are now destroyed, this of course has the
disadvantage that data such. B. Experience points, strength of life, etc. are
lost when the scene changes. After all, they are saved in scripts that are
attached to a GameObject. There are, of course, solutions to this dilemma,
even several. I would like to introduce two of these approaches to you.
Passing values with PlayerPrefs
You have already got to know the first approach: PlayerPrefs. You can easily
all the values that you want to pass, they simply load when exiting a scene
with PlayerPrefs abspei-manuals and at the start of a new scene and the
variables again zuweisen.Hierfür one option would be storing the values in
the OnDestroy - method provides every MonoBehaviour script. If the objects
and components are then destroyed when changing to a new scene, the data
of a script is written away here. In the start - or Awake method they can then
read back werden.Hierzu a small example: The following script has a variable
health whose aktuel-ler value is stored methods PlayerPrefs-on exit with. If a
new scene is then started, the script reads this value back into the variable
from the PlayerPrefs in the start method . Because the key will not yet exist
when the script is called up for the first time, a check is carried out to prevent
errors before reading in whether this key actually exists.
using UnityEngine;
using System.Collections;
void Start () {
if (PlayerPrefs.HasKey ("Health"))
void OnDestroy () {
}
Use start menus for initialization
There are a few points to consider when using the above script. For example,
when the game is ended and restarted, the variable is always assigned the
value of the last game. This may be desirable when continuing an old game,
but certainly not when restarting the game. To prevent this from happening,
there is a simple solution: You can do this in an upstream scene, e.g. B. a
start menu scene, the keys with the initial values. This allows you to use the
above script even without the HasKey query. You can also use the start menu
to ask the player whether they want to continue an old game or start a new
one. Such a script could then look like this:
using UnityEngine;
using System.Collections;
void OnGUI ( ) {
if ( GUILayout.Button ("New")) {
Application.LoadLevel (1);
}
if (PlayerPrefs.HasKey ("Health")) {
if (GUILayout.Button ("Continue")) {
Application.LoadLevel (1);
For example, the above script could be used in the scene with index 0, with
the previous HealthValue script being used accordingly in a scene with index
1.
Prevent destruction
The above procedure has some disadvantages. So it can be quite expensive if
you want to save all parameters of a customizable GUI, a player or an
inventory. But at the latest when you want to transfer textures and materials
from one scene to another, you have a problem here. For textures you can not
handle the PlayerPrefs abspeichern.Hierfür the parent Object class has the
static method DontDestroyOnLoad which prevents the destruction of any
object during charging. Since the transfer parameter is also of the Object type
, you can transfer it to any object (see "Unity's inheritance structure" ), even if
a GameObject is usually passed here. The following example script prevents
the destruction of the GameObject, which the following script is appended,
and all of its components. If the scene is ended and a new one is started, the
entire GameObject is transferred to the next scene. This course of Wertder
variables remain life points received.
using UnityEngine;
using System.Collections;
void Awake ( ) {
DontDestroyOnLoad (gameObject);
}
//Code...
Note here that at the start of the next scene, the Awake - as well as the start
will not run method again. If you use this, you can also use the
OnLevelWasLoaded method , which also receives the current level index.
//Code...
}
DontDestroyOnLoad as a singleton
With DontDestroyOnLoad you prevent the passed object from being
destroyed when the scene changes. However, if you do not switch to a new
scene at all, but maybe just start the same scene again (e.g. because the player
died), the object suddenly exists twice. To solve this problem, the so-called
singleton design pattern can be used be used. This ensures that there can only
be one instance of a class. A static variable is used for this, which stores the
first instance that is generated by the class. If the variable is already assigned
to another instance, it is simply destroyed (in our case, we want to make sure
that the entire GameObject is destroyed ). In addition , either the variable
itself or a property is often made publicly available in Unity easier to access
this instance from the outside. The above MonoBehaviour script could then
look like this as a singleton:
Debug class
Unity has a Console window that displays Unity error messages and
warnings. Unity offers the possibility to output informational texts and
similar ones there. The Debug class offers some useful functions for this, the
main function of which is the Log method . This outputs any text in the
console.
void Start ( ) {
In addition to the normal log method, there are the variants LogWarning and
LogError , which output the messages as warning and error messages, but the
Debug class offers other functions for debugging. You can also use the class
to draw lines in Scene View ( DrawLine and DrawRay ) or use the Break
method to stop the editor. You can find out more about this class in the
Scripting Reference that is supplied with Unity.
void Update ( ) {
}
Compilation order
A special feature of Unity is the script compilation order, which is divided
into four different phases. This is important because from a script you can
only access others that are in the same or an earlier phase , the question
remains how to determine which script is compiled in which phase. This is
controlled in Unity via the folder names in which you store your scripts in the
Project Browser. The names don't really matter, but some of them are
reserved for special purposes, for example Editor. Folders with this name
should only store scripts that also inherit from the class Editor, not from
MonoBe-haviour. You can use these to expand Unity with your own
functionalities , for example. Unity proceeds with the compilation in four
different steps.
1. All scripts are compiled which are located in folders with the names
Standard Assets, Pro Standard Assets and Plugins.
2. All editor scripts that are located in folders named Editor and in the above
folders Standard Assets, Pro Standard Assets and Plugins are compiled.
3. All other scripts that are outside of folders named Editor are compiled.
4. Finally, any remaining scripts (in folders named editor) kompi- liert.Weiter
, there is the reserved folder names WebPlayerTemplates whose scripts
contained are not compiled.
If you want to use your C # script to access a Boo or JavaScript class, the
script you want to access must have been compiled in an earlier phase than
itself. In this case, you could use the JS script in the Move the plugins folder
and it is already accessible from your C # script.
However, this creates a problem: you can only access in one direction, it is
not possible to access in both directions. You should keep this in mind when
working with several programming languages.
Execution order
Sometimes it is important to make sure that one script is executed before the
other, with two scripts you can often still use Update and LateUpdate , but
with three scripts this is no longer possible. For this there are the Script
Execution Order Settings, which you can find under Edit / Project Settings /
Script Execution Order .
You can add a script by clicking the plus sign. You can then leave the script
there or drag it up and change the time value. The scripts are then processed
from top to bottom. All scripts not defined here are called in the Default Time
area in a random order.
CHAPTER 3
Create a simple game
So far we have come a long way towards completing the mechanics of the
game, where we have a scene where we can wander and zoom in and out of
the camera, in addition to the possibility of building a stage within this scene
using the building blocks and forms of monsters that we made. The next step
is to make a slingshot to launch projectiles, in addition to the projectiles
themselves for which we will use the animal images in the Kenney.nl \
AnimalPack folder. These animals are shown in the following picture:
We have to build a extruded mold for each of these four. This template will
initially contain the Sprite Renderer component that has become known to us,
as well as the Rigid Body 2D and Circle Collider 2D components. These
components can transform each image into a physically active object.All we
need to do is adjust the mass values of the solid body component to 5 for
each of these images.In addition, to activate the Is Kinematic option, which
prevents the solid body from responding to external forces, which we will
need to change. Later. The large mass is necessary to make these projectiles
have a noticeable effect when they hit the building blocks or the opponents
upon launch. After that we start to write and add the necessary applets for
these projectiles. The beginning will be with the main applet and most
important is Projectile. The following narrative is described:
using UnityEngine;
using System.Collections;
// The number of seconds the projectile will live in the scene after its launch
public float lifeSpan = 7.5f;
// Has the player grabbed this projectile and prepared it for launch?
private bool held = false;
// Allows the player to control this projectile provided that it has not already been fired
public void AllowControl ()
{
if (! launched)
{
controllable = true;
}
}
// Called at the beginning of the player to hold the projectile in preparation for launch
public void Hold ()
{
if (controllable &&! held &&! launched)
{
held = true;
holdPosition = transform.position;
// Send a message telling the player to catch the projectile
SendMessage ("ProjectileHeld");
}
}
// Carry out the special attack of this projectile after its launch
public void PerformSpecialAttack ()
{
if (! attackPerformed && launched)
{
// Allow your attack only once
attackPerformed = true;
SendMessage ("DoSpecialAttack", SendMessageOptions.DontRequireReceiver);
}
}
// Drag the projectile to the specified location provided it is manageable by the player
public void Drag (Vector2 position)
{
if (controllable && held &&! launched)
{
transform.position = position;
}
}
// Tell if the player is currently holding the projectile in preparation for launch
public bool IsHeld ()
{
return held;
}
Note that the only general variable in this applet is lifeSpan, which
determines how long the projectile stays in the scene after it is launched.
Otherwise, we have state variables launched, held, controllable, and
attackPerformed, all of which are private and can only be controlled by
sending messages or calling functions. In addition to this general variable we
have four special variables reflect the different situations in which the
projectile from the beginning of the game until it is launched until it finally
disappears from the scene. These variables are controllable, held, launched,
and attackPerformed and their initial value is false. The different projectile
modes come in the following sequence:
At the beginning of the game, the projectile is placed on the ground next to
the slingshot, and in the meantime remains static and the player can not
control until the turn comes into play. In this case the value of the
controllable variable is false, which prevents the player from controlling the
projectile.
As soon as the projectile turns into the launcher and is placed on the ejector,
the AllowControl () function is called, which changes the controllable value
to true and allows the player to control the projectile. The other three
variables remain false.
Once the player clicks the projectile, the Hold () function is called, which
assumes that the value of the held variable is changed to true. Since this
variable indicates that the player is holding the projectile, it must first make
sure that the player is allowed to control it by checking the controllable value,
it should also make sure that it is not held by checking the value held itself,
and finally must check the value of launched to be sure That the projectile is
not released yet, because the projectile cannot be held after its release. After
these three conditions are verified, the location where the projected player is
held is stored in the holdPosition variable and the ProjectileHeld message is
sent to report that the projectile was caught.
After grabbing the player begins to move the projectile to prepare it for
launch, where he pulls back and down in preparation for launch. While
holding the projectile, the player is allowed to call the Drag () function,
which moves the projectile to a specific location. As you can see, the process
of moving through this function depends on the fact that the projectile is
manageable and currently held, and it should not have been launched.
When the player drops the projectile, the Launch () function is called, which
can be given a numeric value representing the firing force coefficient. This
coefficient enables us to make more than one slingshot with different firing
forces. When called, this function verifies that the projectile is under the
control of the player and that the player is currently holding it, and that it has
not yet been launched. After these conditions are met, the firing force is
calculated by the distance between the projectile's holding position and its
dropping position and multiplied by the function-supplied operator, since
pulling the projectile further would result in greater firing force. The solid
object is then activated again by setting the isKinematic variable to false and
thus reactivating the solid body's response to external forces before adding its
firing force. After the launch is executed the status variables are updated; the
player is prevented from controlling the projectile by changing the
controllable to false and the launch state is activated by changing launched to
true and is also re-held to false since the player is no longer holding the
projectile. Finally the ProjectileLaunched message is sent in order to inform
other applets that the projectile has been launched.
After launching the projectile, one last step the player can take is to carry out
the projectile attack, such as splitting into three smaller, double-speed or
other projectiles. A player can perform this attack by calling the
PerformSpecialAttack () function, which makes sure that the attackPerformed
value is false; this attack is allowed only once. In addition to this condition, it
must be ensured that the projectile has already been launched by checking the
launched variable; this attack can be carried out only after the projectile is
launched. As you can see, this function does not actually execute the attack,
instead it sends a DoSpecialAttack message that another applet will receive
and execute the actual attack accordingly. By separating the recall from the
attack, we continue to work on the principle of separation of interests and
enable ourselves to program more than one type of attack without affecting
the basic program structure.
Unlike these phases, the IsHeld () and IsLaunched () functions enable other
applets to read the values of special variables but not change their value.
Reading these two variables will be of interest to applets whose work
depends on the projectile applet as we will see shortly. Another note is to use
the SendMessageOptions.DontRequireReceiver option when you send the
DoSpecialAttack message, so we do not require a message receiver. The
reason is that this attack is optional and it is OK to have projectiles that have
no special attack.
With this we have identified the basic programmable ballistics, and we have
some small auxiliary programs for secondary functions.
The first applet is ProjectileSounds and is responsible for ballistics sounds.
What this applet simply does is receive the ProjectileHeld constipation
messages and launch ProjectileLaunched and play the selected audio file for
each process. The following narrative illustrates this applet:
using UnityEngine;
using System.Collections;
void ProjectileHeld ()
{
AudioSource.PlayClipAtPoint (holdSound, transform.position);
}
void ProjectileLaunched ()
{
AudioSource.PlayClipAtPoint (launchSound, transform.position);
}
}
The second applet that we will be discussing from the projectile template
applet is the applet for drawing the projectile motion path after launch. The
drawn path will be points, including fixed distances, along the path that the
projectile traveled from the moment it dropped at the launch slingshot to its
last point. Before explaining the applet we will build a template that
represents the point object that we will use to draw the path. To build the
template, just add a new blank object to the scene and then add the
SpriteRenderer component to it. Then click on the browse button for the
Sprite cell in the component as in the image, and scroll down to the bottom of
the window where you will find below a set of default images that Unity uses
to build the user interface. Select the Knob image and then close the window.
Then name the new template PathPoint and reduce its size on the x and y
axes to 0.75:
Now we can write the path drawing applet and add it to the projectile
template. This applet is described in the following narrative:
using UnityEngine;
using System.Collections;
Vector2 lastPointPosition;
void ProjectileLaunched ()
{
// The projectile is just launched so delete the previous track
for (int i = 0; i <pathParent.childCount; i ++)
{
Destroy (pathParent.GetChild (i) .gameObject);
}
AddPathPoint ();
// Update the variable value as the projectile has been launched
launched = true;
}
newPoint.transform.position = transform.position;
// Set the parent object to the point
newPoint.transform.parent = pathParent;
// Store the location of the added point
lastPointPosition = transform.position;
}
}
This applet uses the template we just created in order to draw points along the
path, so we'll need to define this template via the pathPointPrefab variable.
Then, with the pointDistance variable, we can adjust the distance we want
between two consecutive points. Next we need a reference to Path, an empty
object that we have to add to the hierarchy of the scene as the root object.
This object will be the father of all the track points, and it helps us access
them at once to delete them while drawing a new path as we'll see shortly.
Since we will calculate the distance between each two consecutive points as
the projectile moves to draw the path, we always have to keep the location of
the last point drawn. This location is stored in the lastPointPosition variable.
Finally, we know that the path should be drawn only after the projectile is
launched, so we use the launched variable to know whether or not it was
launched.
Remember that when the projectile is launched, the Projectile applet sends
the ProjectileLaunched message, which the PathDrawer receives through the
function of the same name. Once the message arrives, the previously drawn
path (if any) is deleted by deleting all the sons of the empty object, which we
keep a reference to in the pathParent variable. After the deletion is finished,
we draw a point at the launch location by calling the AddPathPoint ()
function, and then the launched value is changed to true.
What the AddPathPoint () function does is to create a new point in the current
projectile location by using the pathPointPrefab template, add it as a son of
the Path object, and then store its location in the lastPointPosition variable.
As long as the projectile object is in the scene, the Update () function will be
called in each frame, but it will not do anything until the launched value
changes to true. If this condition is met, it means that the projectile has been
released and therefore the path must be plotted as it moves; therefore, we
calculate the distance between the current projectile location of
transform.position and the location of the lastPointPosition. If this distance
increases or is equal to pointDistance, then it is time to add a new point to
this and AddNewPoint () is called. The following image represents the
process of drawing the projectile path as it moves:
Special attacks for projectiles
To complete the projectiles we manufacture a special attack that the player
can perform after launching the projectile. This attack has multiple images in
the original game Angry Birds from which we quote in this series of lessons.
We will be satisfied with two examples to illustrate how these attacks are
built. The first is the velocity attack, which we will adopt for bird-shaped
projectiles, which doubles the speed of the projectile, making its impact even
greater when it hits building blocks or opponents. The second attack we will
adopt for giraffe and elephant projectiles is the fissile attack, where the
original projectile is divided into a number of smaller projectiles that can hit
more than one target in different places.
Let's start with the easiest attack, a speed attack. Since the logic of attacks is
quite different from one attack to another, we have to separate each attack
into a separate applet. The only common factor between these attacks is that
they will receive the DoSpecialAttack message that the Projectile projectile
sends when the PerformSpecialAttack () function is called and the conditions
necessary to perform this attack are checked. A speed attack implementation
applet is called SpeedAttack, and what it does is bring in the solid object
component and then double its speed by a certain amount without changing
its direction. This applet is described in the following narrative. Remember
that attack applets should be added to projectile templates.
using UnityEngine;
using System.Collections;
// Consequently, it performs the DoSpecialAttack speed attack that receives the message
public void DoSpecialAttack ()
{
// Bring the rigid body component of the projectile object
Rigidbody2D myRB = GetComponent <Rigidbody2D> ();
// Multiply the speed by multiplying and then set the speed of the object to the new output
myRB.velocity = myRB.velocity * speedFactor;
}
}
using UnityEngine;
using System.Collections;
// The number of seconds each fragment will live before being destroyed and removed from
the scene
public float clusterLife = 4.0f;
Vector2 clusterVelocity;
// With each new fragment we reduce the vehicle speed horizontally and increase it
vertically in order to ensure fragmentation
clusterVelocity.x = (originalVelocity / clusterCount) * (clusterCount - i);
clusterVelocity.y = (originalVelocity / clusterCount) * -i;
The idea of doing this attack is to receive the DoSpecialAttack message and
then create the specified number of fragments using the specified template. In
order to prevent collisions between fragments and some of them and also
between fragments and the original projectile - where a collision can occur
the moment before it is deleted from the scene - we use the
Physics2D.IgnoreCollision () function and provide it with the two collision
components that we want to ignore collisions between. Note that we knew a
matrix of collision components in order to store the components of all
fragments, and when creating a new fragment we pass on the components of
the collisions of the previous fragments and call the function mentioned
between the old and new components in order to negate the collisions. The
next step is the speed of the splinter motion, where we take the amount of the
original projectile's velocity and multiply it each time by a different value to
get the horizontal and vertical components of the new velocity. These two
components change from fragment to another, where the first fragment
begins with a high horizontal and low vertical vehicle, and then these values
begin to change as the vertical value increases gradually downward and the
horizontal decreases, resulting in the dispersion of projectiles in a manner
similar to what you see in the picture below (I have in this picture Increase
the number of fragments to clarify the idea):
Note that the fragments spread apart from the original projectile fission site.
Then we adjust the mass of each fragment to equal the mass of the original
projectile. While it is logical to divide the mass by the number of fragments
in order to distribute them evenly, copying the original mass of all fragments
will give them greater destruction power, giving the special attack its
preferred advantage.
Ejector industry
Let us now turn to the ejector, a slingshot that will launch these projectiles
towards its targets. Unfortunately, our graphics package does not contain a
slingshot image, so we will try to use some wooden and metal shapes to
create a simple shape that looks like it. Here I will use three wooden
rectangles and a stone triangle to make the shape you see in the following
picture. These objects must be placed as sons of one empty object containing
all of them, and the Order in Layer value in the Sprite Renderer component of
the triangle must also be adjusted and made 1, so that it appears in front of
the pieces as shown in the picture:
In addition to the four images we have scaled and rotated to make a slingshot,
there are 3 blank objects that indicate colored lines to their locations. These
empty objects have a software utility that we will see shortly. The LaunchPos
object represents where the projectile is placed before it is launched, and the
RightEnd and LeftEnd objects represent the locations of the ends of the
rubber band that will push the projectiles when they are launched. The
slingshot in its current form is ready to make the initial version of the mold,
to which we will add some applets and other components to it.
The first of these is the main applet that launches projectiles at targets. Let's
get to know this applet called Launcher in the following narrative and then
discuss the details of its functions:
using UnityEngine;
using System.Collections;
public class Launcher: MonoBehaviour {
// The launch force coefficient of this ejector
public float launchForce = 1.0f;
// Maximum length the rubber ejector rope can extend to
public float maxStretch = 1.0f;
// The location where the current projectile will be placed before being held by the player
public Transform launchPosition;
// Current projectile subject on the ejector
public Projectile currentProjectile;
// Have all the projectiles in the scene been fired?
private bool projectilesConsumed = false;
// Called once at startup
void Start () {
}
// Called once when rendering each frame
void Update () {
if (projectilesConsumed)
{
// There's nothing to do
return;
}
if (currentProjectile! = null)
{
// If the projectile was not fired, it was also not caught by the player
// Then bring the projectile to the launch site
if (! currentProjectile.IsHeld () &&! currentProjectile.IsLaunched ())
{
BringCurrentProjectile ();
}
}
else
{
// There is currently no projectile on the ejector
// Find the nearest projectile and bring it to the launch site
currentProjectile = GetNearestProjectile ();
if (currentProjectile == null)
{
// All the projectiles were consumed, send a message telling them
projectilesConsumed = true;
SendMessageUpwards ("ProjectilesConsumed");
}
}
}
if (allProjectiles.Length == 0)
{
// There are no longer any projectiles
return null;
}
return nearest;
}
// You move the current projectile one step smoothly towards the launch site
void BringCurrentProjectile ()
{
// Bring locations where the projectile will move between them
Vector2 projectilePos = currentProjectile.transform.position;
Vector2 launcherPos = launchPosition.transform.position;
if (projectilePos == launcherPos)
{
// Extruded at the launch site actually, no need to move it
return;
}
// Use linear interpolation with elapsed time between frames for smooth movement
projectilePos = Vector2.Lerp (projectilePos, launcherPos, Time.deltaTime * 5.0f);
// Put the projectile in its new position
currentProjectile.transform.position = projectilePos;
if (currentProjectile! = null)
{
// Make sure not to exceed the maximum elastic cord tension
float currentDist = Vector2.Distance (newPosition, launchPosition.position);
if (currentDist> maxStretch)
{
// Change the location provided to the furthest allowed point
float lerpAmount = maxStretch / currentDist;
newPosition = Vector2.Lerp (launchPosition.position, newPosition, lerpAmount);
}
// Place the projectile in the new location
currentProjectile.Drag (newPosition);
}
The general variables in this applet are launchForce, which represents the
launch force, maxStretch, which is the maximum permissible distance
between the projectile and the firing position during the tension (i.e., the
maximum extension of the rubber thread) and launchPosition, a variable to
store the launch site object named LaunchPos, which we added to the
slingshot template when we created it. Finally we have a reference to the
current projectile located on the ejector which is currentProjectile.
Here I will talk a little bit more about the BringCurrentProjectile () function
to explain the mechanism you use to achieve the smooth movement of the
projectile from its current position towards the launch site. The smooth
movement in game engines depends on the elapsed time factor between each
two consecutive frames, which is in the Unity variable Time.deltaTime. In
addition to this variable we will need a function that calculates the Linear
Interpolation between two different values. But what is a linear interpolation?
It is simply a value between two lower and higher limits. This value may be
an abstract number between two numbers, a position between two locations, a
color between two colors, etc. But where exactly is this value between the
two limits? What determines this location is the interpolation value, which is
a fractional value between zero and one. For example, if we want to calculate
interpolation between the numbers 0 and 10, and the interpolation value is
0.6, the result will be 6, and if the interpolation value is 0.45, the result will
be 4.5 and so on.
We calculate the new projectile position as it moves towards the launch site
using this technique, in which case the interpolation takes place between the
minimum projectilePos current and the maximum target we want to reach, the
launcherPos. The value of the interpolation is relatively low ie it is closer to
the target, which is the time elapsed since the previous tire was rendered
multiplied by 5. The importance of using the time value here lies in the fact
that not all tires are rendered at the same speed. Not fixed and may increase
and decrease. Therefore, to maintain a constant movement speed, we have to
multiply by the amount of time which increases as the number of frames per
second decreases and vice versa, making the movement speed that the player
sees constant regardless of the number of frames per second or less.
The idea is to calculate the distance between the launch location and the
current location of the currentDist and compare it to the maximum expansion,
maxStretch. If this distance exceeds the limit, we will divide maxStretch by
currentDist, thus obtaining the required interpolation between the original
launchPosition.position and the current location of the newPosition. This
value will naturally decrease with increasing distance from the cursor to the
launch site, thus maintaining a constant distance from the launch site, which
is the maxStretch distance. By completing the interpolation, we get the
correct newPosition without affecting the smooth motion, and then use the
Drag () function to move the projectile. It is necessary to use this function
and not to move the projectile directly because it verifies the conditions in
terms of the fact that the projectile is held by the player and has not been
launched, which is the movement conditions according to the rules of the
game.
After writing the applet we have to add it to the empty Object Launcher,
which is the root of all the slash objects that make up the slingshot. The next
applet we will add will draw the rubber cord between the ends of the
slingshot and the projectile. But before moving on to the applet we have to
add the component responsible for drawing the line that will represent this
rope. The component we will add is called Line Renderer and can be added
as usual from the Add Component button and then write the component name
as in the following picture. This component draws a solid line between a set
of points assigned to it via the positions array, starting with the first point in
the array to the last point:
After adding the component we have to adjust some of its values: first we
have to change the number of points that draw the positions line to 3 and then
make it thinner by changing both Start Width and End Width to 0.1, and
finally we'll change its color to red at the beginning and end (you can of
course choose Any other color). These settings are shown in the following
image:
Now let's launch the LauncherRope, which is responsible for drawing this
line between the ends of the slingshot and the projectile. This applet is
described in the following narrative:
using UnityEngine;
using System.Collections;
using UnityEngine;
using System.Collections;
void CheckButtonDown ()
{
if (Input.GetMouseButtonDown (0))
{
// The left mouse button has just been pressed
// Is there an existing projectile?
if (launcher.currentProjectile! = null)
{
// Switch the cursor position from the screen coordinates to the scene
space coordinates
Vector2 mouseWorldPos = Camera.main.ScreenToWorldPoint
(Input.mousePosition);
// Extract the collision component from the object
Collider2D projectileCol =
launcher.currentProjectile.GetComponent <Collider2D> ();
// Is your mouse within the range of the projectile's collision
component?
if (projectileCol.bounds.Contains (mouseWorldPos))
{
// Yes, that is, the mouse button was pressed over the projectile
// Hold the projectile
launcher.HoldProjectile ();
}
}
}
}
// Checks whether the player pulls the mouse using the left button
void CheckDragging ()
{
if (Input.GetMouseButton (0))
{
Vector2 mouseWorldPos = Camera.main.ScreenToWorldPoint
(Input.mousePosition);
launcher.DragProjectile (mouseWorldPos);
}
}
***