BGT Language Tutorial Introduction
BGT Language Tutorial Introduction
Conte�dos
Introdu��o
. Como BGT trabalha?
. O Processo de Cria��o de Jogo
. MOTOR de BGT
. COMPILADOR de BGT
. sintaxe de idioma
. Declara��es
. Blocos
. Express�es
. Comentando
. Texto de impress�o na Tela
. Vari�veis
. Vari�veis; O que S�o Elas?
. Declarando e atribuindo vari�veis
. Vari�veis integrais e P�ssegos Ruins
. vari�veis de ponto flutuantes
. Vari�veis de string
. Constantes
. Resumo
. Fun��es
. Instru��es condicionais
. Se declara��es
. switch e case
. Loops
. loop while
. O loop fa�a-enquanto
. O loop for
. Break e continue
. Vetores e dicion�rios
. Objetos e classes
. t�cnicas avan�adas de objetos
. Objetos em um nutshell
. Heran�a
. Interfaces
. Operador
. Handles para fun��es
. Usando escrituras m�ltiplas
. notas finais
1. Introdu��o
Welcome to the BGT language tutorial! This tutorial assumes that you know nothing
about BGT but that you have plenty of motivation to sit down and learn. The
tutorial
will cover everything from the very basics to the more advanced aspects of the
language, and it is my intent that after you've finished reading it you should be
able to dive directly into the function reference and the example games and, of
course, start building things on your own in due time. So put your feet up on the
table, relax, and get ready for... BGT!
Bem-vindo para o tutorial da linguagem BGT! Este tutorial assume que voc� n�o sabe
nada sobre BGT mas que voc� tem bastante motiva��o para se sentar e aprender.
O tutorial
cobrir� tudo do muito fundamento para os aspectos mais avan�ados do idioma, e � meu
intento isto depois que voc� terminou de ler isto voc� devia ser
capaz de mergulhar diretamente na refer�ncia de fun��o e os jogos de exemplo e,
claro, comecem a construir coisas por conta pr�pria a seu devido tempo. Ent�o ponha
seus p�s em cima nos
tabela, relaxe, e prepare-se para... BGT!
void main()
{
alert("Hello", "I am a BGT script!");
}
The above script may appear somewhat confusing at first glance, but it's really not
that bad. Let's dissect it piece by piece.
void main()
This tells the compiler that we are using the main function, which is the first
block of code that is executed when a script starts. We will cover functions later
on, but it is essential that you remember to create this function when making a
script.
{
You may remember from the previous section that a block of code is always enclosed
in braces. A function is one of these situations.
alert(
This is the name of a function provided by the BGT engine, which displays a simple
message on the screen with a title and some text. The alert function has two
parameters, the first one is the title and the second one is the message that is to
be shown. When calling a function you always specify its parameters (if any)
between a left and a right parenthesis, and separate the parameters with a comma.
"Hello",
This is the first parameter that is given to the alert function, a chunk of text
which will be the title of the message.
This is another chunk of text written inside quotes, the second parameter to the
alert function which specifies the actual text that is to be shown. This is also
the last parameter to the function, which can be seen by the right parenthesis that
indicates the end of a function call. The semicolon after the parenthesis indicates
the end of the statement.
This tells the compiler that we have finished our main function now.
When writing out text like this, you always have to surround it with quotes. This
is to make things easier for the script interpreter, as it may otherwise confuse
your text with actual BGT code.
Look at the script one more time, and keep in mind the explanations that you just
read.
void main()
{
alert("Hello", "I am a BGT script!");
}
Hopefully things should be a little clearer now. If you're still in the dark, then
read the above paragraphs over again a few times before you move on just to make
sure you�ve grasped the fundamental concept.
Espero que coisas deviam estar um pouco limpador agora. Se voc� estiver ainda na
escurid�o, ent�o leia os par�grafos acimas de novo alguns tempos antes de voc�
partir s� para fazer
certo voc� pegou o conceito fundamental.
5. Variables
5.1. Variables; What Are They?
If you were awake enough in the math�s lessons to grasp the basics of equations,
understanding variables should be a piece of cake. But for those of you who fell
asleep or, like me, decided that it was much more entertaining to play Hangman in
the back of the classroom, here's a simple explanation that should solve the
mystery.
string your_name;
This tells BGT we want a variable of type string, called your_name. When you come
to read this code back, you will know that your_name is likely to hold the player's
name. Again, we have a semicolon at the end to denote the end of a statement.
We now have a blank variable. To make this variable usable we need to give this a
value. We can do this using one of two methods. If you have predetermined the
contents of the variable, I.E. a message that may be displayed to the user, you may
assign the value on the same line as the declaration, like so:
A variable can be assigned a new value at any point in the current block of code.
Please note that if you don't assign a value to a variable, but instead just
declare
it like:
type x;
type can be any type of primitive variable supported by BGT (see below). Objects
work differently, but this will be covered later. If you have a declaration like
this without an initial value being given to the variable, then it will have an
undefined value. In other words it may be anything, depending on the type of the
variable. It will just contain whatever happened to be stored in that memory
location at the time when the variable was created, and therefore you should never
use a variable when it is not yet initialized with a value. The BGT script compiler
will try to warn you when it sees that you are doing this, but it cannot do
so accurately in every situation. Therefore you should always give your variables
initial values when you create them unless you can ensure with 100% certainty
that the variable will be given a meaningful value before it is used.
Now that we have discussed how to set up our variables, we can learn about the
different types and changing and comparing them.
5.3. Integral Variables and Bad Peaches
Integral variables are those that contain a basic integer, that is to say, a whole
number. These variables do not allow decimal points.
You can perform arithmetic operations on them like plus, minus, times and divided
by. BGT also supports more complex mathematical functions, should you ever need
them. If you're curious, here comes an example of how integral variables can be
used in the real world.
int apples=5;
int bananas=2;
int oranges=8;
int fruit_basket=apples+bananas+oranges;
Here, you see four variables. One called apples, another called bananas, a third
called oranges and finally one called fruit_basket. But when we gave a value to
fruit-basket we didn't actually specify a value directly, instead we added the
values of the three fruit type variables together to get the total number of fruit
in the basket. Let's have fun and look at a slightly extended version of this
example.
int apples=5;
int bananas=2;
int oranges=8;
int fruit_basket=apples+bananas+oranges+1;
Why did I put +1 in the end? Because there was a peach in there as well, but it had
gone bad so I didn't bother to give it its own variable. Also it clearly
demonstrates
how easy it is to mix variables and literal numbers in expressions, which
technically allows you to write extremely powerful formulae if you wish.
Now we're going to elaborate a little bit and do some more calculations with these
variables. Let's say that we want to find out how many fruit that would be in
the basket if only half the oranges were put in it, plus the bad peach. We could do
the following:
int apples=5;
int bananas=2;
int oranges=8;
int fruit_basket=apples+bananas+oranges+1;
int robbed_basket=fruit_basket-(oranges/2);
Now guess what value the variable called robbed_basket will have? That's right, 12.
The basket contained 16 fruit all in total, 8 of which were oranges. We then
took away half the oranges, and so we ended up with 12.
But look one more time at the line where the robbed_basket variable gets its value.
See that we have put part of the calculation between a left and a right
parenthesis?
In this case it does not mean that we're calling some kind of function, here it
simply has the same purpose as in regular arithmetic which is to make sure that
the expression is calculated in the right order. This way, you can compute advanced
mathematical expressions and be sure that the result is always what you expected.
That is assuming that you wrote the expression correctly to begin with, which is
slightly outside the scope of this tutorial.
I am going to give you one last example of how numeric variables can be
manipulated. This one has no practical use; it is merely meant to show the
flexibility of
the numeric variables, and how they can be used to perform calculations not allowed
in regular math. Take a look at the following...
int x=3;
x=x*3;
int y=8;
int z=x/y;
z+=x-1;
x*=z+4;
As you can see, just a bunch of meaningless calculations that do not serve any
other purpose than to attempt to demonstrate some of the possibilities you have
when
working with numbers in the BGT language.
One new thing that you will notice, however, is the use of += and *=. This means
that, in the case of *= the value on the right side in the mathematical expression
should be taken times the current value of the variable. So for instance:
int x=5;
x*=3;
Now x will have the value of 15, since 5*3 is 15. You can use all the numeric
operators which is to say +, -, * and / in the same way.
There is one additional arithmetical operator you may wish to use, the Modulus
operator. This is represented by a percentage sign (%), and can also be used with
the = sign. The Modulus divides two numbers and gives you the remaining value. For
example:
int x=11;
x%=3;
There are two further time saving operators you may wish to use. These are known as
the autoincrement and autodecrement operators. These are ++ and -- (two plus,
or two minus signs). These are good for loop counters, etc.
When we write the following line:
x++;
We are telling BGT to increment the counter by 1. This does two things. It saves
time for you as the writer, and speeds up the program. If you are increasing one
variable once, you may not notice the change. If, on the other hand, you are
constantly changing a counter, you will notice a difference. Although an integral
variable
is one type, you can specify the potential permitted range of the variable. The two
you will usually be interested in are short, a 16 bit (2 byte) integer ranging
from -32768 to 32767, and long, a 32 bit (4 byte) long integer, ranging from -
2147483648 to 2147483647.
In other situations, though more occasional, you may wish to use unsigned integers.
These are integers which do not allow negative numbers, therefore doubling the
maximum limit.
A full list of keywords relating to integers follows:
� int8: An 8 bit (1 byte) integer, ranging from -128 to 127.
� int16: A 16 bit (2 byte) integer, ranging from -32768 to 32767.
� int32: A 32 bit (4 byte) integer, ranging from -2147483648 to 2147483647.
� uint8: An 8 bit (1 byte) unsigned integer, ranging from 0 to 255.
� uint16: A 16 bit (2 byte) unsigned integer, ranging from 0 to 65535.
� uint32: A 32 bit (4 byte) unsigned integer, ranging from 0 to 4294967295.
� int: Alias of int32.
� short: Alias of int16.
� long: Alias of int32.
� uint: Alias of uint32.
� ushort: Alias of uint16.
� ulong: Alias of uint32.
5.4. Floating point variables
Floating point variables are very similar to the variables that you are familiar
with if you've worked with equations, they contain a number. No more, and no less.
The floating point variables can be with or without decimals.
All floating point variables are signed, meaning they can be positive or negative
numbers.
Supported types are:
� Float: Single precision 32 bit floating point
� Double: Double precision 64 bit floating point
If you do not know what values will be going in your variable, it is probably best
to use doubles, since they support the widest range, allowing positive and negative
numbers, and integers and real numbers, I.E. numbers with decimals.
One other thing to mention is that, if, for any reason, the value of a numeric
variable attempts to go over its limit, the compiler will issue a warning and the
variable will reset to the minimum supported value, so it is essential that you
keep track of any changes that occur to your variables.
5.5. String Variables
String variables are very different from the numeric ones that we just explored.
They are very useful however, and we shall soon see why. A string variable can
contain text, which is to say a string of single characters that will form
something interesting; hence the name, string. A string can hold the name of the
player,
a path on the harddrive or the content of a file, or anything else you like. When
using string values you must always surround them with quotes, so that the
interpreter
doesn't get confused as to what is BGT code and what is part of your string. Does
that ring a bell? That's right... The alert function that we used in our very
first example works with strings!
I think the best way to get the hang of strings, is to see them in action. So here
we go...
As you see here, the first parameter to the alert function is still given in the
same way as we did in the first example, but the difference comes in the second
parameter. Instead of specifying something between quotes to display in the message
box, we gave the name of the string variable that we just made. The result?
That "John Doe" is printed out in the message box with the title "My name is". Yes,
it's that simple!
Now why did we not surround my_name with quotes in the second parameter to alert?
Because that would print out "my_name" literally rather than taking the value
of a variable called my_name, and we don't really want that.
Let's try doing something more fancy with strings shall we?
What? What on earth is this " + my_name + " stuff? Well, " + variable_name + "
simply inserts the value of a variable into a string. So the printout in the
message
box will be:
And as you may have guessed, the variable between the two plus signs does not
necessarily have to be another string. Let's illustrate this with an extended
example...
My name is John Doe, I'm 23 years old and don't you ever forget it!
Naturally the printout will be the same, the script is just one line shorter.
Let's have some fun with the John Doe example code, and make it do something
slightly more interesting. Look at the following snippet and see if you can figure
out what it does.
You guessed it. The random function which is also provided by the BGT engine,
generates a random number in a range that you specify. In this case, we requested
a number between 5 and 50 which means that John Doe will tell us a different age
every time we ask him. The first time I ran this on my machine it said that John
Doe was 25, the second time it assured me that he was 36 and the third time it
claimed that he was 18... Talk about a pathological liar!
Leaving John Doe to contemplate his proper age in peace, we will look at yet
another example of how strings can be added together.
The above example shows how simple it is to add strings together, both variables
and literal values. While string1 and string2 are literal values stored in
variables,
string3 is the result of those two variables added together and string4 is the two
variables added together again but with a literal value in the middle. You can
combine variables and literal values in any way you like, which is quite useful in
a large number of applications as you will undoubtedly discover if you use the
strings in BGT often.
As we saw in the previous section, one could do the following with a numeric
variable:
int x=10;
x+=15;
Which of course gives x the final value of 25. Now, you'll be happy to know that
you can do the same thing with strings. The following code is perfectly legal:
Of course, my_string will now contain the text "Hello there my friend.". You
cannot, as you may have guessed, use any of the other numeric operators like -, *
and
/ in the same way for strings as they would not fill any function in this case.
There is also another important thing to know about strings. In order to be able to
use special characters, we must use what is called an escape character. This
is a designated character that tells the compiler that the next character should
not be treated as a literal character, but rather as a code for the character that
should be used. The escape character used in BGT is a backslash.
The following is a list of the characters to be used after the escape character and
what they truly represent:
� \=\
� "="
� n=new line
� r=return
� t=tab
string string1="this\tis\ta\tstring\tusing\ttabs\tinstead\tof\tspaces.";
string string2="this is a\r\nmultiline\r\nstring.";
string string3="My current directory is \"c:\\program files\\bgt\\my_game\\
my_script.bgt\"";
Notice how the multiline string uses the r and n characters to create a new line.
This is the character sequence used by Windows.
5.6. Constants
A constant is basically the same as a regular variable, with one main difference.
Once a constant is assigned a value it cannot be changed at runtime. Its value
remains the same throughout the program's lifetime, hence the name constant, as
opposed to a variable whose value can change, i.e. vary, at any time.
The purpose of a constant is mainly for readability, and to save time.
You declare a constant the same way you do with any variable, with the addition of
the keyword const before the data type.
Take the following example:
The advantage here is, if your game got a lot larger and you decided to convert to
the ogg format, you simply change the snd_ext constant assignment to ".ogg".
Then all your sound assignments, if they use the constant, are changed to that
value without having to go and change each individual sound.
Also, the BGT engine comes with certain constants. In the documentation
accompanying each function there is a mention whenever a certain constant is meant
to be
used. The constant is then passed to the function by its name, however what is
really happening is that a certain value such as a number or a string gets passed
behind the scenes. You only use the constant name to make things easier for
yourself, you could just as well have written out the number itself and still ended
up with working code. However, when opening a file in writing mode for instance,
the name "file_write" makes a lot more sense than the number 2.
There is also a special type of constant called an enum. This is short for
enumeration, and therefore can only hold numeric, integer values. These are useful
for
grouping constants together, enabling better code management. Here is an example:
enum movement
{
left=-1,
right=1
}
enum weapon
{
fist,
club,
slingshot,
gun,
bomb
}
What we have done here, is to create two sets of enumerations, one called movement,
one called weapon. In each enumeration we have supplied the interpreter with
a comma separated list of constants and, in the case of our movement example, their
corresponding values. For example, we assigned the name left to -1, and right
to 1. In the case of weapon, however, we gave no values. This is because the
interpreter can automatically assume our values. If no values are given, the first
constant will be assigned value 0, and every one after that will be increased by 1.
This is useful here, since we can concentrate purely on deciding what weapons
we want. It also has the added bonus that if we want to add an extra weapon in the
future, there is no number juggling to do.
Please note that enums have to be defined globally; they cannot be defined inside a
function.
5.7. Summary
Let's take a quick look at what we've covered in this chapter, along with some
additional hints.
� Variables are like baskets that can hold data of different types.
� A constant is a variable whose value cannot be changed at runtime.
� Integral variables can hold positive or negative numbers, depending on whether
you declare them as signed or unsigned, but only without decimals.
� Floating point variables can hold positive or negative numbers, with or without
decimals.
� You can perform arithmetic operations on numeric variables such as 3+5=8, both by
using variables referenced by their name as well as literal values.
� If you do not know the values that will be stored in a numeric variable it is
best to use a double.
� Always keep track of your variables, or you may end up with unexpected results if
the limit is exceeded.
� Strings are a list of characters that form something useful.
� You can assemble strings to form new ones by combining variables referenced by
their name, with literal values in quotes.
� Adding two numeric variables together, both of which with the value of 1, will be
2.
� Adding two string variables together, both of which with the value of "1" (do
notice the quotes), will be 11.
� Variables are one of the core fundamentals of game development, and programming
in general for that matter.
� Variables and constants in BGT are case sensitive. Therefore, you have to refer
to them exactly how you declared them or how they appear in the documentation.
6. Functions
We have already seen several examples of calls to functions. In our very first
example we called the alert function when we wanted to print a message on the
screen.
Now, I think it's time to go a little more in depth in regards to functions as they
are used in literally every game on the market. A function is a chunk of code
that can be referred to by a name. For instance, you could have a function called
jump which makes the player, yes, jump. Whenever you wanted the player to jump
you would then call this function and the code inside it would be executed.
Functions are a great help because they not only allow you to structure your
program
in a more logical way, they also allow you to reuse the same section of code
multiple times without having to actually write or paste it each and every time you
want it to execute.
All the functions that we have been calling so far have been part of the BGT engine
itself, but you can also make your own functions to accomplish things. In actual
fact, there is one function that you must create before the engine will even accept
your code. The main section of your code goes here. This is where additional
functions are called, either functions from the BGT engine, from other scripts that
are included, or from your own functions contained in the current script. Because
this function is where all your main code is stored, this function is called main.
We had a brief look at this function when we discussed printing a message to
the screen.
As usual, we will see how it's done in practice before you are showered yet again
with a new large chunk of theoretical jibberish.
void main()
{
alert("Test", "We are now using the main function.");
}
You do not need to call this function yourself, since the engine uses this function
as a starting point.
The word "void" indicates that a function has no return type. We will discuss
returning later on in this chapter. You then have the name of the function and
finally
a list of parameters between parentheses. The main function takes no parameters,
and so the parameter list is empty. Inside the function we just display another
alert box and then we tell the BGT interpreter that our function is finished by
using a right brace character. It's that simple! Almost...
void main()
{
int x=add_numbers(3, 5);
alert("Wow", "3 + 5 is... " + x + "!");
}
int add_numbers(int first, int second)
{
int result=first+second;
return result;
}
This example introduces several new things. First of all it shows how you can
receive parameters in your functions. You do this by specifying variable names when
you create the function, in this case I chose first and second for the two numbers.
Then, whenever you call this function the two parameters that you pass to it
are placed in the two corresponding variables. This means that the variable called
first receives the first parameter, and the one called second receives the second
one. Of course, you may name your function parameters anything you like as long as
they are legal variable names. We also see how a function can give a value back
to the caller by using the return statement. The return statement will interrupt
the function completely and return the value or variable given after the space,
which the caller can then capture by assigning the return value of the function to
a new variable as we saw in the very beginning of the example. In our case we
used the variable x to get our value back from the function, and then we printed it
out in a normal alert box.
You may have also noticed that we did not start the function declaration with the
word "void", but with the word "int". The word void is a keyword only used at
the start of functions to tell the engine that this function does not return any
data, though the return keyword can be used on its own to break out of a function,
I.E. prematurely terminate its execution. Because we return the result of an
addition sum, we tell the engine that we wish to return an int.
A function declaration starts with the required data type for the return, as
covered in variables, with the additional keyword of void. We then give our
function
a name. Straight after that, are our parameters, separated by commas and enclosed
in parentheses. You then open a brace, as a function is a block of code.
You can not only return variables from a function, literal values are perfectly
fine as well. We show this, of course, with another example.
void main()
{
string x=string_magic();
alert("Result", "The string given is: " + x + ".");
}
string string_magic()
{
return "wow";
}
void main()
{
alert("Wow", "3 + 5 is... " + add_numbers(3, 5) + "!");
}
int add_numbers(int first, int second)
{
return first+second;
}
The output is exactly the same as before, our script is just two lines shorter. The
only thing we have done is to remove two variables that were not quite necessary
but probably made things a little easier to follow. Instead of assigning the return
value of the function to our x variable we print it directly in the call to
the alert function. So as you can see, it is no problem having a function call
inside another function call. You may nest as many function calls as you wish,
until
it reaches 10000 calls at which point the BGT interpreter won't be very happy with
you. We will take a look at the string_magic function example once again, but
with yet another twist. We will not include the main function in all examples, just
as long as you know that any code that doesn't belong in another function goes
in void main.
//main function
alert("Result", string_magic() + string_magic() + string_magic());
string string_magic()
{
return "wow";
}
Yes, I know, pointless again. The output from this script is "wowwowwow". It just
shows yet another way in which you can call a function from inside a call to yet
another function and so on. It is also okay to call a function inside the body of
another function. A function can even call itself, but be careful with this as
it may cause a so called stack overflow where too many nested function calls are
done on top of each other if you write your script incorrectly. The limit for this
is, as mentioned previously, 10000 nested calls.
All variables that are declared at the top of the script before the first function
appears, are considered global ones. They can be accessed from inside any function
throughout the script. However, new variables that are declared inside a function
are local only to that function. This means that if you have a local variable
that stores some information and then the function exits, that variable will be
lost unless it is used as the function's return value. Thus, any information that
you wish to access in more than one function should be stored in one or more public
variables unless you wish to pass them as parameters between all your functions
which is not generally recommended.
You should know by now that any big chunk of theory is usually followed by an
example, so here we go.
//Now we are modifying the global variable my_name from inside this function.
my_name="Rodney";
}
I grant you that this was a very silly example, but it serves its purpose none the
less. As you saw, all the variables that were declared at the top of the script
were accessible from inside our function while the ones declared inside it are only
accessible from the function itself. Thus, it would not have been legal to modify
the value of number_of_pets inside another function unless, of course, you declared
one with the same name there.
A function can only ever return one value. It would be pointless to do the
following:
int my_function()
{
return 3;
return 5;
}
As mentioned before, the return keyword will return the value, if any, destroy all
the function variables and disregard any further code in that function. Thus,
the function would only return 3. However, there is a way to solve this should you
need to return multiple values.
Parameters are usually passed into a function by value. This means that, if you
passed a variable as a parameter to your function, the interpreter will read the
value of that variable and pass that to the function. Here is an example.
//main function
int x=5;
my_function(x);
The interpreter would create a variable of type int called x and assign the number
5 to it. It would then check the value of x, which subsequently is 5, and call
my_function, passing 5 as the parameter.
However, parameters can also be passed to the function by reference. Instead of the
interpreter reading our variable and passing it to the function, the variable
itself will be passed. This means that if the function then modifies the variable,
the modification will also apply to the original. In this way, we can easily
return more than one value from a function by modifying our variables by reference.
Another example:
//Global variables
int x=1;
int y=2;
//Main function
my_function(x, y);
You may be wondering why we used void? This is because, as explained before, we are
returning values by way of our parameters, and not the function itself. If you
checked the value of x and y now, they are no longer 1 and 2, they are 10 and 25.
The other thing you may have noticed is after the data types of our parameters, we
put &out (the ampersand sign followed immediately by the word out). The ampersand
tells the interpreter that we are about to specify how the parameter will be
passed. If no & (ampersand) sign is found, the interpreter will assume the &in
flag,
which means we are passing parameters by value.
When we pass parameters by reference, you are required to pass a variable rather
than a constant. The interpreter won't stop you passing a constant, but it will
issue a warning stating that any changes that are made will be lost.
//main function
string text=my_function("Daniel", 5);
Because we called my_function with a string as our first parameter, the interpreter
will call our second my_function declaration.
It is also possible to give your function arguments default values. This simply
means that you don't have to pass the full set of parameters that the function
expects,
but instead you pass just a few and the function then gets called with default
values for the other optional ones. An example follows:
void main()
{
print("Hello!", 10, 20, 30);
print("Hello again!", 10);
}
Here we declare a function called print, which takes four arguments. The first one
is a string which is used for the title of the message that we display, and this
argument is mandatory. We see this because it has no default value given after the
argument name. For the other three values, however, we have an = sign after the
name and then a value that will be the default if this argument is not specified by
the caller. We see in our example that the first call to print specifies all
four parameters, where as the second call only specifies two of them. Therefore, in
the second message that is displayed, the last two integers are set to 0.
What is happening here is that when the interpreter arrives at the if statement it
will check to see if it is true, which is to say if the health variable has a
value lower than 21. If it does, it will execute the code that is between the if
statement and the closing right brace that you see below. You can also do:
if(health < 21)
{
// Play the heart beating sound.
}
else
{
// Do something else if the health is not below 21.
}
In this case, two things may happen. If the value of health is lower than 21, then
the first code section is executed. However if the condition is not true, the
other section of code which is to say the one that is between the word else and the
} statement will be executed instead. In short, this allows you to make one
thing happen if a condition is true and another if it is not.
Another point of interest: if there is only one thing to be done when your if
condition is true, you do not have to use the braces. Here is an example:
if(health<21)
{
//Play heart beating sound
}
if(health<21)
//Play heart beating sound
if(health<21)
{
//Play heart beating sound
alert("Alert!", "You are about to die..."); //show a message
}
It is important to remember that multiple actions must contain the braces. In fact,
it is recommended to use braces in all situations for easier code management,
so that there are no mistakes. It will then make it easier if you wish to add any
further actions inside that if statement in the future.
In the previous section, I told you that any variables that were declared inside a
function were local only to that function. The same holds true for if statements.
Any variables declared from within the if statement are local to that statement,
and cannot be used outside. It is not an everyday occurrence that a variable is
declared from within an if statement, but it may prove handy at some point. The
same goes for any block of code, including loops.
Of course, there are many other checks that you can perform. Below is a complete
list:
� if(x == y) - This is true if x is equal to y. You can perform this check on any
type of variable.
� if (x != y) - This is true if x is not equal to y. You can perform this check on
any type of variable.
� if(x < y) - This is true if x is lower than y. You can perform this check only on
numeric variables.
� if(x <= y) - This is true if x is lower than or equal to y. You can perform this
check only on numeric variables.
� if(x > y) - This is true if x is higher than y. You can perform this check only
on numeric variables.
� if(x >= y) - This is true if x is higher than or equal to y. You can perform this
check only on numeric variables.
� if(!x) - this is true if x is false. This can only be used on boolean variables.
You may be wondering why we use two equal signs when checking if x is equal to y?
This is because with one equal sign we would be assigning the value of y to x,
which is not exactly what we want. So saying:
if(x=y)
Let us illustrate all this with a more practical example. We will once again return
to John Doe and his problems to decide his own age, however this time we will
print out whether or not he is a minor based on the random number that is
generated. Going back to an earlier example, take a look at the following code
snippet
one more time.
Familiar? Good, because it won't be for long... Now try to guess what will happen
when we do the following.
The above code is really very simple, but we will go through it step by step to
make sure that it is 100 % clear. First, we make the variable called my_name which
in this case is entirely unnecessary, but it was part of the original example so we
allowed it to stay. Then, we generate a random number between 5 and 50 and assign
it to the variable called age. After this, we make the string that will contain our
final message. You will notice that the sentense is not complete, and little
wonder since we are about to complete it inside an if statement. If age is lower
than 18 we add some text on to the string that says that John Doe is a minor. If
it is not, then the other code section is executed which instead makes the string
claim that John is of full age. Finally, of course, we print it out in our good
old message box. Try running this code, and see if you understand the logic behind
the printed result.
Now that we've seen how if statements are used, it's time for me to introduce you
to a new type of variable... The boolean. A boolean can only hold two values,
true or false. These values are often used in if statements, like this for example:
bool has_weapon=true;
if(has_weapon==true)
{
// Do something.
}
The words true and false are not names of any predefined variables or constants,
they are actual keywords in the language. For this reason, you could not say:
false=5;
Since false is already used as a keyword. On the other hand, when working with
boolean variables you could do something like:
bool has_weapon=true;
if(has_weapon)
{
// Do something.
}
This may look strange at first glance since we do not actually specify any
condition that is supposed to be met, but since has_weapon is a boolean variable
and
has the value true the code inside the if statement will execute because the entire
expression will come out as true. Similarly, if has_weapon had been set to false
the code inside the if statement would not have been executed since the whole
expression would thus be false.
To demonstrate the different uses of boolean variables along with other variable
types, we will look at a simple usage example for the functions called key_down
and key_pressed. These functions are used for checking the state of certain keys.
Take a look at the following piece of code:
if(key_pressed(KEY_F4))
{
if(key_down(KEY_LMENU))
{
alert("Fine!", "If you want to be boring and exit this program then by all means do
so. Bye.");
exit();
}
else
{
alert("Um...", "Are you, by any chance, trying to close the program down?");
}
}
Here we check if two different conditions are true and act based on what we find.
First of all we check if the f4 key is pressed. If this is true, we check if the
alt key is being held down. If this is true as well, we assume that the user is
closing the program down. If this is not true, we display a taunting message
because
they forgot to hold alt down.
Finally, we have two closing braces so that all the if statements are closed
properly. You may nest as many if and else statements as you want and in any way
you
like, as long as you balance them out with the proper braces in the right places.
Is there a way to test that more than one condition is true, I hear you ask? Yes
there is. The logical operators allow you to check multiple conditions in an if
statement.
Take the following example. We will again check for Alt+F4, but this time, no
messages will be displayed. We will just simply exit.
if((key_down(KEY_LMENU))&&(key_pressed(KEY_F4)))
{
exit();
}
Two main things that you may notice here. For a start, our if statement contains
three sets of parentheses. One enclosing the whole if statement, and another two
sets enclosing each condition. This is purely for readability purposes. We could
have just as easily said
if(key_down(KEY_LMENU)&&key_pressed(KEY_F4))
if((key_down(KEY_LMENU))||(key_pressed(KEY_F4)))
{
exit();
}
This serves no real purpose in this example. It tells the engine to exit if the alt
key or the f4 key is pressed. This could, however, be useful if you have assigned
two keys to one function, for example the down and right arrows moving you one
option down in a menu.
The final operator, which we have briefly mentioned, is the negation operator. This
is written as an ! (exclamation mark). This reverses the result of a condition.
For example:
if(!key_down(KEY_LMENU))
if(!key_down(KEY_LMENU))
Notice that there is only one condition, and one check. This literally reverses the
state of a value, as opposed to comparing the falseness of two values. For example,
if the user is pressing the Alt key, the function returns true. Therefore the
negation operator turns this into false. To check that a given value is equal
(true)
or different (false) to another value, you would write:
if(my_var1!=my_var2)
This would make more sense, both logically and gramatically. Because the
exclamation mark represents negation, in other words not, and equals is self
explanatory,
that translates into English literally as: if my_var1 is not equal to my_var2.
Rather than directly reversing the result like the negation operator, this will
itself
return true if the condition is false, I.E. if the two variables are not equal.
7.2. Switch and Case
Switch...case statements are very similar to if statements, except they are used to
lay out numerous or more complicated checks.
Here is an example:
Int health;
double energy;
switch(health)
{
case 100:
energy=100;
break;
case 90:
energy=89;
break;
case 80:
energy=78;
break;
case 70:
energy=67;
break;
case 60:
energy=56;
break;
//additional values go here
}
The keyword switch is used to start a case statement and is followed by the
variable to be checked inside parentheses. Each condition is introduced by the
keyword
case. The body of the switch statement is enclosed in braces.
At the end of each case in a switch statement, you will generally use the statement
break to finish that case.
If you omit the break statement at the end of a case, execution will continue into
the next case. Although this can sometimes be useful, it is something to be aware
of.
Another keyword that can be used is default. This performs a task for any other
condition not met by the case statement. Taking our above example, we could make
our player's energy decrease more realistic by doing the following:
switch(health)
{
case 100:
energy=100;
break;
case 90:
energy=89;
break;
case 80:
energy=78;
break;
//additional values go here
default:
energy-=0.5;
}
This means now that every time this case statement is executed, it will set our
energy to a predetermined value every time our health reaches a number divisible
by 10, and just subtracts 0.5 off in all other circumstances.
Notice that even a default case still has a break. You can put the cases in any
order you wish.
The expression inside the switch condition can be any expression that results in a
value, so long as the result is numeric. For example, you could have:
switch(number % 100)
However, each case inside the switch, must be a constant: you cannot put
expressions such as:
case n < 5:
or
case number:
Also, you cannot have more than one case with the same value, or more than one
default case.
while(key_pressed(KEY_ESCAPE)==false)
{
wait(5);
}
Confusing? No problem, we will go through it bit by bit. The first line is really
the most complicated one. First you have the word "while", which means that a
loop should begin here. After that you have the condition that needs to be met for
the loop to RUN. In this case, we use the function called key_pressed to check
if the escape key has been pressed, and make the loop run while that function
reports that the key is not in fact being pressed. In short, the loop will continue
to run until the user presses escape. The code between the while statement and
closing brace is then executed over and over again, and after each execution the
condition is tested once again to see if it is now true. If it is, the code after
the closing brace will begin executing instead. In our loop we only make the
program
wait for 5 milliseconds, which basically pauses execution for a very short time
between every check of our condition. If we didn't include this line, the
processor's
CPU would begin running at 100 % which is not really what we want. So in any loop
that runs for a longer period of time you should always have a short pause, and
5 milliseconds is usually a good compromise between speed and CPU usage.
But let's say that you wanted to perform a particular task x number of times, would
a loop be the right option? Certainly. Here is a quick example that shows how
you can make something happen 1000 times.
int x=0;
while(x<1000)
{
// Put your code here.
x++;
}
x going from 0 to 999. We could of course have given x an initial value of 1 and
let the loop run while it was lower than 1001, or by all means while it was lower
than or equal to 1000 (see the chapter on if statements for details), but there is
a particular reason why we chose to start at 0 which will be explained further
in the next chapter.
Here is another example. We will display the numbers 0 to 10 in a message box.
int x=0;
string numbers="";
while(x<=10)
{
numbers+=x;
if(x<10)
{
numbers+=" ";
}
x++;
}
alert("Printing numbers 0 to 10", numbers);
The loop executes while x is less or equal to 10, in other words, until x is 11.
int x=0;
string numbers="";
while(x<=10)
{
numbers+=x++;
if(x<=10)
{
numbers+=" ";
}
}
alert("Printing numbers 0 to 10", numbers);
Here, we have changed x in exactly the same place as we checked it. X will change
after its value has been checked (notice the ++ operator on x).
We also slightly changed our if condition to check if x is less than or equal to
10. This is because otherwise the spaces would stop from 9 upwards, since x is
being increased before the if condition is checked.
8.2. Do while loops
We have looked at while loops, now it's time to show you something very similar.
Let's start off with waiting for the user to press escape again:
do
{
wait(5);
}
while(!key_pressed(KEY_ESCAPE));
There are two points of interest here. First, the line that says do tells BGT where
the loop starts. The block of code, again enclosed in braces, is the section
of code that is to repeat while the condition is true. Finally, there is one
additional line after the close brace which tells the interpreter which conditions
should be met in order for the loop to continue.
You may notice that in this while statement, there was an exclamation mark before
the condition. This tells it to repeat the loop as long as the condition is false.
In short, the following two lines mean exactly the same thing:
while(key_pressed(KEY_ESCAPE)==false)
while(!key_pressed(KEY_ESCAPE))
skipped if the condition is not met. However with a do while loop, the condition is
not specified until the end of the loop, which means the code is always executed
at least once.
8.3. For loops
The for loop is the most powerful of the loops, and is used mainly for counting.
Essentially, it combines the various parts of loop control into one statement.
Consider the while loop in the above examples. Before we started the loop, the
variable was initialised to a starting value (in our examples, it was 0). Then, we
had the while condition at the top of the while loop. Finally, just before the
closing brace of the while, we re-initialised x ready for next time around.
In a for loop, these three stages are packaged into one line:
variable, as if you were modifying its value anywhere from the script.
Second, we tell it upon what conditions our loop will continue to run. In our case,
if x is less than 10.
Lastly, we tell it how to reassign the variable ready for the next cycle of the
loop. Although we increased x by 1 as would be the case in most other languages,
the reassignment expression can be anything you like. Take a look at the following
code.
Every cycle of the loop will now multiply the current value of x by 2.
8.4. Break and Continue
There are two last possible scenarios that we need to cover. Sometimes you will
want a lot of things to occur continuously, but you may not know exactly when you
want the loop to stop or you may want to stop it for several different reasons that
can't all be used as the condition. In this case, you might do something like
the following:
while(true)
{
if(something==true)
{
break;
}
if(something_else==true)
{
break;
}
if(yet_another_variable==true)
{
continue;
}
// more code here...
wait(5);
}
There are three new things here. The first thing is on line 1, where we don't
exactly have a condition for our loop. All we say is "while(true)", which doesn't
really make a lot of sense at first glance. However, it's not so strange because
true, of course, is always true which means that the loop will never end simply
because the condition can never be false. So in short, we have created an endless
loop. Naturally we don't want the loop to go on forever; we do want to stop it
at some point, but we do it from inside the loop itself. The above three if
statements check for conditions that don't actually exist, so if you ran this
script
it would give you an error. What I am trying to show is that you may, at your own
discression, break out of the loop for any reason and at any time using the break
statement that you see inside the first two if's. We briefly saw the break
statement in action when looking at the switch...case statements. The break
statement
basically forces the loop to terminate and begins executing the code that comes
after it. As mentioned before this is very useful when you want to exit the loop
for a number of reasons, or when you don't know exactly when it should stop.
It must be noted that, if the loops are nested, the break statement will only break
out of the loop it is declared in.
The other keyword you saw in the third if statement is continue. Again, this
keyword can be used anywhere from inside a loop. The continue keyword tells the
interpreter
to skip the remainder of that cycle and move onto the next. This is useful, for
example, when you are reading from a file and there are certain things, like blank
lines that need to be skipped.
9. Arrays and dictionaries
So far, we have seen the basic uses of variables. We have seen how they can be
manipulated, and how they are used in a general programming context. There is,
however,
a great issue that you will undoubtedly discover sooner or later, and that's the
incredibly large amount of different variables that you would have to keep track
of even in a middle-sized game. Imagine if you will, that you have a game board
with 50 squares on it. The user is supposed to roll a pair of dice, their position
increases by the resulting amount, and then you want to check what should happen on
the square that the player lands on. For the purposes of this simple example
we will assume that the variable representing a square can have 3 values; 1, 2, and
3. These values can mean anything that you want, of course. 1 could mean that
the user goes back three squares, 2 could mean that they get to remain where
they've landed and 3 could mean that they get to move forward three squares. Now
let
us try and construct part of this game board quickly. We will let the first square
be number 0 rather than number 1, for reasons which will become clear shortly.
int board0=2;
int board1=2;
int board2=2;
int board3=2;
int board4=1;
int board5=2;
int board6=2;
int board7=2;
int board8=2;
int board9=3;
int board10=2;
int board11=2;
int board12=2;
int board13=2;
int board14=1;
...
Let us stop there, as you will undoubtedly be furious at the amount of paper that
you will have wasted if you printed out this tutorial. You will, after looking
at this example, agree that the code is incredibly repetitive and tedious? Let's
say instead that this was a side scrolling game and the different numbers
represented
ground, fire and water and the like, you might have had 300 squares instead if the
level was a large one. Imagine the joy of writing out 300 such lines as you saw
above... It's just not practical. Not only does it take ages to write the actual
contents of the grid or board, but you would have to have 300 if statements in
order to establish which of the 300 variables should be examined after the player
moved to a new square. Can you say nightmare? I thought so. Luckily, there is
an easy enough solution... Arrays.
An array is, simply put, a list of variables. The array itself has a name like any
other variable, but it contains different values that you can refer to by index.
An array can be as small or as large as you need it to be. As always, we will look
at an example to wet our apetite.
int[] board(50);
for(int x=0; x<50; x++)
{
board[x]=2;
}
Wait, wait... Many new things here. Let's go through the code line by line as
usual. The first line tells the engine to create a new array, which is to say a
list
of variables, of type int. The square brackets following int tells the engine that
this variable is an array. The array is then assigned to the variable called
board, so that we have a way of referring to entries inside the array later. The
number inside the parentheses tells the engine to give this array 50 entries. On
the next line we use a for loop to make a new variable called x, and then start a
loop that will run while x is lower than 50. On the line after that, something
very interesting happens. Here we assign a value of two to an entry in the board
array. This entry is not specified with a literal value in this case, however.
It could certainly have been, but it makes much more sense to use a loop since we
then do not need to specify each individual entry explissitly as, of course, we
would then be nearly as badly off as with the individual variables in the previous
example. You will observe that the index of the array, which is to say the entry
in the list to access, is specified between a left and a right bracket and not with
a left and a right parenthesis, which you might have expected. On the last line
we simply tell the engine that our loop ends here. What we have done is to go
through the entire list of 50 items, automatically assigning a value of 2 to each
one. After this we only have to assign values manually to those entries in the list
that are supposed to be something other than 2. Quite a bit simpler than the
nightmare example from earlier, right? I thought so.
Now you may also have understood the reason for our specifying the first square on
the board in the original example as 0 rather than 1. This is simply because
the first index in an array is always 0, never 1. So if you have an array with 100
entries and you wish to loop through it to perform some task, you would start
at 0 and go up to 99. If you try to access an entry in an array which is out of
range, you will get a runtime error. This means that the interpreter will not
complain
until the array is evaluated, therefore it is absolutely essential that you test
the game at regular intervals, especially any array related sections, otherwise
you may find you receive some rather angry players on your back.
You can treat an array entry just like a regular variable, which means that you can
use it in if statements and loops and anything else that you can do with normal
variables, with the only difference that you specify an index in the array rather
than a unique variable name.
You may also have arrays with multiple dimensions, in order to make things such as
an x-y grid or even x-y-z. For example, if you want to make a chess board you
could do the following:
int[][] chessboard;
[Link](8);
for(int i=0; i<8; i++)
{
chessboard[i].resize(8);
}
To access an element in an array with multiple dimensions you use the same exact
method as when you accessed elements before, just with the different dimensions
specified between brackets. To access the square in the upper left corner of our
chess board, for instance, one could write:
chessboard[0][0]=5;
And to access the square in the lower left corner, you would do the following:
chessboard[0][7]=5;
The same goes for a board with three dimensions. If you wanted a board which was 3
by 3 by 3, you would declare and initialize it like this:
int[][][] board;
[Link](3);
for(int i1=0; i1<3; i1++)
{
board[i1].resize(3);
for(int i2=0; i2<3; i2++)
{
board[i1][i2].resize(3);
}
}
board[2][0][1]=10;
By using an array with three dimensions you could, for example, represent a grid
with x, y and z coordinates. This would be a true 3d game with left, right, back,
forward, up, and down movement.
Arrays are used for many different things. As a matter of fact, a string itself is
an array, though it doesn't look it on the outside. Each element of a string
array, is a single character.
Here is an example:
This would return the letter i. Remember that arrays always start with 0, therefore
entry 2 holds character 3.
As strings can be used as function parameters and return values, so can arrays.
However there may be times when you don't know your array's length, either because
it is a returned string or array, or because a loop is always changing values
depending on certain conditions. No problem. An array holds two special functions
to allow you to check or change your array. Length is self explanatory. It returns
the length of the array. Please note that this does not mean the final element
number. If you are using this method as the condition of a loop you need to check
that your counter is less than that of length, as shown in the below example,
which will display every character of the string in a message box:
string my_string="string";
for(uint my_counter=0; my_counter < my_string.length(); my_counter++)
{
alert("my_string","my_string["+my_counter+"]="+my_string[my_counter]);
}
Notice that to obtain the length we say my_string.length(). This is because the
function length() is stored within the variable. This is known as a method. We will
discuss methods in more detail in the next chapter. Also you will have noticed that
I used a variable of type uint rather than int. This is because the length method
returns an uint as opposed to an int or a double. It is always recommended to have
signed and unsigned integers matching, otherwise the compiler will flag a warning
when you attempt to run your script.
There is a final method that is used to control an array. it is called resize, and
is used to, you guessed it, resize an array, either adding new elements or removing
elements. It takes one parameter, the number of elements that the new array should
contain. Note that, like length, you are declaring the actual number of elements
that should be present, not the number of the last element. For example:
This will resize the array down to 4 elements, ending on entry 3. Note that when
you resize an array to smaller than its original value, entries are removed from
right to left, I.E. from the last element backwards. Therefore the resulting string
will be "this".
It is possible to specify exactly which values will go into an array at the point
where the array is defined. To do so, simply put an equals sign after the name
of the array, and follow it with a list of values in braces. It sounds more
complicated than it is. For example, let us define an array of integers and
initialize
it with some meaningful values, all in the same line of code:
There is a slight limitation with arrays, and this is that they can only store
values of the same data type. You could not, for example, have one entry with the
value 45, and another with the value "hello". However, this can be resolved with
another type of variable, known as dictionaries. A dictionary is what is called
an object with methods, which I touched upon briefly in our last example. More on
this in the next chapter.
dictionary board;
for(int x=0; x<50; x++)
{
[Link](x, 2);
}
Again, lots of new things to discuss. The first line simply declares a dictionary,
which we refer to as board. The next line then starts a loop, like we did with
the array, from 0 to 49. The line after that tells the interpreter to create an
entry in the dictionary, called the current value of x, and assign the number 2
to that entry.
This has certain advantages over arrays, since you could in theory store values of
x and y coordinates, like so:
dictionary board;
for(int x=0; x < 4; x++)
{
for(int y=0; y < 4; y++)
{
[Link](x+","+y, 2);
}
}
We accomplish this by using a nested loop to increase both counters and assign the
initial value of 2 to each square. Notice that we are using two values separated
by a comma inside our name string. This just makes it easier for us to separate the
squares from one another, as the dictionary object just sees it as a string;
it has no idea what the name might mean to us. In practice, all that is happening
is that a string with a unique name is assembled which is to say our square numbers
separated by comma.
Similarly, to retrieve the value of a certain square, you may do the following:
int square_value;
[Link]("2,2", square_value);
The second parameter of the get method is passed by reference, meaning the value is
stored in the variable that we pass it. If my meaning is unclear, refer back
to the functions section which contains detailed information about the use of
parameters.
As well as being able to set and retrieve values in dictionaries, you can also
change, and even delete them:
if([Link]("2,2"))
{
[Link]("2,2");
}
This tells the interpreter to look in the dictionary called board for "2,2". If it
finds it, this value is then deleted. This cannot be done with standard variables.
It is very important that you check the existence of a dictionary entry. If you
delete or check an entry that doesn't exist, this will not cause any syntax or
runtime
errors to occur, but the variable you are attempting to use to retrieve the value
will not change.
You can also use the delete_all method to delete all assignments from the
dictionary.
Please note that dictionaries are significantly slower than arrays and regular
variables, so use them with care.
10. Objects and classes
Objects are a very powerful and absolutely essential feature when making games with
the BGT engine, so be sure to read this chapter very carefully. An object is
a regular variable, but with special functionality. You can think of an object as a
variable that contains other variables inside itself, as well as functions that
do something with that particular object. Another name for an object is a class. To
make things a little more clear, we will begin with an example.
sound ambience;
[Link]("[Link]");
This example does several things. It tells the engine that we want a new object of
type sound, that we want the object to be assigned to a variable that we call
ambience, and finally it calls a function within the ambience variable, specifying
the filename of the sound to open.
When an object is created, it can take a list of parameters which largely determine
how the object will behave. It is also possible for objects to take no parameters
at all, which the sound and timer are good examples of. The timer object would be
created as follows:
timer jumptime;
Once an object has been created, you may begin using it. An object has what is
called properties, which are basically variables that are stored inside the object
and which are used both to get and set various pieces of information during the
lifetime of the object. The sound object, for instance, has a property called
volume.
This determines how loudly the sound will play, and can be modified in real-time.
Here is an example of how to create a sound object with a sound that is opened
as a stream, and then how to set its volume to -6 decibel which which is to say
about half of the original amplitude:
sound ambience;
[Link]("[Link]");
[Link]=-6;
if([Link]>-10)
{
// Do something
}
In this case, we do something if the volume is greater than -10 decibel. Some
properties cannot be modified but only checked, while others can only be modified
at certain times but not at others. You will find all the information about this in
the properties list for each object in the object reference.
Objects also have what is called methods. A method is simply a function like any
other that you will find in the BGT engine, with the only difference that it works
exclusively with a particular object. To go back to the sound object, there is a
method called play_wait which starts the sound playback and then waits for it to
finish before returning. In fact, the load and stream functions we experimented
with earlier are also good examples of methods.
Let us extend the previous example to include playing the sound as well as setting
its volume.
sound ambience;
[Link]("[Link]");
[Link]=-6;
ambience.play_wait();
Familiar? Good, as we are basically just calling a function but one that operates
on our specific copy of the sound object, called ambience in this case. play_wait
takes no parameters, but many other object methods do in which case they are
specified between a left and a right parenthesis just like in a regular function
call.
It is perfectly possible to make two versions of the same object, in which case
they will work completely independently of each other. If you make two sound
objects
for example and then call play on both, you will get both sounds playing at the
same time.
You may want to pass objects around various functions of your own. To do this, it
is essential that you pass handles, and not the object itself. A handle is
essentially
a reference to the original object. To do this, you would do something like the
following:
In short, when you declare an object handle, you put an @ (at) sign after the
variable type. You can then use this variable as if it were a standard object, the
only difference being that you are referencing the original variable. When you want
to change the value of your handle, for example to make it point to another
object of the same type, you simply put the @ (at) sign before the variable name.
You can destroy a handle by setting its value to null. This is the object
equivalent to 0. With this in mind, though you cannot manually destroy the contents
of
an object, a way to work around this is to set all your global objects as handles,
and only use the objects themselves in functions. This way, although the objects
themselves are local variables, they have global variables still referring to them.
Therefore the object will always remain usable until its last handle is destroyed.
You can also make your own objects, also known as classes, with their own methods
and properties. For example, you could make a class for an enemy, as follows:
class enemy
{
int health;
int speed;
int position;
void fire_weapon()
{
//weapon code goes here
}
void move(int direction)
{
//movement code goes here
}
}
The only new line here is the first one, but it is only the same as declaring a
variable, with the slight difference that you have to tell the engine how to make
that class. In this case, our enemy class has three properties, health, speed and
position, and two methods, fire_weapon and move. However, we need not change the
position property directly, since the move function will do that for you, along
with any other code that you put in there. It is important for this reason that
you document which methods and properties should or should not be touched.
To use our class we would then write:
enemy robot;
Then our class would be ready for use, just like the objects provided in BGT.
There is a slight problem though. The enemy's health hasn't been assigned a value.
This means that every time we declared an enemy variable, we would have to set
every health to 100. This is easily solved with the constructor and destructor
functions.
The constructor function simply instructs the engine how our properties should be
set up, and/or performs any other tasks that we might want to have done whenever
a new instance of our class is created. The constructor function is optional, and
is declared with the same name as the class. The destructor function is called
when our class instance is destroyed. It can be used to do any necessary clean-up
work. The destructor is also optional, and is declared in a similar fashion as
the constructor but with a ~ (tilde) sign prepending the class name. Finally, both
the constructor and destructor functions are the only functions which do not
declare a return type; not even void.
Let us extend our enemy class to include a constructor:
class enemy
{
int health;
int speed;
int position;
enemy()
{
health=100;
position=0;
speed=300;
}
void fire_weapon()
{
//weapon code goes here
}
void move(int direction)
{
//movement code goes here
}
}
Now, every time we declare a variable of type enemy, its properties will always be
set correctly.
class enemy
{
int health=100;
int speed=300;
int position=0;
void fire_weapon()
{
//weapon code goes here
}
void move(int direction)
{
//movement code goes here
}
}
In this case, we don't actually implement a constructor because the properties are
given default values directly in their declaration. However, the constructor
may need to do other work so we will stick to using it in the future as well.
To make your class more portable you can use parameters for your constructor,
rather than expecting the user of the class to modify the properties directly. We
show this with our rapidly growing enemy class:
class enemy
{
int health;
int speed;
int position;
enemy(int init_health, int init_speed, int init_pos)
{
health=init_health;
position=init_pos;
speed=init_speed;
}
void fire_weapon()
{
//weapon code goes here
}
void move(int direction)
{
//movement code goes here
}
}
Now to declare our enemy, we would write the following:
This would set the properties of our enemy object instance to the values that we
specified. That way you can create enemies of varying strengths, speeds and
randomly
scattered on the game board. Note that since we define only one constructor which
takes parameters, it is no longer possible to declare our enemy without specifying
these parameters (see below for more information on automatic default
constructors).
As I mentioned in the functions section, functions can have the same name, so long
as the parameters are different. This also holds true for class methods, including
the constructor, but not the destructor. This means, that if you wanted to allow
your enemy to be set with different values, you could have a constructor where
the user must specify the parameters, but you could also have a constructor where
no parameters have to be given. This way, you can decide whether to declare your
enemies with parameters or not. The compiler will then find the constructor that
matches your parameters (if any), and call it for you. We will now demonstrate
this with our enemy code. I will not include the class properties or methods, I
will simply use our constructors.
class enemy
{
//properties go here
//constructor without parameters
enemy()
{
health=100;
position=0;
speed=300;
}
//constructor with parameters:
enemy(int init_health, int init_speed, int init_pos)
{
health=init_health;
position=init_pos;
speed=init_speed;
}
//class methods go here
}
Of course, you could put your class methods wherever you liked, however it made
logical sense to put our constructor at the top of our class, since this is the
first function it calls.
Now to declare our enemy, since we have two constructors, we could either do
enemy robot;
or
The first declaration would simply give us an enemy with his default values.
However, our second declaration gives our enemy twice the strength and speed of our
first. Although 150 is half the value of 300, we are telling the program the time
in which it takes him to move, not how fast he moves in a given time.
if you do not define a constructor in your script, the compiler will automatically
make an empty one for you that takes no parameters. This means that by default,
we could declare any of our classes as you saw above with no argument list.
However, if you define a constructor that takes parameters, the default one is no
longer
automatically implemented. It is only implemented automatically by the script
compiler if there is no constructor at all specified by you. Therefore, if you want
Please note that, as mentioned earlier in this tutorial, primitive variables (which
is to say variables that are not objects), will contain undefined values if
you don't give them initial values explicitly. This applies not only to global
variables and variables inside functions and methods, but also to properties in
classes.
If you declare an object like:
enemy robot;
Then the default constructor (if present) will be invoked implicitly, so unlike
with primitive variables such as int and float, the contents of the robot variable
is not at all undefined. The constructors are supposed to initialize the properties
used internally by the class, however. That does not happen automatically. In
short, you must make sure that all the properties that you use in a class are
initialized to meaningful values before they are used - just like you do with
primitive
variables outside of classes. The constructors are a good location in which to do
this. Alternatively you can give the properties initial values when they are
declared,
just like you would with global variables or local function variables.
Destructors more or less work in the same manner as constructors, except that they
are called when your class is destroyed. This is usually either when its last
reference is destroyed, or when the program exits. Here is our basic enemy once
more, with a single constructor taking no arguments, and a destructor:
class enemy
{
//properties go here
enemy()
{
health=100;
position=0;
speed=300;
}
~enemy()
{
alert("glory!","your enemy is dead!");
}
//methods go here
}
It would be pointless to run this example. In fact, if you implemented this example
into a game, once you exited the game you would get message boxes left right
and centre telling you your enemy was dead, because the interpreter is destroying
all active objects and therefore calling their destructor functions, if any are
present. Just like with the constructor, the compiler will make an empty destructor
for you by default if you choose not to create one yourself. This means that
you only need to define one explicitly if you wanted to perform some specific
action when the class was destroyed.
11. Advanced object techniques
The previous chapter has given you just enough information to begin using objects
productively in your game creation endeavors. This chapter will extend your arsenal
If anything appears unclear, you may wish to reread the previous chapter as it lays
the foundations for this one.
An object can be imagined as a box with variables and functions living inside it.
The variables inside an object are called properties, and the functions inside
an object are called methods.
Every object belongs to a class, or type, and an object's class determines which
properties and methods the object has. For example, a sound object, belonging to
the sound class, will always have a play method because the sound class says so.
class bird
{
void walk()
{
// Walking code for birds goes here.
}
void run()
{
// Running code for birds goes here.
}
void fly()
{
// Flying code for birds goes here.
}
}
Now, in an effort to make the game more realistic, we come up with the idea that
the sparrow in the above code should actually be able to sing. How might we go
about this?
One approach might be to modify our bird class by adding a sing method. The obvious
problem is that this would give every bird, not just the sparrow, the ability
to sing. Even in a game world, doves should still coo, and eagles should still cry.
After all, it was realism which prompted the addition of the sing method in
the first place.
Our second approach would be to create an additional class for sparrows which is
identical to the bird class in every way but one: it has a sing method. Here is
the corresponding code for both classes, in full:
class bird
{
void walk()
{
// Walking code for birds goes here.
}
void run()
{
// Running code for birds goes here.
}
void fly()
{
// Flying code for birds goes here.
}
}
class sparrow
{
void walk()
{
// Walking code for sparrows goes here.
}
void run()
{
// Running code for sparrows goes here.
}
void fly()
{
// Flying code for sparrows goes here.
}
void sing()
{
// Singing code for sparrows goes here.
}
}
This approach indeed solves our problem because now sparrows can sing but birds in
general cannot. There are a few disadvantages though:
even though you are sure you fixed the bug, you will soon find out that it still
persists. Even worse, your customers might be the ones to find out. Now assume
you had created classes for cooing doves and crying eagles in the same way. A bug
in the fly method would then have to be fixed in four places. Forget just one,
and the bug will remain. Testing might even suggest it was fixed because the bug
occurs for some kinds of birds and not for others.
3. Suppose your game ended up containing an array of birds, like so:
bird[] aviary(50);
You would have no way of putting a sparrow into the aviary because all elements of
an array must share a common data type.
Wouldn't it be wonderful if we had a way to define a class for sparrows which
inherits all the functionality from the bird class and simply adds the sing method?
And wouldn't it be even better if BGT somehow knew that a sparrow is just another
kind of bird so that we could place a sparrow into an array of birds? You will
be delighted to know that such a way exists in BGT. It is called class inheritance.
Suppose we have defined the bird class as above. Now we can define the sparrow
class as follows:
In the above code, the sparrow class inherits from the bird class. We call sparrow
a derived class, or subclass, and we call bird a base class, or superclass. These
are all just different ways of expressing the same fact, and we take the liberty to
use them interchangeably because all of them are in common use.
To define a derived class, you follow the class name with a colon after which you
write the name of the base class, as demonstrated above. The derived class will
inherit all properties and methods defined in the base class. It is as if every
sparrow object contained a hidden bird object. After all, part of being a sparrow
is being a bird. In this zen-like statement, by the way, is contained the entire
concept of inheritance.
It is important to note that our aviary is an array of bird handles rather than an
array of birds. When treating a sparrow as a bird, it is vital that we use a
handle. Consider the following code:
That last line above is actually more complex than you might think. It creates a
new bird b2 from the data contained in s, but b2 will not be a sparrow. You might
say that b2 contains the birdness of s but not the sparrowness. This is quite
different from b1 because b1 is not a copy of s but merely a handle through which
s can be treated as a bird. b1 and s are names for the same object in memory, but
b2 is another bird altogether.
Not only can a derived class add methods to a base class but it can also modify the
behavior of existing methods. Let's say we wanted to give sparrows their own
unique fly method. The sparrow class would then look like the following:
void main()
{
bird b; // Create a bird
sparrow s; // Create a sparrow
bird@[] aviary = { b, s }; // Put handles to both of them into the same aviary
aviary[0].fly(); // Calls the fly method of the bird class
aviary[1].fly(); // Calls the fly method of the sparrow class
}
Here we have modified the behavior of a method by defining, in the derived class, a
method of the same name and with the same parameters (none in this case). We
say that the fly method in the sparrow class overrides the one in the bird class.
While properties and methods are inherited, constructors are not. However, since
every sparrow object contains a hidden bird object, two constructors need to be
executed when a sparrow is created. Just as a house is built from the foundations
upwards, it is the base class constructor which executes first. The opposite
applies
when an object is destroyed. In this case, the destructors are called from the
derived class upwards, so the sparrow destructor will come first. You can memorize
this by reminding yourself that destroying something is the opposite of creating
it.
Another way of looking at it is to imagine that the first thing a derived class
constructor does is to call a parameterless base class constructor. This happens
automatically, and in most cases is just what you want. There are cases, however,
when you would rather call another constructor in the base class. For this
scenario,
BGT has a special keyword called super. You can use the super keyword just like the
name of a function, only it refers to a constructor of the base class.
Suppose the bird class had an additional constructor which would take the bird's
wingspan as parameter:
class bird
{
double wingspan;
bird(double wingspan)
{
[Link] = wingspan;
}
void walk()
{
// Walking code for birds goes here.
}
void run()
{
// Running code for birds goes here.
}
void fly()
{
// Flying code for birds goes here.
}
}
The above code uses the "this" keyword which you may not have seen before. Anywhere
within the body of a class definition, the keyword "this" can be used to refer
to the object currently considered. In case of a constructor, this would of course
be the object currently created. In the above case the keyword is necessary because
there are two things to which the name wingspan could possibly refer to, one of
them a property of the object to create, the other a constructor parameter. If we
had merely written
wingspan = wingspan;
then we would have assigned the constructor parameter called wingspan to its own
value, a valid but pointless operation.
Now we can modify the sparrow class so that it calls our new constructor:
In this way we have specified that a sparrow is always created with a wingspan of
3. The elegance of this approach is that we did not have to touch the wingspan
property directly but could instead use code which was present in the base class.
As you may have realized by now, code reuse is what object-oriented programming
is all about.
Let us add classes for eagles and doves just to demonstrate how easy it is. And
while we are at it, we will give each bird a make_sound method which causes it to
produce its characteristic sound. Here is our entire code, in full:
class bird
{
double wingspan;
bird(double wingspan)
{
[Link] = wingspan;
}
void walk()
{
// Walking code for birds goes here.
}
void run()
{
// Running code for birds goes here.
}
void fly()
{
// Flying code for birds goes here.
}
void make_sound()
{
// Code for generic bird sound goes here.
}
}
You might wish to run this code to watch polymorphism in action. Notice how our for
loop just tells every bird to make its characteristic sound, and each bird behaves
quite differently. If later on we added another kind of bird to our aviary, the for
loop could remain unchanged. This leads to the fascinating observation that
old code can call new code.
A tool is only really powerful in the hands of a person who knows not just how, but
also when to use it. In the case of inheritance, a good approach is the following:
Use inheritance when not using it would require you to maintain several identical
copies, or similar variations, of the same code. Use inheritance when modelling
real-world concepts which are themselves hierarchical. Finally, use inheritance
when objects of very similar but not absolutely identical types will be given equal
treatment. With some practice you will recognize those situations early on in the
design phase before you have even written the first line of code. The art and
science of what this paragraph talks about is called object-oriented design, just
in case you would like to do some research on it.
A tool is even more powerful in the hands of a person who, in addition to knowing
when to use it, also knows when not to use it. So we close this section with a
word of warning about inheritance. While it is certainly a powerful technique which
has many valid uses, many programmers, when first learning about it, get carried
away by its elegance and conclude that finding just the right class hierarchy must
be the solution to every possible problem of software design. If you aren't
careful,
your class hierarchies may increase beyond the reasonable limits because you would
like them to cover just about every subclass imaginable. For example, we might
have defined separate classes for every kind of eagle or dove known to science. BGT
will uncomplainingly let us extend our class hierarchies to arbitrary unfathomable
depths. But unless our game will be about ornithology (the branch of zoology that
studies birds), the two-level hierarchy presented above will probably be more
than enough. If the truth be told, in most cases even a single bird class should
suffice. Or to quote Python inventor Guido van Rossum: "Simple is better than
complex.
Complex is better than complicated."
11.3. Interfaces
In the previous section you learned about the powerful concept called polymorphism.
One way to achieve polymorphism is through inheritance because BGT allows us
to refer to an object via a handle to its base class. Inheritance makes sense when
classes share a lot of code or lots of conceptual similarities. Sparrow and dove,
for example, share all the code for walking, flying, and running, and in addition
they are conceptually similar due to the fact that they are both birds.
class musical_instrument
{
uint complexity;
musical_instrument(uint complexity)
{
[Link] = complexity;
}
void make_sound()
{
alert("Info", "You hear the sound of music.");
}
}
Birds and musical instruments are different in every way but one---they are both
sound sources. But this similarity alone warants the idea of giving them equal
treatment, for example by storing handles to them in an array of sound sources.
Use inheritance when not using it would require you to maintain several identical
copies, or similar variations, of the same code. This is certainly not the case
because bird and musical_instrument share no code whatsoever except a common method
name.
Finally, use inheritance when objects of very similar but not absolutely identical
types will be given equal treatment. Note the expression "very similar." Birds
and musical instruments are not even considered remotely similar in everyday
discourse.
Try as we might, it seems we cannot create a good case in favor of inheritance. But
there is another way of expressing similarity in one respect for types which
are dissimilar in every other respect.
both classes have a method of the same name and with the same parameters, none in
this case. Another way of putting this is stating that bird and musical_instrument
implement the same interface consisting of a parameterless method called
make_sound. Here is the code which defines this interface:
interface sound_source
{
void make_sound();
}
Note the semicolon that immediately follows the parentheses after make_sound. In an
interface definition we do not include the bodies of methods. An interface is
merely a list of method signatures, and if a class contains methods with the given
signatures of an interface, we say that the class implements that interface.
The whole point is that an object can be manipulated through a handle to one of its
interfaces.
We have to tell BGT that the bird class and the musical_instrument class both
implement the sound_source interface. The syntax for this is exactly the same as
for
inheritance, so instead of
class bird
we write
class bird : sound_source
and instead of
class musical_instrument
we write
class musical_instrument : sound_source
Here is a code example for everything we have learned about inheritance and
interfaces:
interface sound_source
{
void make_sound();
}
class bird : sound_source
{
double wingspan;
bird(double wingspan)
{
[Link] = wingspan;
}
void walk()
{
// Walking code for birds goes here.
}
void run()
{
// Running code for birds goes here.
}
void fly()
{
// Flying code for birds goes here.
}
void make_sound()
{
// Code for generic bird sound goes here.
}
}
void main()
{
sparrow s;
eagle e;
dove d;
bird@[] aviary = { s, e, d };
for(uint i=0; i<[Link](); i++)
{
aviary[i].fly();
aviary[i].make_sound(); // Each according to its kind
}
drum my_drum;
sound_source@[] my_sound_sources = { d, s, my_drum };
for(uint j=0; j<my_sound_sources.length(); j++)
{
my_sound_sources[j].make_sound();
}
}
Note that, while a class may inherit from at most one other class directly, it may
implement any number of interfaces. If you wish to express that a class called
c implements the three interfaces i1, i2, and i3, you simply separate the interface
names by commas, like so:
Interfaces and inheritance are not mutually exclusive, so a class may implement
certain interfaces as well as inheriting from another class. So to express that
class c1 inherits from class c2 as well as implementing interfaces i1, i2, and i3,
you would write:
Let us write a simple class for representing three-dimensional vectors. In case you
need a refresher, a three-dimensional vector is simply a combination of three
numbers. For example, (3, 4, 5) is a three-dimensional vector. We say that a vector
is composed of three numbers, and the three numbers are the components of the
vector. By convention, when dealing with a three-dimensional vector, we call its
first component the x component, its second the y component, and its third the
z component. So (3, 4, 5) has, for instance, a y component of 4. In mathematics,
vectors are commonly used to describe locations in space. For example, (3, 4, 5)
can be interpreted as follows: From a fixed location called the origin, go three
meters to the right, four meters forward, and then fly five meters upward. And
don't you start arguing with a mathematician that gravity won't let you---after
all, this is not physics class. Then again, I shouldn't have brought meters into
this. In games, by the way, vectors can play an important role because they are
very adequate for describing where things are, how fast they move, and how quickly
they accelerate. They can even describe which way something or someone is facing.
So never underestimate a vector even though it's just three numbers.
Vectors can be added, and the way to add them is to add the individual components.
For example, adding (3, 4, 5) to (5, 4, 3) gives us (8, 8, 8).
A vector can be multiplied by a number called a scalar, and the way to do that is
to multiply the individual components by the scalar. For example, multiplying
(3, 4, 5) by 6 gives us (18, 24, 30).
Finally, vectors can be compared for equality. The rules say that two vectors are
equal when their individual components are equal such that the two vectors share
equal x, y, and z components. For example, (3, 4, 5) is equal to (3, 4, 5) but not
to (4, 5, 3).
class test_vector
{
double x;
double y;
double z;
test_vector(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
double get_x()
{
return x;
}
double get_y()
{
return y;
}
double get_z()
{
return z;
}
string to_string()
{
return "(" + x + ", " + y + ", " + z + ")";
}
test_vector add_to(test_vector@ other)
{
return test_vector(x+other.x, y+other.y, z+other.z);
}
test_vector multiply_by_scalar(double scalar)
{
return test_vector(x*scalar, y*scalar, z*scalar);
}
bool is_equal_to(test_vector@ other)
{
return x==other.x && y==other.y && z==other.z;
}
}
void main()
{
test_vector v1(3, 4, 5);
test_vector v2(5, 4, 3);
test_vector@ v3 = v1.add_to(v2);
alert("For your information", v1.to_string() + " + " + v2.to_string() + " = " +
v3.to_string());
test_vector@ v4 = v1.multiply_by_scalar(6);
alert("For your information", "6 * " + v1.to_string() + " = " + v4.to_string());
if(v1.is_equal_to(v2))
{
alert("For your information", v1.to_string() + " and " + v2.to_string() + " are
equal.");
}
else
{
alert("For your information", v1.to_string() + " and " + v2.to_string() + " are not
equal.");
}
}
Observe the clumsy expression v1.add_to(v2) which we used to add two vectors. A
real timesaver would be a possibility to define addition of vectors in such a way
that we could simply write v1+v2 and have BGT automatically do the right thing.
This is indeed possible and is called operator overloading. An operator is a symbol
which stands for an operation. For example, the plus symbol "+" stands for
addition. Overloading is the process of extending the meaning of a symbol. For
example,
the meaning of the plus operator in BGT is to add numbers and strings. Now we will
extend, or overload, the plus operator so that it carries the additional meaning
of adding vectors.
The way to overload an operator in BGT is to define a method with a special name.
For example, to overload the plus operator we would define a method called opAdd,
and to overload the star operator, we would define a method called opMul. While
we're at it, we can even overload the double equals (==) operator (==) by defining
a method called opEquals. Incidentally, we have already defined all three of those
methods, so we simply have to rename them. Here is the revised code with operator
overloading:
class test_vector
{
double x;
double y;
double z;
test_vector(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
double get_x()
{
return x;
}
double get_y()
{
return y;
}
double get_z()
{
return z;
}
string to_string()
{
return "(" + x + ", " + y + ", " + z + ")";
}
test_vector opAdd(test_vector@ other)
{
return test_vector(x+other.x, y+other.y, z+other.z);
}
test_vector opMul(double scalar)
{
return test_vector(x*scalar, y*scalar, z*scalar);
}
test_vector opMul_r(double scalar)
{
return this*scalar;
}
bool opEquals(test_vector@ other)
{
return x==other.x && y==other.y && z==other.z;
}
}
void main()
{
test_vector v1(3, 4, 5);
test_vector v2(5, 4, 3);
test_vector@ v3 = v1 + v2;
alert("For your information", v1.to_string() + " + " + v2.to_string() + " = " +
v3.to_string());
test_vector@ v4 = 6 * v1;
alert("For your information", "6 * " + v1.to_string() + " = " + v4.to_string());
if(v1 == v2)
{
alert("For your information", v1.to_string() + " and " + v2.to_string() + " are
equal.");
}
else
{
alert("For your information", v1.to_string() + " and " + v2.to_string() + " are not
equal.");
}
}
A careful reader may have noticed at this point that I cheated by also defining a
method called opMul_r. Many of the magical method names used for operator
overloading,
such as opAdd or opMul, also have counterparts with _r appended. The r stands for
reverse, and all it does is tell BGT that we wish to overload the same operator
but with operands swapped. In this case, the method opMul defines what happens when
an expression like v*5 is evaluated for a vector v, and the method opMul_r defines
what happens when an expression like 5*v is evaluated.
An operator with only one operand is called a unary operator. To overload a unary
operator, you define a method with no parameters because the only operand is the
object itself. Here are the two magic method names for the unary operators which
can be overloaded:
� opNeg overloads -
� opCom overloads ~
� opPreInc overloads ++ (prefixed)
� opPreDec overloads -- (prefixed)
� opPostInc overloads ++ (postfixed)
� opPostDec overloads -- (postfixed)
Next we come to the binary operators and a somewhat longer list. A binary operator,
as you may have guessed, is one which acts upon two operands. To overload a
binary operator we define a method which takes one parameter because the other
operand is the object itself. Here is the list of magic method names in all its
glory:
The following operators are also binary but they are considered different as they
are the so-called assignment operators of BGT. An assignment operator is one which
changes the value of a variable or property. For example, when we write the simple
statement
i = 5;
we have used the assignment operator =. Here is the corresponding list:
� opAssign overloads =
� opAddAssign overloads +=
� opSubAssign overloads -=
� opMulAssign overloads *=
� opDivAssign overloads /=
� opModAssign overloads %=
� opAndAssign overloads &=
� opOrAssign overloads |=
� opXorAssign overloads ^=
� opShlAssign overloads <<=
� opShrAssign overloads >>=
� opUshrAssign overloads >>>=
Next we will cover what is known as the index operator. You have probably already
seen how it is used only at that time you may not have been aware of the fact
that it is an operator. It is the syntax you use for retrieving an element from an
array by following the array name with a pair of brackets within which you specify
an index. For example, if we had defined an int array called a, the expression a[3]
would be an instance of using the index operator.
To overload the index operator, we define a method called opIndex. It takes the
index as parameter and returns the value at the given index. Here is an overloaded
index operator for the vector class:
double& opIndex(int i)
{
if(i==0)
{
return x;
}
else if(i==1)
{
return y;
}
else if(i==2)
{
return z;
}
}
An important detail to notice about the above code is the return type. Rather than
merely returning double, this method is returning something called double&. Recall
that we discussed the & symbol before when we introduced the concept of references.
This method returns a reference to a double.
Returning a value by reference is a way to tell BGT not to make a copy of the value
but instead return its location, or address, as if a handle had been used. An
additional advantage of a reference, however, is that it can be used on the left
side of an assignment. This means we can now change the y component of a vector
v by writing
v[1] = 42;
Six operators are left. All of them are binary, and all of them calculate a yes/no
answer to a specific question about the relation between two values. This is
why we call them relational operators. Another common term for them is comparison
operators because they are commonly used to compare values.
We begin with the equality operators == and != which answer the question whether
two values are equal or not equal, respectively. To overload those two operators
we need only define the single method opEquals. It must take exactly one parameter,
and it must also return a bool value.
The four remaining operators are <, <=, >, and >= which answer the question whether
the first operand is less than, less than or equal to, greater than, and greater
than or equal to the second operand, respectively. All four of them can be
overloaded in one fell swoop by defining a method called opCmp. This method should
take
exactly one parameter, and it should return an int value according to the following
rules:
The second step is to use the function type just as you would use any other type
when working with handles. Just as with any other type, the at sign (@) is used
to denote a handle. The following example illustrates how to pass a function via
its handle to another function:
void print_a_message()
{
alert("Message", "The answer is 42.");
}
void main()
{
do_n_times(print_a_message, 3);
}
The interesting part of this source code is the function do_n_times. It takes a
handle to a function and a number as parameters, and its effect is to execute the
given function the given number of times.
Handles to functions are especially useful when writing code which you know will
execute some external function at some point, but you have no way of knowing, at
the time of writing, which function will be executed. It might not even be the same
function for every run of your program. In most cases, you will even know the
function's signature, just not the actual implementation behind it. Should you be
faced with a problem of that kind, remember that your code need not be hard-wired
to execute any particular function but may instead use a handle to any function the
user of your code may wish to define.
13. Using multiple scripts
One very useful feature of BGT is the ability to combine multiple scripts into one
program. This can be handy if you are writing a large game, since it is very
time consuming to look through one script for a certain function. Therefore, we
have the ability to categorise our functions spanning multiple scripts, such as
a script for menu functions, a script for the player, a script for the AI, another
for sound management, etc. In this way, your main script may only be 200 lines
long, for example, instead of 1500.
To use multiple scripts, you have to include them in your main script with the
following statement:
#include "[Link]"
When this happens, the engine will read this script as if it were part of the main
script, and any variables, functions or classes that you use can either be present
in the main script or in one of your includes. Because of this, it is not necessary
to add a main function in included scripts, since once the compiler gathers
the data from all the included scripts, the main function is going to be in your
main script. Therefore if you put the main function in other included scripts you
would have multiple void main() functions and the compiler will flag an error.
When you include a script specifying a relative path, it will first search for the
script in the current working directory. That will usually be the directory in
which your script is stored. If the script cannot be found, it will look in the BGT
includes directory. If the script still cannot be found, the engine will raise
an error.
The end user does not need these include files, since all your included scripts
will be packaged in with your program once it is compiled.
Final notes
Well, that's more or less everything. If you have gotten this far, then you should
have everything you need to get started. At this point, I would strongly recommend
thinking about everything you have learned and going back and re-reading any
sections or parts that don't make sense to you. Play around with the examples
provided
in the tutorial so you become familiar with them, and then feel free to make your
games known to the world. The BGT help file comes complete with a full set of
reference guides for functions and objects, with fully functional examples, which
you can refer to at any point if you are stuck.
All I can do now is wish you the best of luck, and happy coding. Hope to see your
first hit available soon!
Memory Train
In this first tutorial we will develop a game called "Memory Train." No, it will
not involve motorized strolls down Memory Lane, and neither will we deal with
wagonloads
of microchips. This game will train your memory, hence the title.
Contents
The story of every game begins with an idea, an initial metaphorical spark in the
designer's brain. This is the one creative impulse out of which the entire project
evolves. It is vital that we hold on to that initial idea throughout the entire
process of designing, implementing, and testing. Only when we hold on to that first
creative impulse will we later be able to determine if we are still on track and
take corrective measures if it turns out that we are not. Such an initial idea
will usually fit into one small paragraph of text, often a single sentence.
Before we dive into programming tasks, let us talk briefly about the process that
leads from that initial idea to the finished game. This process involves a number
of activities, including gameplay design, story design, sound design,
implementation, and testing. For the purposes of describing the game creation
process, let
us assume that each of these activities is carried out by another person.
The central role is played by the gameplay designer because her job is to create
the high-level design of how the game will operate. If we were to design a card
game, for instance, the gameplay designer would primarily concern herself with
creating the rules of the game. Here are some questions the gameplay designer would
need to ask herself: How many cards does the game use? Will it be a trump game?
Will there be suits and ranks? How will the score be determined? How long should
the typical game take? Will it be possible to play against the computer? Against a
human opponent at the same computer? Over the network? All of these questions
can be answered without drawing a single picture, without recording a single sound
effect, and without writing a single line of code.
In determining the high-level design, the gameplay designer generates the input
required by most of the other team members. For example, the programmer needs to
know the exact rules of the game in order to be able to translate them into code.
Eventually, the combined effort of programmer and sound designer will culminate in
a finished prototype. This is in turn closely inspected by the gameplay designer
to ensure that the finished product will meet her expectations. In case the
prototype does not live up to the gameplay designer's expectations, the appropriate
changes will be performed by the various team members and a new prototype is
created. This loop continues until the gameplay designer determines that the
resulting
prototype is in fact the game she designed. Finally, as with any piece of software,
the game will have to go through the various stages of alpha and beta testing
to ensure its quality. Alpha testing is performed behind closed doors by a
dedicated team of testers, while beta testing is performed by potential customers
volunteering
to give your product a try before it is finally released.
2. Designing Memory Train
The objective in our game is to repeat sequences of tones which get more complex as
the game progresses.
Here's how it works: The game is played using the arrow keys, and each of the four
arrow keys is associated with a specific tone. When the game begins, the computer
randomly plays one of the four tones, and the player has to hit the corresponding
arrow key. Next, the computer plays that first tone again and then adds a second
tone, and the player has to hit the two corresponding arrow keys in sequence. If
the player gets it right, the computer adds yet a third tone, and the player has
to repeat all three tones in sequence by hitting the corresponding keys. This
process continues until the player makes a mistake by either pressing the wrong key
or failing to react within a certain amount of time. When the player makes a
mistake the computer will play a special sound after which the final score will be
announced. The final score is the number of tones which the player was able to
repeat in sequence.
Here's an example:
The computer plays the tone for the up arrow key.
The player hits the up arrow key.
The computer plays the tones for up arrow and left arrow, one after the other.
The player hits up arrow then left arrow.
The computer plays the tones for up arrow, left arrow, left arrow.
The player hits up arrow, left arrow, left arrow.
The computer plays the tones for up, left, left, down.
The player presses up arrow, right arrow, thereby making a mistake.
The game ends, and the final score is 3 points because the longest sequence the
player successfully repeated consisted of three tones.
A key aspect of successfully playing this game is memorizing which arrow key is
tied to which tone. To make this as easy as possible we will implement a keyboard
practice mode in which the player can press the arrow keys and listen to the tones
they generate without any time constraints. To enter this practice mode, the
player selects the appropriate option from a menu. Not only will this make our game
look considerably more professional but it will also allow me to explain how
to create menus in BGT. And if you thought it would end there, let me inform you
that our menu will also have background music.
Exercise:
Reread the above game design and create a list of the sounds the game will contain.
Since this is a tutorial about programming rather than sound design, you will need
to create the sounds that you wish to use for the game. In the below code, we
have selected the filenames that we use for each of these sounds. The four tones
are named [Link], [Link], [Link] and [Link], the error sound is named [Link], and
the music loop is named, appropriately enough, [Link].
It is almost time to actually start programming! But before diving into source
code, let us get some preliminaries out of the way.
Given the fact that you are reading this tutorial, it is quite likely that you have
already installed BGT on your computer. But just in case you haven't, now is
the time to do so.
Next, please create a new, empty directory for this project, and place the sounds
you created inside this directory.
Finally, open your favorite text editor and create an empty text file called
memory_train.bgt in the newly created directory. Some text editors, including
Notepad,
have a tendency to add a .txt extension to any file name that does not already end
in .txt. A tried and tested remedy for this is to include the file name in double
quotes. Another way to prevent the automatic .txt extension is to choose "all
files" in the file type field before clicking the "Save" button.
If you have studied the language tutorial, you will know that every BGT script
contains one or more functions. The word "function" comes from the Latin "functio"
which means "execution" or "performance." In programming, a function is some code
that performs a well-defined task.
A function, in performing its task, can call other functions to carry out subtasks.
The important thing to keep in mind here is that when one function calls another,
the calling function is not finished. This leads to the rather peculiar fact that
several functions may be executing at the same time but only one of them is in
control. As an example from daily life, let us assume that the function
"get_dressed" has called the function "put_on_socks". Now the function
"put_on_socks" is
in control but the function "get_dressed" is not finished. Instead, it is patiently
waiting in the wings for "put_on_socks" to finish. As soon as this happens,
"get_dressed" is back in control and continues executing where it left off, which
will be just after the call to "put_on_socks".
I strongly urge you to reread the previous paragraph until you understand it
completely. One of the main reasons why people find programming confusing is that
they
have misunderstood the concept of functions and function calls. In case you are
still confused, the next time you put on your socks you will feel a warm glow
inside
and the concept of function calls will make perfect sense.
As an added bonus, once a function finishes it may return a value to its caller.
This is appropriately called the function's return value, or simply its result.
It is well worth our time to take a look at the smallest BGT script which could
possibly be written. For although it stops almost immediately after it starts and
does exactly nothing in between, it yet provides the structure, or framework, of
any other BGT script including Memory Train. Since it will eventually become part
of Memory Train anyway, now is also the time to copy it into your memory_train.bgt
file. Here it is, in full:
void main()
{
}
This defines a single function called main. The word "void" specifies that this
function has no return value, which makes sense once you realize that it is the
solitary function in this script and thus has nobody to return anything to. But its
lack of a return value has a much more practical reason, and this has to do
with the special significance of the name "main". The main function is special in
that it is where execution of every BGT script begins, and the execution of every
BGT script ends when the main function finishes. The two braces you see above
embrace between themselves the entire execution of your script.
Let us now make our script actually do something by placing some instructions
between those braces.
Exercise:
Find the alert function in the BGT reference, and with the help of the reference
modify your script to display a message box. The title of the message box should
be "Important Information", and the message text should be "Hello, I am John Doe,
and I am a programmer." Instead of John Doe, put your own name into the message.
Hint on using the reference: The "alert" function is part of BGT's foundation
layer.
void main()
{
alert("Important Information", "Hello, I am Jane Smith, and I am a programmer.");
}
To execute your script, simply save the file, locate it in Windows Explorer and hit
enter on it. Note that this only works when BGT is installed.
Let's see what happens if we add another function call. Our new script is as
follows:
void main()
{
alert("Important Information", "Hello, I am Jane Smith, and I am a programmer.");
alert("How many roads must a man walk down?", "The answer is 42.");
}
When you run this script, you will notice that, as you might expect, it displays
two message boxes, one after the other. This serves to illustrate the point I made
above, namely, that the main function is not stopped when it calls the alert
function. As soon as the alert function has finished doing its first job, control
flows
back into the main function which then proceeds to call the alert function again.
A BGT script can do much more than carry out instructions in sequence. Sooner or
later our scripts must learn to make decisions based on what the user does. Only
then can we develop interactive scripts, and interactivity is, after all, what
differentiates a game from a play.
void main()
{
question("A personal question", "Do you believe in the flying spaghetti monster?");
}
When you execute this script, it displays a message box similar to the one
displayed by a call to alert. However, this time the solitary OK button is replaced
by
a Yes and a No button.
Exercise:
Asking the user a question without reacting to the answer is pretty pointless, so
we will now modify the script to make a decision based on which of the two buttons
the user clicks. As you learned by consulting the reference, the "question"
function returns a value of 1 for the Yes button, 2 for the No button, and 0 if an
error
occurs.
This defines a variable named "answer" which can hold a value of type "int". "int"
is shorthand for "integer" and means a whole number, i.e. a number without a
decimal point. If you would like to learn about the various other types supported
by BGT, let me refer you to the language tutorial.
When we have defined a variable, we can assign a value to it by writing the name of
the variable, an equals sign, the value we would like to assign, and finally
a semicolon:
answer = 42;
In our running example, we don't want to place the number 42 in our variable.
Instead, we would like our variable to store the value that the "question" function
returned. We achieve this by simply replacing our 42 with the call to the
"question" function:
void main()
{
int answer;
answer = question("A personal question", "Do you believe in the flying spaghetti
monster?");
alert("Thank you!", "Your answer was " + answer + ". See you later.");
}
If you try out this script, you will notice that you get different messages
depending on which of the two buttons you choose. This is because the variable
"answer"
receives the value returned by the "question" function. So this is in fact our
first interactive script.
Note also how we used the plus sign to tie several things together into something
larger:
"Your answer was " + answer + ". See you later."
Scanning this from left to right, we find some text in double quotes, a plus sign,
a variable name, another plus sign, and some more text in double quotes. In this
case, the plus signs serve to tie these three things together into the message we
would like to display, like tying together three strings of pearls into a longer
string of pearls. Programmers refer to a piece of text as a string because it is
easily visualized as a string of characters. To use this term in context, we have
just added a string, a number, and another string to form our message, which is
itself a string.
You might be wondering, if the plus sign is used for tying strings together, how
would you add two numbers? The surprising answer is that the plus sign is used
for that purpose as well. The plus sign is a symbol which may refer to different
activities, depending on context. In technical terms, the plus sign is an
overloaded
operator.
In our running example, our script is now able to react differently depending on
which button the user clicks, but our reactions are not yet very meaningful.
Wouldn't
it be great if we could display completely different messages depending on the
user's response? The following script does exactly that:
void main()
{
int answer;
answer = question("A personal question", "Do you believe in the flying spaghetti
monster?");
if(answer == 1)
{
alert("How interesting!", "Thank you for being that honest.");
}
else if(answer == 2)
{
alert("I thought so!", "Now don't tell me the invisible pink unicorn got you
first.");
}
else
{
alert("Whoops!", "Something is dreadfully wrong here! Maybe it's the monster taking
revenge.");
}
}
This script uses the "if" statement to make a decision based on the value of the
"answer" variable. If the value is 1, the first message is displayed. Otherwise,
if the value is 2, the second message is displayed. Finally, if the value is
neither 1 nor 2, the third message is displayed, indicating that an error has
occurred.
A condition is something which can either be true or false. This is in line with
the usage of the word in every-day English. For example, the condition for going
by train is that you are in possession of a valid ticket. If the condition is false
you may not board the train.
The condition to test whether two things are equal looks like this:
a == b
Note the use of a double equal sign. This is required because the single equal sign
is already used for something else.
Exercise:
What is the single equal sign used for? Hint: The answer is contained earlier in
this tutorial.
Note that we have enclosed every branch of the above "if" statement in its own pair
of braces. Strictly speaking, this is necessary only if a branch consists of
multiple instructions. Since all three branches in the above script consist of a
single instruction, namely, a call to "alert", we could just as well have written
the following:
void main()
{
int answer;
answer = question("A personal question", "Do you believe in the flying spaghetti
monster?");
if(answer == 1)
alert("Ramen to you, then!", "Thank you for being that honest.");
else if(answer == 2)
alert("I thought so!", "Now don't tell me the invisible pink unicorn got you
first.");
else
alert("Whoops!", "Something is dreadfully wrong here! Maybe it's the monster taking
revenge.");
}
Over the years of my programming career I have formed the habit of always including
the braces from the start even in such cases where they are not strictly necessary.
This way, when I add more statements at a later date I never have to worry about
whether or not to add another pair of braces.
Just so that you know, the braces in a function definition are mandatory. For
instance, the definition of your main function will always contain braces even if
the function consists of a single instruction---in fact, even if it should consist
of no instructions at all!
Exercise:
Read the relevant parts of the language tutorial to familiarize yourself with the
various programming constructs for making decisions. In particular, pay close
attention to the if, while, and for statements as they will be used in this
tutorial.
In our quest for theoretical background we have digressed quite a bit from our
original endeavor, which was to implement the game we designed in the previous
chapter.
Now is the time to get back to it.
void main()
{
}
Exercise:
The helper layer contains a helpful tool for creating menus. Can you find it in the
reference?
The tool we are after is called dynamic_menu. It allows us to easily put together a
menu in which the player can select an item with the up and down arrow keys
and activate it by pressing enter. As you undoubtedly know, such menus abound in
audiogames, with typical menu items being "start game", "test speakers", "options",
Let's take a moment to summarize what you already know about functions and
variables.
In this section we will go one step further and talk about a kind of variable which
has other variables and functions living inside it. A value with variables and
functions inside it is called an object, and a variable holding an object is called
an object variable, just as a variable holding an int is called an int variable.
If this all sounds just a bit confusing, let me assure you that it will quickly
become second nature to you once you see it in action. In just a few paragraphs
you will realize that objects are all about simplicity.
#include "dynamic_menu.bgt"
void main()
{
show_game_window("Memory Train");
dynamic_menu menu;
menu.add_item_tts("Start game");
menu.add_item_tts("Keyboard practice");
menu.add_item_tts("Exit game");
menu.allow_escape = true;
[Link] = true;
[Link]("Please choose a menu item with the arrow keys, then hit enter to activate
it.", true);
}
Exercise:
With the help of the reference, try to figure out what the above script will do.
Test your hypothesis by running the script. Note: This script contains some
constructs
we have not covered yet, but they will all be explained below.
Let's start with the elements you already recognize. First, you will immediately
have noticed that this is your standard main function with a sequence of
instructions
between the braces. Next, examine the following line:
show_game_window("Memory Train");
This is some name followed by a pair of parentheses with some data between them,
and finally a semicolon. What do we call such a thing? Yes, a function call.
Exercise:
How many parameters does the above function call have?
Exercise:
Find out what the function does.
You may be wondering why we would want to display a window in the first place when
our game will not have any visual elements. The answer is that on Microsoft
Windows,
any program that handles keyboard input should display a window. Our game will only
be able to react to keyboard input when our game window is active. This will
also allow the player to switch back and forth between our game and other running
applications.
Both of these statements are variable definitions. One of them defines a variable
of type int and gives it the name "answer", the other one defines a variable of
type dynamic_menu and gives it the name "menu". And just as int is a data type for
numbers, dynamic_menu is a data type for menus. The word "dynamic" is used here
meaning "flexible" because you, as game programmer, can decide which items the menu
will contain.
Notice the striking similarity to function calls? The only difference is that this
time we are calling a function that lives inside our menu variable. Recall that
an object is a value with variables and functions living inside it. In this case we
have on our hands an object of type dynamic_menu.
To call a function inside an object, we write the variable name, a full stop, and
then the name of the function we are calling. This is followed by the usual pair
of parentheses which, as we have learned, is part of every function call. A
function inside an object is called a method. To use this term in context:
add_item_tts
is a method of the menu object.
An object gets its methods from its type. Thus, to find out what methods an object
provides we need only know what type, or class, the object belongs to. The "menu"
object is of class dynamic_menu, so we need only consult the reference of
dynamic_menu to find out about the available methods.
Exercise:
With the menu set up the way it is, we need to perform one more step to present it
to the user:
[Link]("Please choose a menu item with the arrow keys, then hit enter to activate
it.", true);
This section covers two additional building blocks required for putting Memory
Train together. Let us begin with sound, which will be an essential feature of any
audio game you will ever develop.
void main()
{
sound intro; // Creates the sound object.
[Link]("[Link]"); // Loads the sound; equivalent to putting a tape into the
player.
[Link](); // Starts playing.
while([Link])
{
// Do something while the intro is playing.
wait(5); // Give other Windows tasks 5 milliseconds time.
}
// The sound has stopped.
}
Some games, including Memory Train, make use of a speech synthesizer on the
player's computer. The way to do this in BGT is to use a tts_voice object, where
tts
stands for "text to speech." The two most interesting methods of tts_voice are
speak and speak_wait. The speak method will begin speaking the string which was
passed
to it and will continue speaking in the background while your program may do other
things. In comparison, the speak_wait method will additionally wait until the
speech has stopped, and only then will your program continue with the next
instruction.
3.7 Time
All but the simplest of games need to keep track of time in some way. BGT provides
a timer object for this purpose. The most important features of the timer object
are the restart method and the elapsed property. By calling the restart method,
your program is able to reset the timer back to zero in much the same way as if
you were restarting a stopwatch. The elapsed property will always contain the
number of milliseconds since the timer was last restarted, or, if it was never
restarted,
elapsed will contain the number of milliseconds since the timer was created.
In order for your game to be able to respond to keyboard input, BGT contains two
functions which let you check the status of any given key on the user's keyboard.
Use the key_down function to find out if a given key is being held down at that
moment. Note that key_down will return true as long as the key is being held down.
If you are not interested in how long a key is being held but rather would like to
be informed of every keypress just once, use the key_pressed function instead.
When a key is held down, key_pressed will return true only the first time you check
that particular key, and false on subsequent calls. Only if the key was released
and is now being held down once more will key_pressed return true again.
To find out which keys can be checked and how they are expressed in BGT, consult
appendix A of the BGT help system.
Armed with a basic understanding of the fundamental features of BGT, we can now
begin coding the bulk of Memory Train. Let us start with the main function which,
as you learned earlier in this tutorial, is the heart of any BGT script. The
responsibilities of the main function are as follows:
� 1. It loads the various sounds required by the game into memory.
� 2. It sets up the game menu.
� 3. It speaks a short intro.
� 4. It repeatedly runs the menu and executes the function the user has chosen.
� 5. The repetition ends as soon as the user presses the escape key in the menu or
chooses the "exit game" entry.
While we are at it, we will also define some global variables the main function
refers to. Our code now looks as follows:
#include "dynamic_menu.bgt"
// Sound objects.
sound music;
sound error_sound;
sound[] tone(4); // The four tones used for sequences.
void main()
{
// Load the four tones.
tone[0].load("[Link]");
tone[0].volume = -10;
tone[1].load("[Link]");
tone[1].volume = -10;
tone[2].load("[Link]");
tone[2].volume = -10;
tone[3].load("[Link]");
tone[3].volume = -10;
// Set up voice.
tts_voice voice;
// Set up menu.
dynamic_menu menu;
menu.allow_escape = true;
[Link] = true;
menu.add_item_tts("Start game");
menu.add_item_tts("Keyboard practice");
menu.add_item_tts("Exit game");
// Show game window and speak welcome message.
show_game_window("Memory Train");
voice.speak_wait("Welcome to Memory Train!");
do
{
choice = [Link]("Please choose a menu item with the arrow keys, then hit enter to
activate it.", true);
if(choice==1)
{
[Link]();
play_round(); // We will define this function later.
music.play_looped();
}
else if(choice==2)
{
[Link]();
keyboard_practice(); // We will define this function later.
music.play_looped();
}
}
while(choice!=0 and choice!=3);
If the user chooses the keyboard practice item from the menu, our above main
function will call a function named keyboard_practice. This has the following
responsibilities:
� 1. It speaks instructions on how to operate the keyboard practice mode.
� 2. It waits for the user's keypresses.
� 3. When an arrow key has been pressed, it plays the appropriate tone.
� 4. When escape has been pressed, keyboard practice is aborted.
void keyboard_practice()
{
tts_voice voice;
voice.speak_wait("Press the arrow keys to find out which key generates which
tone.");
voice.speak_wait("Press escape to stop practicing.");
while(!key_pressed(KEY_ESCAPE))
{
if(key_pressed(KEY_LEFT))
{
play_tone(0);
}
else if(key_pressed(KEY_DOWN))
{
play_tone(1);
}
else if(key_pressed(KEY_RIGHT))
{
play_tone(2);
}
else if(key_pressed(KEY_UP))
{
play_tone(3);
}
wait(5);
}
}
Notice how we introduced the play_tone function in the above code for
keyboard_practice. The responsibility of play_tone is to play a single tone. Here
is the corresponding
code:
void play_tone(int i)
{
tone[i].stop();
tone[i].play();
}
This function first stops the tone in case it is already playing, then restarts
playing it. By the way, if the expression tone[i] confuses you, this might be a
good time to read up on the subject of arrays in the language tutorial.
If the user chooses the "start game" item from our game menu, the main function
calls the play_round function. This function is responsible for carrying out an
entire round of play, after which the score is announced. Here is the code for
play_round:
void play_round()
{
// Initialize game state.
bool game_over = false; // The game will continue as long as this is false.
int[] sequence; // The running sequence.
int sequence_length = 0;
float time_between_tones = 500; // The initial speed at which tones are played, in
milliseconds.
float time_between_inputs = 2000; // Maximum time to input next key before player
gets bounced.
do
{
// Add another tone to the sequence.
sequence_length++;
[Link](sequence_length);
sequence[sequence_length-1] = random(0,3);
// Play back the sequence from start to finish.
output_sequence(@sequence, time_between_tones);
// Let them repeat it if they can.
game_over = input_sequence(@sequence, time_between_inputs);
// Every time another five tones have been mastered:
// increase the speed ever so slightly.
if((sequence_length%5) == 0)
{
time_between_tones = time_between_tones*0.9;
// Make sure it does not fall below 150.
if(time_between_tones < 150)
{
time_between_tones = 150;
}
}
}
while(!game_over);
int score = sequence_length-1; // minus 1 because they failed on the last.
tts_voice voice;
voice.speak_wait("Your final score was " + score);
}
The output_sequence function plays a sequence of tones so that the player may try
to memorize it. It requires two parameters, the sequence to output, and the time
between the tones. This flexible approach was taken to enable the game to increase
the speed over time. Here is the code for output_sequence:
The final puzzle piece is the input_sequence function. This function is responsible
for testing if the player remembers the sequence correctly. If the player fails
to press one of the arrow keys in a given time, or presses the wrong arrow key,
this function will return true to indicate that the game is over. If, on the other
hand, the player manages to replay the sequence correctly, then this function
returns false, indicating that the game is to continue. Here is the code for
input_sequence:
bool input_sequence(int[] @sequence, float time_between_inputs)
{
timer clock;
for(uint i=0; i<[Link](); i++)
{
// Do the following for every tone in the sequence:
[Link]();
int input = -1; // Set it to something invalid.
while([Link] < time_between_inputs)
{
if(key_pressed(KEY_LEFT))
{
input = 0;
}
else if(key_pressed(KEY_DOWN))
{
input = 1;
}
else if(key_pressed(KEY_RIGHT))
{
input = 2;
}
else if(key_pressed(KEY_UP))
{
input = 3;
}
if(input>=0)
{
break; // Stop waiting because something was typed.
}
wait(5);
}
// Game is over if timed out or wrong key.
if(input!=sequence[i])
{
error_sound.play_wait();
return true;
}
// Play back successful tones as feedback
play_tone(input);
}
The complete source code for Memory Train is contained in the code fragments given
in sections 3.9 to 3.14. In order to test the game, you can simply paste all
of the code fragments into the file memory_train.bgt which you created earlier. To
run the game, simply navigate to the file memory_train.bgt in Windows Explorer,
then press enter to start.
Final exercises:
� 1. Extend the game to provide a two-player mode. Both players sit at the same
keyboard. Player one uses the arrow keys to play, and player two uses the keys a,
s, d, and w. In this variant, sequences are not randomly generated by the computer.
Instead, player 1 starts with one tone, player 2 repeats the one tone and adds
another, player 1 repeats the two tones and adds a third, etc. The game ends when
one of the two players fails to press a key in time, presses the wrong key, or
presses a key when it was not his or her turn. The player making the mistake loses,
and the other player wins with a score equal to the longest sequence he typed.
� 2. Extend the game so that it provides different difficulty levels. Note that
this is both a design and an implementation exercise.
Windows Attack
Contents
� 1. Introduction
� 2. The name of the game
� 3. Sound design
� 4. Game state
� 5. The sound pool
� 6. The game loop
� 7. Auxiliary functions
� 8. The virus class
� 9. The main function
� 10. Exercises
1. Introduction
Welcome to the second installment of Game Programming in Practice, a series of
tutorials in which you learn about core BGT concepts by seeing how they are used.
As you follow these tutorials you create actual games while the syntax, structure,
and techniques of BGT programming become second nature to you.
Let me take this opportunity to remind you that errors are an integral part of
programming. They happen to the best of us, and rather than seeing them as
frustrating
stumbling blocks, you will do much better considering them as opportunities to test
and even improve your logical skills.
Finally, remember to take frequent breaks. Stepping back from a problem for a while
and returning to it refreshed might make all the difference.
With these formalities stored safely in the back of your mind, let's get going!
2. The name of the game
In this tutorial you will create a game called Windows Attack. If you have ever had
trouble with computer viruses in the past, you will be delighted to know that
this game finally lets you take sweet revenge.
Here is the basic idea: You are an antivirus program which must target and destroy
approaching computer viruses before they reach and infiltrate the system!
Let's flesh out this basic idea until it becomes a real game.
Computer viruses will fall towards your system from above, making noise as they
approach. Your job is to target a virus with the left and right arrow keys until
its sound is in the center of your stereo field, then hit the space bar to trigger
your deadly digital destruction device of doom. If you make it in time, you will
score some points depending on how well you performed. However, if a virus has made
it all the way down, it will do some damage to your system. Now, the system
can only stand so much damage until it crashes with the dreaded blue screen of
death, at which point the game will be over.
3. Sound design
Exercise
Based on the game's description in section 2, can you come up with a list of
required sounds?
// Game state
int player_position; // Current horizontal position of player
virus@[] board; // Locations without viruses will contain null
int lives; // Game over if this falls to zero during play
int score; // Current score
If you have completed the first tutorial in this series or read some of the BGT
language tutorial, understanding the above code won't be much of a problem for you.
Let us pause briefly, however, to inspect the most complicated of the lines above:
The name of the variable declared here is board, and the data type is virus@[].
This is best understood when reading it from right to left: The brackets tell us
that this is an array, the @ sign indicates it is an array of handles, and the word
virus specifies the type of object to which the handles will refer. The virus
class will be defined later in this tutorial.
It is a useful habit to define a function which initializes the game state so that
whenever we wish to start a new game, we need only call this function and can
rest assured that each game begins in a well-defined initial state.
You might argue that it is unnecessary to explicitly set all the board locations to
null. After all, null is exactly the value with which handle variables get
initialized.
However, remember that we might want to play multiple games in one session, and so
the board might still contain leftover viruses from the previous game. The lesson
here is that a little extra housekeeping never hurts and leads to well-defined
states at key points in the flow of your program.
Also, note that we have defined constants for the board size and initial number of
lives. In general, it makes sense to define constants for values which will
eventually
be fixed but which you might change from time to time in your development process.
It is of course perfectly valid to write all constants as literal numbers, but
the advantage of a named constant is that you can change its value at the point of
its definition and the change will apply to every point in your source code where
the constant is used. As a general rule, any value which is used more than once in
your source code should be given a name.
5. The sound pool
In the first part of this series of tutorials, you learned that sounds are played
using sound objects, and that the number of sound objects you need is the number
of sounds your game will play simultaneously. For small projects like Memory Train
you will usually manage the creation, utilization and destruction of those sound
objects in your own code. But when matters start to become more complex, such as
when lots of sounds need to be created or repositioned in response to events in
the game world, managing all the sound objects yourself can be a nuisance. For
these situations, BGT contains a powerful helper class called sound_pool. It is
called
a pool because it can manage a large collection of sound objects and use them as
needed. For example, when you order the sound pool object to play a sound, it will
look through its pool of sound objects to find one which is currently inactive, and
only if none can be located will a new sound object be created.
All this may sound dreadfully complicated at first, but the good news is that the
sound pool class is incredibly easy to use because you do not need to concern
yourself with most of its technical intricacies. You simply order it to play the
right sound at the right time and place, and the sound pool will take care of the
rest.
Another killer feature of the sound pool is that it can automatically position the
sounds for you. You simply tell it the coordinates of your sound sources and
your listener, which will usually be the player, and once again, the sound pool
will take care of the rest.
Exercise
Consult the BGT reference about the sound_pool class and familiarize yourself with
its methods.
One potentially confusing aspect of the sound pool is its use of so-called sound
slots, so let us address them right away to prevent you from forming the wrong
model about them.
When you ask the sound pool to play a sound, for example by calling the play_2d
method, it will return a number called a sound slot. The sound slot is simply a
number which the sound pool has assigned to the sound, in much the same way as the
government assigns social security cards to citizens when they get their first
jobs. The government uses social security numbers to quickly find or update a
person's data. That's why they ask you for this number whenever you call them. In
much the same way, when you ask the sound pool class to update a sound which is
already playing, you will need to provide the sound slot which was returned to you
when you initially started the sound. If some of this doesn't seem to make sense,
just read on, and all will be revealed.
6. The game loop
We already dealt with the initialization of the game state. Let us now turn to the
game itself while it is in progress.
Traditionally, this is expressed as a loop which drives the action and which is
therefore sometimes called a driver loop. The job of the driver loop is to run
until
the game is over and to orchestrate the flow of the various components of the game.
In other words, the driver loop is responsible for the movement of time. Let's
see what this would look like:
sound_pool pool;
void game_loop()
{
timer virus_act_timer;
timer virus_spawn_timer;
while(lives > 0) // We loop until the game is over
{
player_act(); // Check if player pressed a key and let him move or shoot
if(virus_act_timer.elapsed >= 200) // Viruses move 5 times a second
{
viruses_act();
virus_act_timer.restart();
}
if(virus_spawn_timer.elapsed >= 3000) // New virus every 3 seconds
{
virus_spawn();
virus_spawn_timer.restart();
}
wait(5); // Be nice to other apps on this machine
}
}
7. Auxiliary functions
Our game loop references a number of functions which we still have to define.
First, there is player_act, the function which allows the player to move and to
shoot
at viruses.
void player_act()
{
if(key_pressed(KEY_LEFT) and player_position>0)
{
player_position--;
}
else if(key_pressed(KEY_RIGHT) and player_position < (board_size-1))
{
player_position++;
}
if(key_pressed(KEY_SPACE)) // Shoot
{
shoot(); // Let's do this in its own function as it is more complex
}
if(key_pressed(KEY_ESCAPE)) // Exit game
{
lives = 0; // Simulate game over
}
pool.update_listener_2d(player_position, 0);
}
void shoot()
{
if(@board[player_position] is null) // Missed
{
pool.play_stationary("[Link]", false); // Centered, not looping
}
else // Hit
{
board[player_position].die(); // We tell the virus it has died
score++; // Player gets a point
@board[player_position] = null; // No more virus at this position
}
}
Now, let us define the function viruses_act, which gives each virus currently on
the board a chance to move.
void viruses_act()
{
for(int i=0; i<board_size; i++)
{
if(@board[i] !is null)
{
board[i].act();
}
}
}
Finally, let us define the function virus_spawn, which randomly selects a location
on the board and, if the location is still vacant, places a new virus there.
void virus_spawn()
{
int location = random(0, board_size-1);
if(@board[location] is null) // Check if it is vacant
{
virus newbie(location); // Create a new virus
@board[location] = @newbie; // Register it with the game board
}
}
8. The virus class
As if the previous two sections hadn't been code-intensive enough, let's complete
the picture by defining the virus class.
class virus
{
int height; // Height above ground. When this falls to zero the virus lands
int falling_sound; // Stores the sound slot for the falling sound loop
int location; // Location of virus on the game board
virus(int location) // Constructor
{
[Link] = location;
height = 20;
falling_sound = pool.play_2d("virus_fall.wav", player_position, 0, location,
height, true);
}
void act()
{
height--;
if(height<=0) // Virus has landed
{
pool.destroy_sound(falling_sound);
pool.play_2d("virus_land.wav", player_position, 0, location, 0, false);
lives--;
@board[location] = null;
}
else
{
pool.update_sound_2d(falling_sound, location, height);
}
}
void die()
{
pool.destroy_sound(falling_sound);
pool.play_2d("virus_hit.wav", player_position, 0, location, height, false);
}
}
9. The main function
Our mission is almost complete. We have defined the game state and its
initialization, and we have specified the game loop along with all its helper
functions.
Let us conclude by defining the main function which will set all of it in motion.
void main()
{
show_game_window("Windows Attack");
sound start_sound;
start_sound.load("[Link]");
tts_voice voice;
[Link]("Welcome to Windows Attack!");
start_sound.play_wait();
initialize();
game_loop();
pool.destroy_all();
sound game_over_sound;
game_over_sound.load("game_over.wav");
game_over_sound.play_wait();
voice.speak_wait("Game over! Your score was " + score);
}
Let us take just a few minutes to go over what you learned in the first two parts.
In part I, Memory Train, you took a straight dive into the syntax and basic
functionality of BGT. You learned how the execution of your game program can be
described
in terms of functions calling other functions and making decisions based on what
they return. More specifically, you saw how to display a game window and ask for
keypresses, how to load and play sounds and background music, how to keep track of
time, and, last but not least, how to exit your game.
In Part II, Windows Attack, you took a more systematic approach to game design. You
took the description of a game and extracted from it a list of required sounds,
a description of the game state, and finally, the source code of the game itself,
complete with a self-made script class. In addition, you learned to harness the
power of a sound pool to position an arbitrary number of sounds in real-time.
This final installment will provide you with some powerful programming techniques
and design approaches to make your life easier as you begin to work on larger-scale
projects. You will probably not learn any new syntactic constructions here, nor
will the mini game you will write be anywhere as spectacular as those you
programmed
in the first two parts. Instead, this part is designed to put the tools into your
hands which, combined with imagination and creativity, will allow you to create
games of high software quality and consistent user experience.
If anything in this tutorial should appear confusing or unclear, chances are you
will find the missing puzzle pieces in the language tutorial, the BGT reference,
or, indeed, in the first two parts of this series. Sometimes it is also a good idea
to just continue reading with dogged determination, and you might find the answer
in the next paragraph. In any case, you should take frequent breaks, maintain a
calm, rational, detective-like approach to complexity, and remember that bugs
happen
to the best of us, and are valuable lessons only real life can teach.
the Kingdom of Quang stood proudly. Proudly indeed it stood, impervious to the
ravages of slime, until its two supreme rulers, during one of their regular and
excessive
drinking binges, got into a fateful and bloody argument about who stole the cookies
from the cookie jar. The question was never answered, but such was the monstrosity
of the ensuing war that the mighty Kingdom of Quang fell and sank beneath the sea.
The wizards of Quang, however, having gazed deeply into their crystal balls,
knew that tragedy was afoot, and managed to transfer all the knowledge of their
high culture into a powerful magical artefact---the Amulet of Quang.
You, a secret agent of the even more secret world government, have been transported
to the vast underground empire of Quang. Your mission is to recover the Amulet
of Quang and transport it back to the surface of the earth.
3. Let's get serious
Here is a more serious description of the game.
The Amulet of Quang is an audio adventure in which the player can explore his
surroundings, walk from place to place, and interact with the various objects which
litter the game world. Every area of the game map may provide its own background
music, and objects may or may not emit sound. The player will be informed by means
of a spoken message when the main character spots a new object. The player can
obtain a detailed description of an object by examining it. Some objects can be
picked
up. The objects the player is carrying are organized into an inventory list which
can be accessed at any time during the game. It is possible to use objects in
the inventory list. The game can be paused at any time.
4. Designing the interface
Exercise
Analyzing the description in the previous section, create a detailed specification
of the game's interface. The interface consists of everything the player needs
to know in order to successfully play your game. Mostly, this would be the kind of
information you might put into a user manual. Here are a few questions to put
you on the right track:
1. How does the player walk around the map?
2. When and in which way is the player informed of available objects?
3. Will there be footstep sounds?
4. How does the player examine, pick up, or use an object?
5. How does the player access the inventory list?
6. How does the player examine or use an object in the inventory list?
7. How does the player pause and unpause the game?
8. Finally, how would the player exit the game?
Here is my solution. In case our answers differ, this does not mean that you have
got it wrong, but merely that we have chosen to create slightly, or maybe even
dramatically, different kinds of games. The main reason why I am detailing my
solution here is to show you the level of detail required for a user interface
specification.
In general, the more complete your specification at design time, the less trial and
error at coding time. It is perfectly reasonable at design time to leave certain
details open to discussion or change, but this should happen as the result of
deliberate choice rather than unconscious omission. So let us agree for the moment
to stick to my specification, and if afterwards you feel that your solution is
worthwhile trying, I shall leave it as an exercise to you to go back and redo from
that point onward. In fact, I highly recommend doing so. Also, do not be put off if
it seems to take days, or even weeks, to get exactly right. This is to be expected.
The player walks around the game map by using the four arrow keys. Pressing and
then immediately releasing an arrow key will move the player exactly one step in
the corresponding direction. Holding an arrow key will move the player one step in
the corresponding direction every half second. In this way, a steady motion is
established just by comfortably holding down a key, but an impatient player can
still accelerate the process by tapping the key more quickly.
To give the player feedback that motion has indeed occurred, the game will play a
footstep sound for every step the player takes. If, after taking that step, the
available directions for the next step have changed, we will play a slightly
modified footstep sound to alert the player to that change. For example, assume the
player is walking down a long east-west corridor, and suddenly comes upon an exit
to the north. This would be the time when our modified footstep sound would be
played. So we define two footstep sounds, one of them soft and unobtrusive, the
other slightly more demanding, which might be accomplished by making it slightly
louder or higher in pitch.
Let's move on to what we might call the model of visibility. This is the set of
rules which determine what objects the player sees at any given time. Such a model
could get arbitrarily complex, taking into account such details as distance and
size of objects, lighting conditions, fog, or other objects which obstruct the
view.
For this design, however, we will keep it simple by defining just two basic
visibility rules. An object is visible if
1. it has the same x or y coordinate as the player, i.e. it is straight to the
north, east, south, or west,
and
2. there is no wall anywhere between the player and the object.
Whenever the player moves such that a new item is spotted, we will provide a spoken
message stating what kind of object it is, in what direction it lies, and how
many steps it would take to get to it. Note that we will only speak the objects as
they move into view, not those which were already visible before. For example,
if the player took one step north, we wouldn't need to speak any visible objects to
the north or south because they would have been visible before taking the step.
If this sounds confusing, I recommend you reread the two visibility rules above and
then form some examples in your mind to illustrate how they work. Specifically,
it is important to understand that taking a step to the north or south can only
change what is visible to the east or west, and vice versa.
A general description of the area at large is spoken when the player enters it, and
repeated as requested via the l command. Note: l as in look.
To examine, pick up, or use an object, the player must first move to that object's
location. A special sound is then played to indicate that an object has been
reached, and the object is announced. To examine it, the player uses the x command.
The t command will attempt to pick up the object. Finally, hitting the space
bar will attempt to interact with the object in some way. For instance, hitting the
space bar on a door might try to walk through that door, and hitting the space
bar on a gong might strike the gong.
The tab command will cycle through the objects in the player's inventory. To use an
object in the inventory, the player first cycles to this object by pressing
tab until the object is announced, and then presses u, as in use. To examine an
object in your inventory, you would first cycle to it by pressing tab repeatedly,
and then press i, as in inventory information.
To pause the game at any time, the player can press the p command. The same command
also resumes a paused game.
Finally, the escape key will exit the game. The game may also exit when the player
wins or dies, but escape is always available to exit prematurely.
Pressing page up or page down will allow the player to adjust the volume of the
music. This is especially useful in conjunction with spoken messages, because the
music might drown out the voice if too loud.
To make the experience of learning the interface as smooth as it could possibly be,
we will implement a help feature which will make our game self-documenting.
Help can be accessed at any time by pressing f1, whereupon the game will provide
spoken information about all the possible keyboard commands at that time.
5. Music
Exercise
One important aspect of game music is its ability to gracefully fade in and out.
Write a fade function which can be called as follows:
fade(sound@ noise, double start_volume, double end_volume, double time, bool
interruptable);
When called, this function should fade the given sound object from the start volume
to the end volume in the given time in milliseconds. If interruptable is true,
the player can interrupt the process by pressing the space bar. The fade function
returns true if the player interrupted the process using the space bar, and false
otherwise.
bool fade(sound@ noise, double start_volume, double end_volume, double time, bool
interruptable)
{
[Link] = start_volume;
timer t;
double elapsed = [Link];
while(elapsed < time)
{ [Link] = start_volume + (end_volume-start_volume)*elapsed/time;
wait(10);
elapsed = [Link];
if(key_pressed(KEY_SPACE) && interruptable)
{
return true;
}
}
[Link] = end_volume;
return false;
}
We will use a sound object to play our in-game music, so let us declare this object
now:
sound music; // The one sound object used for all music
Next, we need a variable to keep track of the current volume of the music as the
player configured it using the page up and page down commands:
Now let us define a function to change the currently playing music. This function
will accept as parameter the filename of the music to change to. The elegant thing
about this function is that it will fade out the currently playing song, then fade
in the new one, resulting in a graceful transition from one track to the next.
And while we are at it, let's define some more music-related functions:
Note that in the remainder of our source code, we will never manipulate the music
object directly, but only through the functions we just defined. Software
engineering
calls this approach modularization. It makes sure that whenever something is wrong
with the music, we need only check the correctness of our music functions, because
since all music-related activity takes place through these functions, this is where
any music-related bug would logically be found.
sound_pool pool; // The sound pool used for all sound effects
Here is my solution:
� [Link]: A standard footstep
� step_alert.wav: A footstep after which the available directions have changed
� [Link]: The player has reached an object
9. Inventory
Now we are slowly making our way from the very general-purpose modules to the more
game-specific functionality. In this section we deal with the player's inventory
list, a dynamically expanding or shrinking list of all the items the player is
currently carrying. We are going to use an array to keep track of the handles to
the items in the player's inventory, and we will provide functions through which
this array is manipulated.
// Removes an item from the inventory; returns true on success, false on failure
bool inventory_remove(item@ entry)
{
uint index; // For searching the inventory for the entry
for(index=0; index<[Link](); index++)
{
if(inventory[index] is entry) // Found it
{
break;
}
}
if(index >= [Link]()) // Entry not found
{
return false;
}
// Move all above entries one slot down
for(uint i = index+1; i<[Link](); i++)
{
@inventory[i-1] = @inventory[i];
}
// Finally, make the inventory one slot smaller
[Link]([Link]() - 1);
inventory_position = -1; // Make it invalid
return true;
}
void inventory_cycle()
{
if(inventory_position+1 >= int([Link]()))
{
inventory_position = 0; // Go back to first if at the end
}
inventory_position++;
}
10. Items
The inventory list is all about managing and cycling through the items in the
player's possession. Let us now turn to the question of what an item is in the
first
place.
In an adventure game, an item can be defined as any object with which the player
might want to interact. For example, if one of the game's challenges consisted
of unlocking a door with a key, both the door and key would be considered items. We
use the word item here as a generic term for any object in the game world,
including
treasures, monsters, doors, and weapons.
For our purposes, an item must provide at least three pieces of information:
1. A name; this is how the game refers to the item when describing what the player
sees, or when cycling through the inventory.
2. A description; this is what the player hears when examining the item.
3. A takeable flag; this determines whether or not the item can be picked up. Note
that flag is just programmer lingo for bool. Setting a flag to true is sometimes
referred to as just setting it, and setting it to false can also be called clearing
it.
Let us translate this into code:
class item
{
string name;
string description;
bool takeable;
item()
{
alert("Programming Error", "Cannot make item without parameters.");
}
item(string name, string description, bool takeable)
{
[Link] = name;
[Link] = description;
[Link] = takeable;
}
}
11. Areas
Areas comprise the geography of an adventure game. They have been known under
various names, including rooms, levels, or maps. But no matter how we refer to
them,
they are the world through which the player navigates.
Some mainstream games go out of their way to simulate real world geography,
complete with heights, textures, and weather conditions. While all of this is
certainly
possible in BGT, for simplicity's sake we shall stick with a basic setup here,
keeping track of just the locations of walls and items on our maps. We represent
this information in two arrays, both of them two-dimensional. In addition, every
area will provide a general description of where the player is located. This will
be announced when the player chooses to look around by pressing the l command.
class area
{
int size_x; // East-west size of area
int size_y; // North-south size of area
bool[][] walls; // true for wall tiles, false for floor space
item@[][] items; // Items on the map
string description; // Announced when looking around
area()
{
alert("Programming Error", "Cannot construct area without parameters.");
}
area(int size_x, int size_y, string description)
{
this.size_x = size_x;
this.size_y = size_y;
[Link] = description;
[Link](size_x);
[Link](size_x);
for(int i=0; i<size_x; i++)
{
walls[i].resize(size_y);
items[i].resize(size_y);
for(int j=0; j<size_y; j++)
{
walls[i][j] = false;
@items[i][j] = null;
}
}
}
void add_wall(int x, int y)
{
walls[x][y] = true;
}
void add_item(item@ entry, int x, int y)
{
@items[x][y] = @entry;
}
bool get_wall(int x, int y)
{
if(x<0 or x>=size_x or y<0 or y>=size_y)
{
return true; // The wall of the universe
}
bool ret = walls[x][y];
return ret;
}
item@ get_item(int x, int y)
{
item@ ret = @items[x][y];
return ret;
}
}
12. Player position
To represent the player's current position, we need to store the current area as
well as the player's current x and y coordinates.
area@ player_area;
int player_x;
int player_y;
13. Looking around
Let us now define a function which looks out from the player's location in a given
direction, and reports on what the player sees. This function takes an x and
a y increment. For instance, to look eastward, you would supply an x increment of 1
and a y increment of 0. To look south, you would supply an x increment of 0
and a y increment of -1.
1. Generate a pack file using the pack_file object in the engine. In this pack you
store all the files that you wish to include in the final executable.
#include "[Link]"
Replace [Link] with the name of the pack that you created in step 1. The
engine automatically detects whether a given include file is a pack, or a regular
script file. Note that you may not include more than one pack file in each BGT
game.
3. When it is time to access the pack file, simply specify * as the pack file name
wherever the name of the original pack file would normally have been given. For
example, at the start of the main function you might write:
set_sound_storage("*");
If you run this script from source, the sounds will be searched for in the pack
file on disk just as if you had specified the name that you gave in the include
statement. However, when this script is compiled into an executable it will refer
to the contents of the pack file that is stored inside the program itself.
You may access the contents of the pack directly using the pack_file object, as
follows:
pack_file pack;
[Link]("*");
The same rule as above applies regarding execution of the script from source as
opposed to when it is being run as an executable. In short, whenever a script is
run from source the pack file that was included will be accessed directly on disk.
4. You may now compile your script, and the given pack file will be included along
with your compiled game code in the resulting executable. As mentioned above,
* should be used wherever the real pack file name would have been specified if you
wish to access the pack inside the program.
Pathfinder Tutorial
Contents
� 1. Introduction
� 2. What does the pathfinder do?
� 3. Specifying the size of the map
� 4. Setting a callback function
� 5. Putting it all together
� 6. Exercises
1. Introduction
Mazes, or maps, abound in games, and this would include audiogames as well. If you
have played a sidescroller before, you will have navigated a maze with the four
primary directions of right, left, up, and down. A first person shooter, on the
other hand, will usually present you with a type of map in which up and down are
not so relevant; instead, you usually walk forward with the up arrow key and turn
around with the left and right arrow keys.
Both cases are examples of two-dimensional maps, in which positions are given by
two coordinates, usually called x and y. In the case of a sidescroller, increasing
the value of x might mean moving right, and increasing the value of y might mean
moving up. We say that the x axis points right and the y axis points upward. In
the first person shooter we might have the x axis pointing east and the y axis
pointing north.
Part of the challenge of most map-oriented games is navigation. The player listens
for the location of an object, turns in that direction, walks forward, finds
a dead end, backtracks, tries another route, and so on. As game developers we
sometimes wish to bestow the same kind of intelligence, or appearance of it, upon
entities other than the player. We want our enemy robots to chase the player,
skillfully circumventing acid pools as they do so. We want our peasants to go about
their daily business without bumping into walls. And as the game world changes, we
want its inhabitants to be smart enough to correct their course.
2. What does the pathfinder do?
Simply put, the pathfinder object finds a path from a source point to a destination
point on your game map. The path is represented as an array of vectors, in other
words, a list of points.
3. Specifying the size of the map
Before the pathfinder can even begin its work, it needs to know the size of the
game map. You specify this by calling the create_map method, as in the following
code:
void main()
{
pathfinder holmes;
holmes.create_map(10, 5);
// More code here.
}
This would tell the pathfinder that your game map has a width of 10 and a height of
5 squares. In other words, your x coordinates will be in the range from 0 to
9, while your y coordinates will be in the range from 0 to 4.
4. Setting a callback function
If you are new to BGT, or indeed new to programming, you may not have seen the
concept of a callback function before, so a brief explanation is in order.
Imagine the following situation: you are still at the office, and you know that
upon arriving home you have only twenty minutes to catch a train, so you call home
to ask a family member to pack a suitcase for you. Five minutes later your family
member calls you back to ask whether you wish to take the black tie or the red
tie with you, to which you respond that you would like the red one. Another ten
minutes later she calls you back once again to inquire if she should prepare a meal
before you leave, to which you respond that you probably don't have enough time.
Another twenty minutes later she calls you back yet again to ask how long you will
be away.
Let us now translate this into programming terminology. Asking a family member to
pack your suitcase means calling a method called pack_my_suitcase on an object
of class family_member, like so:
void main()
{
family_member matilda;
matilda.pack_my_suitcase(); // Impolite but to the point.
}
Now, what if Matilda needs more information to fulfill your request? Let us define
a function to take care of her questions:
string my_callback(string inquiry)
{
if(inquiry == "Tie color?")
{
return "Red.";
}
else if(inquiry == "Prepare meal?")
{
return "Nope.";
}
else if(inquiry == "Time away?")
{
return "Three days.";
}
else // We don't understand the question.
{
return "Beats me.";
}
}
Finally, let's modify our main function to tell Matilda about our callback function
so she can call it if necessary:
void main()
{
family_member matilda;
matilda.set_callback_function(my_callback);
matilda.pack_my_suitcase(); // Impolite but to the point.
}
Just like in our real-life scenario above, Matilda will call our my_callback
function repeatedly until she has gathered all required information. Note that we
never
call my_callback directly; we simply provide it in case Matilda might need to call
it. Providing a callback function is indeed very much like leaving a phone number
in case there are any questions. We never call our phone number directly, but it
gets called by another entity if that other entity deems it necessary.
Let us now apply what we just learned to the pathfinder object. Up until now we
have told it only the size of our map. If we now ask it to find a path between two
locations, it cannot perform this operation without asking us a lot of questions
because it doesn't know anything about our map except its size; in particular,
it has no idea where walls are blocking the path, doors might need to be opened
first, or acid pools might make travel hazardous if not impossible. In short, we
need to provide a callback function which tells the pathfinder how difficult it is
to get from one square to an adjacent square. The pathfinder will always decide
on the least difficult path, adding up the difficulties along the way.
What does our callback actually return? We hinted at this a few paragraphs ago by
saying that it returns the difficulty of getting to one square from an adjacent
square. This difficulty, otherwise known as cost, or risk, is simply an int in the
range 0 to 10. A value of 0 means there is absolutely no risk or cost associated,
while a value of 10 means it is impossible to go there, just as impossible as
walking through a wall unless you happen to be a ghost, or possibly Chuck Norris.
So 0 means free right of passage, while 10 means no way. Of course, some paths are
neither completely clear nor impossible to take, and these will have cost values
greater than 0 but less than 10. For example, you might assign a value of 2 to
shallow water, a value of 6 to deep water, and a value of 9 to fire.
In our running example, the callback function simply consults an array called maze
to see if there is a wall at the location in question. If so, it returns 10,
indicating that it is impossible to go there. Otherwise it returns 0, indicating
the way is clear.
Of course we need to tell the pathfinder about our callback function. The following
code demonstrates this:
void main()
{
pathfinder holmes;
holmes.create_map(10, 5);
holmes.set_callback_function(maze_callback);
// More code here.
holmes.destroy_map();
// Maybe even more code here which doesn't need pathfinding.
}
As you can see, we tell the pathfinder about our callback by calling its
set_callback_function method. This method takes a single parameter, which is the
callback
function itself, in this case, maze_callback. If you are curious how we managed to
pass a function to another function, consult the language tutorial about the
funcdef keyword. Note that this is entirely optional; you don't need to know how it
works in order to use it. Such are the joys of well-designed programming languages.
In the code above, we seized the opportunity to introduce yet another method,
called destroy_map. This method will free some memory that the pathfinder uses
internally
to calculate paths, and so it is recommended you call destroy_map when you won't
need the pathfinder for some time, and then call create_map once more if you need
it again. For the technically interested, this memory is also freed as soon as the
pathfinder object is destroyed. It is, however, entirely safe to ignore the
previous
sentence if it doesn't make sense to you. Object creation and destruction are
covered in great detail in the language tutorial.
5. Putting it all together
Now that we covered the infrastructure, let's do some real pathfinding! It is time
you studied the source code example given in the pathfinder chapter of the BGT
reference. You might want to copy it to a file on your hard drive in order to
experiment with it, and to be able to have it open in another window while you
continue
reading.
You might begin by mindfully reading the code top to bottom to see which components
you recognize. Can you find where the pathfinder object is created, and the
map size is defined? Can you spot the callback function, where it is defined, and
where it is passed to the pathfinder object?
The program is well-commented so you should have no trouble figuring out what it
does. You might call it a maze solver. It lets you walk around a grid using the
arrow keys, placing and removing walls along the way. Finally, you can define a
starting location, move to the destination location, and let the solver do its
magic.
What you will hear is a list of directions from start to destination. Now, I wish I
would have had something like this when I was playing Zork.
Let us look at the statement that actually asks the pathfinder to find a path for
us:
The find method takes as arguments the starting coordinates, the destination
coordinates, and finally a string called user_data. This string is not used in the
example, and so it is left empty. As it happens, this is the exact string which
gets passed to the user_data parameter of your callback function. This might be
useful when you want the callback function to behave differently depending on the
circumstances, or indeed depending on the reason why a path needs to be found.
The find method returns an array of vectors. For the purposes of the pathfinder, a
vector is simply a pair of x and y coordinates, i.e. a point on your map. So
the elements of the array make up the route of travel from starting point to
destination. If no path could be found, the find method will return an empty array.
6. Exercises
1. Modify the example such that the user can place acid pools at arbitrary
locations on the map. An acid pool is not entirely impossible to walk through, but
since
it is quite an unpleasant experience the solver should minimize contact with them.
2. Now let's add another kind of hazard. Modify the example such that the user can
place puddles of water at arbitrary locations on the map. Such puddles are much
less inconvenient than acid pools, but the solver should still avoid them if
possible, except, of course, if this involves walking through acid instead.
3. Can you modify the callback function such that moving east is more difficult
than moving west, and moving north is more difficult than moving south?
4. Can you make the feature of exercise 3 optional, but still use only one callback
function?
5. Make a ghost which can pass through walls, but only when going east.
6. In a first-person shooter, how might you code an enemy who chases the player?
How can you ensure the enemy is smart enough to correct its course when the player
moves?
7. Look up the desperation_factor property of the pathfinder object in the
reference. Can you see how this would be useful? Consider the example of enemies
becoming
more desperate when the player approaches the end of the level. What other
scenarios can you think of?
Performance Optimizations
Introduction
You have a game that you are relatively happy with. It has nice sounds, seems
stable, and is enjoyable. However, you start to notice that as the game grows, it
is becoming slower and slower. Is all hope now lost? Not quite.
There are plenty of steps you can take to make your script run optimally. Below is
a summary of the most effective techniques which should make you notice a
difference
immediately. They are ordered roughly after how useful they are in a general gaming
context.
Optimize sounds with preloads
In larger games, you most likely have a lot of sounds that are playing
simultaneously. Often times you will probably be using the sound pool to manage
your audio
environments. The sound pool loads all the sounds that you ask it to play, into
memory in their entirety. It does not stream them as this would create a lot of
problems when the number of playing sounds grew. However, loading sounds from disk
is very costly in terms of CPU cycles, and as a result it has a great impact
on the general performance of the game. To combat this problem, BGT has a very
useful but often overlooked feature. Whenever you load the same sound more than
once,
it does not actually read the sound from disk again. It simply tells the new sound
object to refer to the same internal memory location as the existing one does
where the actual data is stored, while still giving you a completely independent
sound to work with in terms of pan and volume etc. This is called cloning. The
benefits of cloning become especially apparent for encrypted Ogg Vorbis files as
the engine does not only have to decrypt the data after reading it from disk, but
also decode it which takes an additional amount of time that may be significant if
the sound is more than a few seconds in length. If this takes place in the middle
of gameplay where a lot of things are happening, you will notice a severe lag as
well as possible stuttering of the sounds that are already playing. As mentioned
earlier, the sound pool loads all the sounds from disk whenever you ask it to play
them. Thus, you will run into this performance bottleneck very quickly with the
sound pool as well as with any other code that loads sounds from disk frequently.
Luckily, there is a very easy solution to this problem. It is to simply load all
the sounds into memory at the start of each level. When this is done and what
sounds
are loaded will naturally depend on the characteristics of your game, but the idea
is to load any sound that you might need during the game before the actual gameplay
starts. You keep these sounds around constantly, usually in a global array, so that
they never go out of scope and are thus never unloaded until you specifically
want them to be. When the sounds are loaded, the BGT engine is able to clone all of
them when the sound pool or any other piece of code attempts to load them. The
result is that while you will introduce a more or less noticeable lag while the
sounds are initially loaded, you will also see that performance increases
dramatically
during the gameplay as nothing needs to be loaded from disk. Disk reading and
writing is one of the slowest processes in an operating system, and Windows is no
exception. Add decryption and Ogg Vorbis decoding on top of this, and you will see
that the benefits of preloading sounds really cannot be stressed enough. Below
is a basic example of how the preloading mechanism can be constructed:
// Create a small class that will hold the sound handle for each preload along with
its filename.
class preload
{
sound@ handle;
string name;
}
preload[] preloads;
// The following function takes a filename as its only argument and loads the sound
as a preload.
// It returns true on success and false on failure.
// It will fail if the sound has already been loaded as a preload, or if something
goes wrong while actually loading it.
// Verify that the sound hasn't already been loaded as a preload by comparing the
filenames.
for(int i=0;i<[Link]();i++)
{
if(preloads[i].name==filename)
return false;
}
sound temp;
[Link](filename);
if([Link]==false)
{
return false;
}
preload loader;
@[Link]=temp;
[Link]=filename;
preloads.insert_last(loader);
return true;
}
void reset_preloads()
{
[Link](0);
garbage_collect();
}
If you use this code, you simply have to call the preload_add function multiple
times to load all of your sounds when appropriate. reset_preloads is a convenience
function that not only resets the global array, but also ensures that the data is
actually unloaded from memory by running a full garbage collection cycle. If you
use the sound pool, it is a good idea to call the destroy_all method in it before
unloading the preloads so that the garbage collector can do its work more
effectively.
Note that a full garbage collection cycle might take some time depending upon the
behavior of your script, so you may want to test how severe this is and then
determine
if it is appropriate to do at this point in time. It is generally a good idea to
invoke the garbage collector at the end of a game round, though.
Call wait often
Knowing when to use the wait function is absolutely crucial for the performance of
your game. When you call wait, you give the computer a chance to do other things.
Windows is a multitasking operating system, which means that all the programs that
are running at any given time share the resources of the hardware. wait should
be called, usually with a parameter of 5, in any loop that runs for an extended
period of time such as the game's main loop. Failing to do this will result in a
CPU usage of 100% for the game process. This in turn will cause general instability
of the operating system in many cases, and will make most laptop fans go haywire.
wait also performs other useful tasks such as keeping the game window's internal
event queue up to date, and invoking the garbage collector in small increments.
This means that if you fail to call wait in a script loop that generates garbage,
which is to say creates and destroys some resource in each iteration, the garbage
collector will be full of data without getting a sporting chance to clean it up
effectively. BGT has a safety mechanism which kicks in if the garbage collector
is close to overflowing, but this forces the engine to run a full collection cycle
which is generally not what you want during gameplay. In short, wait not only
gives the CPU a rest; it also keeps the engine running smoothly. Use it!
Avoid streams when possible
Streams are very tempting to use because they start playing the sound very quickly,
and there is not a great amount of memory overhead for them if compared to loaded
sounds. However, streams should be used with care. While certainly tempting, they
have one serious flaw. A stream achieves its quick starting time by only loading
a little piece of the data into memory and then beginning to play it. While this
first piece is playing, the stream is busy loading the next little chunk. As soon
as the first piece has finished, the second one begins playing and the third is
then loaded and prepared for playback. This sounds simple, but can cause a lot of
issues when it comes to performance if too many streams are playing at once. If you
have an Ogg Vorbis sound that is encrypted, the engine needs to perform the
following steps for each and every individual chunk of data:
1. Read it from disk into memory. Disk reading and writing is, as stated above, one
of the slowest processes in an operating system (Windows included).
2. Decrypt it. This is generally pretty fast, but it still adds to the overall
processing time quite a bit when done many times.
3. Decode the decrypted data from Ogg Vorbis back to raw audio samples. This also
takes a bit of time as the Ogg Vorbis format is quite complex.
4. Finally feed the decoded data to the audio device which involves copying more
memory.
All of these things combined result in a fairly costly operation as far as CPU
cycles are concerned. It generally works fine when you have just a few streams
going,
but if this number grows you will start to notice high CPU usage as well as
occasional stuttering of other sounds or even little repeating chunks in those
streams
that may not get enough processing time to load their next chunk before the
previous one has finished. Therefore, you only want to use streams for sounds that
are
very long and that do not play right in the middle of a fast, action packed game
level. As a rule of thumb, it is better to use more memory rather than more
processing
power. Load sounds as much as possible and take advantage of the cloning feature
outlined above. Streams have their place, but consider what happens in the
background
before deciding to use them.
Use the reserve method in the array
If you have an array to which you know you'll be adding and removing a lot of
elements using insert_last, insert_at and friends, it is a good idea to reserve
memory
in advance. Each time an insert method is called, it will allocate just enough
memory to store the new item. Similarly, each time a remove method is called it
will
free the memory for the given item directly. This may seem like a good thing, but
can actually hurt performance a great deal because memory allocation takes time.
Therefore, allocating more memory than you actually need is a common optimization
strategy in order to avoid many small allocations along the way. The reserve method
will allocate memory for the requested number of items internally, without actually
resizing the array. In other words it is possible to have an array with 0 elements
in it, but plenty of memory reserved for future use. This means that the next time
you call insert_last for instance, it will not allocate any new memory but will
simply place the newly created item in the already reserved memory.
The following script will show you the difference between inserting one item at a
time without reserving memory, versus reserving memory and then inserting items:
void main()
{
timer counter;
int[] list;
int[] list2;
for(int i=0;i<100000;i++)
list.insert_last(i);
alert("First result", "Time elapsed without reserved memory: " + [Link] +
" milliseconds.");
[Link](0);
[Link]();
[Link](100000);
for(int i=0;i<100000;i++)
list2.insert_last(i);
alert("Second result", "Time elapsed with reserved memory: " + [Link] + "
milliseconds.");
}
void main()
{
// Set up a timer to measure performance.
timer counter;
[Link]();
string some_data="hello";
for(int i=0;i<100000;i++)
{
if(some_data=="ball")
{
// Do something.
}
}
alert("Result", "The operation took " + [Link] + " milliseconds.");
}
void main()
{
timer counter;
[Link]();
int some_data=5;
for(int i=0;i<100000;i++)
{
if(some_data==5)
{
// Do something.
}
}
alert("Result", "The operation took " + [Link] + " milliseconds.");
}
Registration in BGT
1. Introduction
A lot of users are requesting a way to protect their games from piracy, which is a
very understandable attitude considering how much time and money that is usually
spent on a commercial game. However, the sad truth is that there is no bulletproof
way of accomplishing this. If you search for the name of your favorite software
plus the word crack on Google or any other popular search engine, you will almost
always find someone who has taken the time to break the registration system and
post either a patch, a key generator or some other way of bypassing the security
system. The problem becomes even greater when using a game engine, because the
way in which things like unique computer ID's are generated will be the same for
every game produced with it. This is not to say that the ID itself will be the
same for each game, but the internal generation code will ultimately be the same.
In other words, the more BGT does for you behind the scenes in order to simplify
your work, the easier it will be for a cracker to break almost every game produced
with the engine. For this reason, BGT provides an absolute minimal set of features
for registration management and it is up to you as the script writer to think of a
way of generating keys etc. The advantage with this is that every game will do
it a little differently, making it a lot harder for someone to generate some
ultimate BGT game crack that will work everywhere. This is still far from a
guarantee
that your game will not be cracked, but it does make it harder as the cracker will
have to start from scratch for each new game.
2. Methods
So how do I go about protecting my product, you ask? There are three main types of
registration systems used today. Below follows a list of them, with pros and
cons of each:
2.1. Name/key
This system is by far the simplest, most user friendly, and least secure. The user
simply enters their name, pays for the software, and gets a key back that is
a scrambled version of their name plus some unique constant identifying the game in
question. Without this game specific constant, a user who purchases one game
from you could use the same key to register all your other games as well. The only
difficult part with this system is to scramble the user's name in a creative
way. There is no right or wrong way of doing this, and there is really only one
rule. Make it as irrecognizable as possible and you are off to a good start. BGT
provides a large number of string related functions so it's really up to your
imagination which ones you want to use and how to combine them.
Pros:
� Very user friendly. A customer who has purchased the game can back up their name
and key and reuse this information if they get a new computer for example, since
it never changes.
� Easy to implement. all that is required is a creative way of obscuring the user's
name, a constant identifying your game which is scrambled in with the name to
form the final key, and you're done.
Cons:
� Trivial to crack. All a user has to do in order to give your game to their best
friend and the best friend's relatives is to hand out their name and key.
Pros:
� Considerably more secure than name/key. A user cannot simply copy over their
registration key to another machine.
� Easy to implement. This requires about the same amount of work to implement as
name/key, given the unique computer ID generation function provided in BGT.
Cons:
� Requires a lot more maintenance. As soon as the user reformats their computer or
changes a certain hardware component, they will need to contact you in order
to get a new key just to keep using the product they have already paid for.
� Vulnerable on a massive scale. If the product ID generation system in BGT is
cracked, every game that uses it will be cracked in an instant.
Pros:
� Full control. You as the developer have full control over who is registered, how
many computers they are using the game on, and can limit the number of allowed
registrations at any time.
� Reusability. Since the customer does not need a product ID but can use their name
instead, they are able to register it on multiple computers with the same
information
provided that you allow for this.
Cons:
� Requires an Internet connection at the time of registration. As stated previously
this is usually not an issue, but it may be. . Completely dependent on the server.
If the server goes down for any reason, the paying customers will not be able to
register and use the product they have purchased.
� Harder to implement. A server script is needed for the system to work, which
requires a good understanding of a scripting language such as PHP as well as an
excellent
awareness of security design techniques.
� Vulnerable. Local information must be stored to tell the game that it is already
registered. This information is not difficult for a cracker to forge.
3. Conclusion
After looking at all three of these registration systems, we can come to one
obvious conclusion. No matter what we do, our game will be cracked eventually. If
it
is a good game and someone feels it is worth the time spent, you do not stand a
chance no matter what measures you have taken. This applies especially to the
product
ID system described in section 2.2, as the internal generation code will be the
same for all BGT games even though the resulting ID is not the same. On top of
this,
the product ID system requires a lot of maintenance and is likely to get
frustrating both for the developer and the end users. It is almost inevitable that
the
product ID generation code in BGT will eventually be cracked, as are all similar
systems if the cracker is determined enough. When this happens, you have not only
got your game pirated; you have also made it a lot harder for the honest customers
to actually use your software. In the end, the product ID has not served any
other purpose than to complicate life both for you and your users while not
preventing cracks. The same is true for the online system, as this also has several
1. It is very easy; both on the developer in terms of maintenance and the end users
as regarding portability.
2. As soon as the other two systems are cracked which will happen sooner or later,
they only complicate things for no purpose.
3. The people who crack your game would not have purchased it in the first place.
However, you will get a lot more sales from honest customers if you show that
you trust them with a system that does not limit their use of your software.
Naturally, the choice is yours. I have attempted to outline the three most common
types of registration systems in order to help you make an educated choice as
to which one you wish to use. BGT provides the facility required for all three (a
large array of string functions, a computer ID generation function, and access
to http servers using either the http object or the url_get and url_post
functions). However please understand that if our product ID generation system is
cracked,
there is not much that we or anyone else can do to solve the situation. As per our
license agreement, we can not assume any responsibility for loss of profit or
any other direct and/or indirect issues and/or damages that may arise as a result
of your use of this software.
Serialization Tutorial
What Is Serialization?
Serialization is the process of taking data inside of an application such as
integers, floating point numbers and strings, and converting them to a stream of
bytes.
In game related terms, serialization allows you to save game data and load it back
again at a later time. This can be anything from entire levels with enemies and
traps, to simple configuration data.
Many developers, when faced with the task of saving game levels, will come up with
a very application specific format where the order in which things are saved
and loaded matters greatly, and where it is difficult to adapt the saving and
loading routines as new versions are made. BGT's serialization functionality
attempts
to circumvent both of these problems by using keys and values. Each saved piece of
information has a corresponding name which is used when it is time to look up
the data again when it needs to be loaded. This means that the order in which
things are saved is no longer relevant because each value is looked up based on its
name, rather than based on where in the long list of values it is expected to be
stored. Furthermore, when you release a new version of your game which requires
new content to be saved, your old data files will still be compatible with the new
version provided that you are able to set the new variables to default values
if they are not found.
Serializing your Data
The first step to serializing data is to decide exactly what needs saving. For
instance, a game that only saves its data at the beginning of each level will not
need to store the player's position, since this will generally be set when the
level starts. However, a player's inventory will need to be saved, as this could
change from game to game and therefore should not be surmised.
Once you have done this, all your chosen variables need to be stored in one or more
dictionaries. Such variables will typically consist of global variables and
class properties.
Dictionaries provide an easy way to assign a name to your variable that can easily
be stored, and when inventing your names it is important to use names that are
unique, otherwise values will be overwritten. A simple way to do this is to use the
same name as your variable, including any class names. For example, if you have
a map class with an environment stored in a 2d array, you might store a location in
your map as follows:
my_dictionary.set("map/location/"+x+"/"+y, [Link][x][y]);
Either way, it needs to be unique, and recognizable to you as the script writer, so
your values can be consistent during loading and saving.
To serialize your data, you need to call the serialize function in BGT, passing the
dictionary as a parameter. The function will then return a string, that you
can easily save to a file. See the serialization function chapter in the reference
for more details.
It is important to note that, while the resulting string is hard to read, numbers
are easily discernible, and the text data is encoded in Base64, making it easy
to decode the string back into text. This means that if you wish to make your saved
data secure to prevent cheating, you will still need to use the encryption
functions
provided by BGT.
Deserializing your Data
Deserializing data is slightly more complicated, since you have to make room for an
event where a specific value is not saved, for example in a different version
with a new feature that has not yet been used. A simple approach might be to set
all variables in a given class to default values before loading, and then simply
overwriting these defaults with all the things that are successfully retrieved from
the data file. You may also want to store a version number, so that in the
unfortunate
case where there is absolutely no way to make saved games backwards compatible you
can at least inform the user about this and bail out.
To deserialize, you first need to retrieve the data from the storage location,
decrypt it if applicable, and call the deserialize function, passing the string as
a parameter. It will return a dictionary object, whose values you will then need to
store back in the relevant variables.
If a value you look for cannot be found in the loaded dictionary, the variable will
not be changed, which of course doesn't matter in this case, as we have assigned
a default value to the variable for the game to fall back on.
For an example of how you might go about deserializing your data, see the
deserialize function chapter in the reference.