You are on page 1of 145

MADLAD DESIGNS

DBPro Guide
Beginners Guide to DarkBasic Pro
Nickydude

2008

R E V I S 0I O N 1
Table of Contents

Introduction .......................................................................................................................................... 2
TDK_Man's Dark Basic Programming for Beginners .............................................................................. 3
Part 1 - Variables ............................................................................................................................................ 3
Part 2 - Layout, Structure And Style .............................................................................................................. 14
Part 3 - Elementary Commands 26
Part 4 - File Access ........................................................................................................................................ 46
Choosing The Correct Variables .................................................................................................................... 57
Dark Basic Functions .................................................................................................................................... 60
Everything you wanted to know about strings .............................................................................................. 63
Timer Tutorial............................................................................................................................................... 73
Dark Basic Matrix Primer .............................................................................................................................. 77
Dark Basic 3D Collision (DBC & DBP) ............................................................................................................. 85
2D Shooting - A Basic Introduction (DBC & DBP) ........................................................................................... 94
BitMap Font Tutorial .................................................................................................................................. 100
Space Invader Tutorial ...................................................................................................................... 103
Building a Framework ................................................................................................................................. 103
Design, Design, Design! .............................................................................................................................. 103
Structure .................................................................................................................................................... 104
Game Data ................................................................................................................................................. 105
Standing on 2 Feet ..................................................................................................................................... 105
The Story So Far ......................................................................................................................................... 105
The Plan Ahead .......................................................................................................................................... 106
Building Blocks ........................................................................................................................................... 106
Rapid Development .................................................................................................................................... 106
Create and Destroy .................................................................................................................................... 107
Adding a state machine .............................................................................................................................. 107
The Plan Ahead .......................................................................................................................................... 108
Big Changes, Fast! ...................................................................................................................................... 108
Conclusion .................................................................................................................................................. 110
Zork Tutorial - Part One .............................................................................................................................. 111
Zork Tutorial - Part II - OBJECTS AND INVENTORY ....................................................................................... 128

1
Introduction

My very first DPBro program... yes, the planet spins!

Having just bought DarkBasic Professional, it does seem a bit overwhelming. I have programmed
BASIC in the past but that was a very long time ago so I’m going to have to relearn it all over again.
So, in order to help me I’m compiling the tutorials I’ve found into one document which can be
printed out and read away from the computer (it also stops you having to switch back and forth
between online tutorial and DBPro).
Andy.

2
TDK_Man's Dark Basic Programming for Beginners

Part 1 - Variables

This is the first of an intended series of tutorials aimed at teaching the newcomer to programming the basics
of the BASIC programming language. In part 1 you will find a very brief introduction to programming languages
and an explanation of variables.

The examples will be in Dark Basic Classic, but the examples and theory should apply with little or no
modification to any dialect of BASIC - including DB Pro.

It will cover the elementary groundings and anyone with any previous programming experience will find little
of interest in this tutorial. Smaller sections of code which can be typed into DB will appear in Courier font to
make the text easier to follow:

SET DISPLAY MODE 800,600,16


CLS
PRINT "Hello World"

Where Do I Start?

OK, so you've downloaded the DB demo or bought it, loaded it up and don't know what to do next. You've no
programming experience and can't make any sense out of the help files and/or manuals.

Well, you need to learn the ABC's and start simple, so that's what we will do...

Programs:

In a nutshell, computers manipulate data by transferring numbers around in memory.

For example, when a number is placed in a certain part of memory called the video memory, a dot will appear
on your monitor screen. The colour of the dot depends on the value of the number.

Every image, text character and coloured dot you see on the computer screen is nothing more than a number
in your computer's memory.

When you play a computer game, all you are seeing is the results of all these numbers being manipulated to
create what you see on the screen. A computer program is simply a list of instructions the computer has to
follow to do it.

To write a game, you need to create the machine code which tells the computer's CPU where to move the data
from and where to put it.

However, as binary (all 0's and 1's) is not the easiest way to go about it, back when I first started programming,
a language called 'Assembler' was developed. This turned the programming process into something more
manageable and easier to learn.

call_oz(GN_Nln)
push iy
pop hl
ld de, 2
ld b, 6

3
call_oz(GN_Gdn)
ld d,b
ld e,c
ld hl, 10
call_oz(GN_M16)
ld b,h
ld c,l

Even so, as you can see from the above snippet of Z80 assembler, the commands were still obscure and
looking at it you had very little idea exactly what it did. Besides that, something like clearing the screen - which
we now take for granted with CLS in DB - took many lines of code, because each individual dot on the monitor
had to be manually set to black in a loop - and all you had at your disposal were simple instructions like loading
values into registers.

This was known as a 'Low-Level Language' as you had to get down to talking directly to the hardware. Also, you
had to learn the version of ASM for the CPU in your machine. This might be Z80, 6502, 68000 or one of many
others - none of which for the main part were interchangeable.

The BIG advantage that programming the hardware like this was FAST - blindingly fast! And, that remains true
to this day - a program written in assembler will be a lot faster than the same program written in a higher-level
language, but a thousand times bigger and more difficult to write. Instead, short assembler routines are
written for specific purposes where speed is essential.

Over time, other programming languages appeared which removed the low-level commands like LD, PUSH and
POP and replaced them with higher level commands like PRINT and REPEAT...UNTIL. When these programs
were run, the high-level instructions were compiled into low-level machine code that the computer could use.

Examples of these languages were Pascal, Forth, C and COBOL - each having their own areas of expertise.

And then along came BASIC or Beginners All-purpose Symbolic Instruction Code which as the name suggests
was aimed firmly at beginners.

Clearing the screen was reduced to CLS, outputting text to the screen used PRINT and so on. It was easy to
learn but slow as it was interpreted (not compiled into machine code), though later on, compilers appeared
which sped things up a bit. Most importantly it's 'nearly English' syntax meant you could read the majority of
BASIC code and know more or less what it was doing straight away.

Variables:

All versions of BASIC are built using the same building blocks. A program which uses these building blocks can
be run on practically any machine, though things start to alter when you get to the commands which make use
of the hardware differences between machines - for example, versions of BASIC running on a PC and a Mac will
both have unique commands.

One of the building blocks in any higher level language is variables. Without them you would have an
impossible task writing anything but the smallest program.

So, what is a variable? Well, a variable is in effect a substitute for something which can change - either a
numeric value or an alphanumeric string of characters. A numeric variable can be used in a formula or
calculation in exactly the same way as a number can.

Take the simple example:

A=100
Here we are creating a variable called A and giving it the value 100. I found it easiest when learning to program
by thinking of it as follows…

4
The computer creates a cardboard box in memory, sticks a label on the front of the box and writes 'A' on the
label. It then puts 100 into the box.

From this moment on, whenever your program refers to 'A', the computer goes through all of its boxes until it
finds the one with 'A' on the label and gets the value out to use.

B=50

OK, another box only with B on the label and 50 stored inside it. Easy eh?

C=A+B

What does C equal? If you thought AB then you are thinking in algebra terms and it's not the same!

Like before, the computer gets the 100 out of box A and the 50 out of box B and adds them together, putting
the resulting 150 into a new box it creates and labels 'C'.

You will notice that BASIC uses the syntax of 'the bit on the left side of the equals sign ends up containing the
results of everything on the right side'. This is different to the way you do it in conventional maths, so be
aware of this.

In fact it would be more precise to read 'A=100' as A becomes equal to 100 rather than A equals 100 as in
computer terms 'A=100' may not be true. The variable 'A' can change, so it may NOT equal 100!

For example think about these two (admittedly pointless) lines in a DB program:

A=10
A=100

On the second line, A already equals 10 (as defined on the first line), so A=100 is a false statement as A actually
doesn't equal 100 - it equals 10! If the first line also said A=100, then the second line would be true as A does
indeed equal 100 at that point. You should now see the reason for reading it as 'A becomes equal to' rather
than just 'A equals'.

This may sound very confusing at first, but later on it will make more sense when you will find out about
BASIC's IF statement which is used to do logical comparisons using boolean logic - where the answer to a
question is always either yes (1) or no (0). For example, in BASIC you can say:

IF A=100

…which will return a 0 (false) or a 1 (true) depending on whether or not A does equal 100 or not. Computers
never say 'I don't know' or 'maybe' in these cases!

5
As the A=100 is the same as what we have just covered, it is important to have firmly in your mind the
difference between the two. One sets the variable's contents to 100 and the other compares the contents of A
with 100 to see if they are the same.

As an aside, in the programming language Pascal, these two tasks have a different syntax to avoid this
confusion. Setting the variable A to equal 100 in Pascal is done with A:=100 with the colon meaning 'becomes'.
As such it does actually read A becomes equal to 100. IF A=100 in Pascal is exactly the same as BASIC (without
the colon).

Variable Names:

The only rules you should apply are:

1. Don't use reserved words as a variable names (words used as BASIC commands like Print, Do, Loop
etc), though you can use variable names of which only a part is a reserved word. For example, Sprite
is a reserved word and not recommended as a variable name, but MySprite would be OK.
2. Always start a variable name with an a..z or A..Z character - never a number. A number can be placed
on the end or in the middle of the variable name if you wish though. Even a variable name like
ABC123DEF is acceptable!
3. Don't use spaces or any special symbols in variable names other than the _ (underscore) character.
This can be used to separate words to make them more readable. Eg: Time Left is a no no. Time_Left
is OK.

You can use any combination of characters and numbers in a variable name as long as you follow rule 2 above,
so make use of the ability and use a name which makes sense. So, if you are storing a value which represents
the players score then use a variable called 'Score'. A line which says:

Score=Score+100

…makes more sense than X=X+100 as the variable name tells you what you are adding 100 to! X could mean
anything.

Also, unlike some programming languages, you don't have to declare variables prior to using them. The first
time you refer to them in your program they exist, and if you don't specify that they contain anything, numeric
variables automatically contain 0 (zero).

OK, that's numeric variables… or is it? Well, not quite.

In DB there are two types of number - Integers, or whole numbers like 10, 123 or 1000 and Reals (or floats -
floating point fractional numbers), like 1.373, 328.45 or 1000.09.

Integer and real variables need different amounts of memory to store values, so you need to tell DB what type
of value it is you are storing. You do this with the variable name. Variables which need to store real numbers
are identified by putting the # symbol on the end of the variable name. This effectively tells DB to make a
bigger cardboard box to hold the decimal point and numbers after the decimal point.

So, the following examples are correct:

Age=21
Apples=8
Height#=137.45
ObjAngleX#=120.33
You also have to be careful not to accidentally mix the two types of variable as you can end up with hard to
track errors. For example:

6
A#=100.23
B#=10.14
C=A#+B#

What is C equal to this time? If you said 110.37 you would be wrong. If you said 110 then you would be
correct.

A# and B# are added together to get the correct result of 110.37, but the result is placed into C which is an
integer variable, so the .37 bit is lost. This effect is known as variable casting in some programming languages.
(The result is comparable to the DB function INT() in DB).

Sometimes you may want to do this, but when learning, the chances are that you do not, so beware!

Strings:

As well as numeric variables, you can also have string variables which are pretty much the same, but hold
alphanumeric characters (a..z A..Z 0..9) rather than numbers. This can be a single character, a word or a
complete sentence up to 255 characters long. And, being alphanumeric strings can also be numbers so beware
as numbers in strings are still strings and cannot be treated as if they were number variables!

String variables are defined by putting a $ sign on the end of a variable name and their contents have to be
enclosed in double quote symbols to clearly define where the string starts and ends.

For example:

A$="The cat sat on the mat."


Name$="Fred Bloggs"
Age$="21"
FaveHobby$="5-A-Side Football"

…are all legitimate string declarations. But, note that in the third example Age$ may equal 21, but adding the
variable to itself would equal 2121 - NOT 42 as it's a string variable - not a numeric variable.

Arrays:

Arrays are very useful. Arrays are easy if you are shown how they work in the right way. Arrays are a nightmare
to new programmers if you are not!

Arrays are nothing more than groups of normal variables, each with an index number with which to access
them. The main difference is that arrays have to be dimensioned before you use them as DB has to make sure
that it builds enough cardboard boxes (to continue the analogy). This is done with the DIM command.

Arrays can be single or multi-dimensioned so let's take a look at single dimensioned arrays first…

An example:

Say you were writing a simple game where each person typed in their name and the program stored it, along
with their best score. The variable Name$ could hold their name and the variable Score could hold their best
score.

But, that's only good enough for just one single player. Use the same variables for player number two and
player number one's name and score are lost! So, we use Name1$ and Name2$ for the names along with
Score1 and Score2 for the scores.
I think you can see where we are going with this… If you had 100 people playing this game, we would need
Name1$, Name2$, Name3$ all the way up to Name100$ - and the same for the variable Score.

7
Also, the line in your program which increments the score would have to be repeated 100 times - once for
each player.

Arrays rid you of this hassle. Using:

DIM Name$(100)
DIM Score(100)

...will create two arrays, the first allowing you to store 100 strings of up to 255 characters (1 to 100, though
you also have number 0) and the second, 100 numbers. This is a 1 dimensional array.

Memory is allocated in a continuous block large enough to store the requested number of variables stated in
the DIM statement. The Score array would look something like this:

Each of the 100 variables is accessed by using its index number. This way you can have:

Name$(1)="John"
Name$(2)="Dave"
Name$(99)="Pete"

And so on.

Score(1)=2000
Score(2)=2500
Score(3)=2100

…would be the respective scores. For example, Score(12) would belong to player Name$(12) and so on. When
you realise that the actual number can be replaced by a numeric variable it makes it possible to say things like
Name$(CurrentPlayer) and Score(CurrentPlayer) in your programs.

So that's single dimensioned arrays. What about Multi-Dimensioned arrays?

Well, they are basically the same, but instead of the array being one single line from 0 to 100, they are created
in a 2 dimensional grid format. They are best thought of as being like the old-fashioned pigeon holes you find
in schools (or a wall covered with lockers) where there are a number of boxes running across and down.

Dimensioning an array like this just needs you to use two values in the DIM statement - the number of boxes
across and the number down. Bearing in mind that array indexes start at 0, to get a numeric array grid of 25
variables (5 across and 5 down) called Location, you would use:

DIM Location(4,4)
However, while learning, if you wanted an array of say 10 across and 5 down you would be better off using
DIM ArrayName(10,5) and completely ignoring the 0 element until you get the hang of things. Doing this, for
our 5x5 integer array we would therefore use:

8
DIM Location(5,5)

It doesn't matter which method you use - 0 based arrays or 1 based arrays - just use the one you understand
and are happiest with. Remember you don't have to use the 0 part of an array - it's just a waste of memory if
you don't. With small arrays, this isn't really a problem, though the larger the array, the more memory is
wasted.

When created, you access the contents using Location(1,1) or Location(1,3) etc. The first element in brackets is
the index across the array (the 'X' value) and the second element is the index down the array (the 'Y' value). In
the above diagram, the box with the 200 in it would be set with Location(3,2)=200. Once again, in your
programs, you can replace the numbers with variables and use something like Location(XLoc,YLoc).

Finally to complicate things even further, you can take the dimensioning one step further by creating 3
dimensional arrays. This would be akin to having a block of variables in a 3D Rubiks cube structure and storing
a value in each of the small boxes that make one up. This would be done with:

Dim CubeArray(3,3,3)

In this example, the three values correspond to the X, Y and Z axis of the array and this will create a block of
variables as shown in the image on the right. As you can see, the 'top slice' is just like the 2 dimensional array
discussed in the previous section. However, as the DIM statement has a further Z value, then the array is
stacked four deep - using the indices 0 to 3.

This gives you 4 variable grids, each one 4 across and 4 down and to set the variable highlighted in red to the
value 10, you would use CubeArray(1,3,2)=10. In other words, 1 along the X axis, 3 along the Y axis and 2 along
the Z axis.

9
But, I doubt if you'll need to use an array like this for the foreseeable future…

What You Can Do With Variables:

It's difficult to start giving programming examples this early in a series of tutorials as you will probably not be
familiar with the commands being used so I'll try to keep the examples as simple as possible.

In essence, you use variables to store things that can vary in your programs or when you want to store
information that isn't available when you write the program.

For example:

Print "Please Enter Your Name: ";


Input Name$
CLS
Print "Hello ";Name$

This simple program will print the message 'Please Enter Your Name.' onto the screen. The next line uses
BASIC's INPUT command. When your program reaches this line, it will stop for the user to type their name in.
Whatever they type will be stored in the string variable Name$ when they press the Enter key.

On the next line, CLS will clear the screen and on the last line, the PRINT command will print 'Hello' followed by
the contents of Name$, (whatever name they typed in).

Another example:

Print "Please Enter Temperature In Centigrade: ";


Input C
F# = 1.8*C+32
Print C;" degrees centigrade equals "; F#;" degrees Fahrenheit."

This example asks the user to enter a number for a temperature in Centigrade (Celsius) and stores the value in
a variable called C.
The next line uses the variable C in a formula to convert the temperature to Fahrenheit and stores the result in
the variable F#. Notice that this is a real number variable as the formula includes the value 1.8 which means
that the result may be a real number too. The last line prints out the result in a formatted sentence.

There you have it - two very simple programs which demonstrate the use of variables. However, these
programs run just once and then end. You have to re-run them every time you want to use them again. What
we want is for the programs to keep working until we tell them to stop. That however is down to program
layout and structure - which just happens to be one of the topics in the next tutorial!

Operators:

Variables aren't much use if you can't do anything with them, so a number of options are available to you for
this task. Some of these options can be applied to numeric variables, some to strings and some to both.

Maths Operators:

Numeric variables wouldn't be very useful if you couldn't use them with basic maths in your programs so the
basic operators add, subtract, multiply and divide are represented by +, -, * and / respectively.

Therefore, adding together two numeric variables is just as easy as adding two numbers. For example to add
30 and 50 normally we would put 30+50 and the answer would be 80. In a program, that would equate to:

10
A=30
B=50
C=A+B
Print C

...and when C is printed to the screen you see the number 80 printed.

More complicated formulas can also be built up with a combination of the four basic operators such as
A=M*V/X+Z. However this brings into play a very important aspect of maths when programming - Order Of
Precedence. This is best explained with the following example so type it in and run it.

A=7
B=2
C=3
D=A+B*C
Print D

What is printed? It should be 27 right? 7+2 equals 9 then the 9 is multiplied by 3 to make 27. So why does it
print 13?...

The reason is that Dark BASIC (like all programming languages) carries out maths in a specific order. Multiply
and divide (* and /) calculations are done first, followed by addition and subtraction (+ and -). So, our little
calculation above is carried out by multiplying B and C together first to get 6 and then adding A which makes a
total of 13.

Now this may not be what you want - you may want it to be done in the order that results in 27. You can force
DB to do this by use of the '(' and ')' symbols (parentheses). Anything enclosed in parentheses is given a higher
order of precedence and will be calculated before anything else. So:

D=(A+B)*C

...will force A to be added to B first before the result is multiplied by C.

Relational Operators:

These are used to compare data items (numeric and strings) and use the = (equals), < (less than), > (greater
than), <= (less than or equals), >= (greater than or equals) and <> (does not equal) symbols.

Each one returns true (1) or false (0) and we touched on the subject earlier in the tutorial when we looked at IF
A=100.

In a nutshell, we need in our programs a method to make decisions. Do something only if another thing has
already been done or act on a users input - that sort of thing. With relational operators and IF...THEN that is
possible. So, let's have a quick rundown on IF...THEN first:

IF...THEN

Without IF...THEN statements, programs would be impossible to write as there would be no way to make
decisions. The basic form is as follows:

IF condition
Do stuff here only if the condition is met
ENDIF
or...

11
IF condition
Do stuff here only if the condition is met
ELSE
Do this stuff only if the condition is NOT met
ENDIF

In both above examples, the condition is deemed to have been met if the condition is tested True (returns a 1)
and the relevant code is executed. Using IF...THEN you don't actually get to see the 0 or the 1, but that's what
DB returns and acts on! The first example is used if you want to carry out the enclosed code ONLY if a single
condition is met - otherwise nothing is done. The condition might be as simple as checking to see if a variable
equals a certain value or the user has clicked a mouse button.

The second example is used if you want to do one thing if a single condition is met and something else if it is
not. If the condition was that a certain variable equalled say 10 then if it did equal 10 the block of code
between IF and ELSE would be executed. If the variable contained ANY other value the code between the ELSE
and the ENDIF is executed.

OK, that out of the way, on with the description of relational operators. It is taken for granted that the
following examples are used with variables as conditions in IF..THEN statements so instead, I'll show examples
with proper numbers along with what they would return.

Equals (=)

Checks to see if two items are equal. 1 is returned if they are, 0 if they are not. Eg:

10 = 10 - Returns True (1) as 10 does indeed equal 10.


10 = 11 - Returns False (0) as 10 does not equal 11.

Less Than ( < )

Checks to see if the first item is less than the second. 1 is returned if it is, 0 if it is not. Eg:

2 < 10 - Returns True (1) as 2 is less than 10.


7 < 5 - Returns False (0) as 7 is not less than 5.
9 < 9 - Returns False (0) as 9 is not less than 9.

Greater Than ( > )

Checks to see if the first item is greater than the second. 1 is returned if it is, 0 if it is not. Eg:

2 > 10 - Returns False (0) as 2 is not greater than 10.


7 > 5 - Returns True (1) as 7 is greater than 5.
9 > 9 - Returns False (0) as 9 is not greater than 9.

Less Than or Equals (<=)

Checks to see if the first item is less than or equals the second. 1 is returned if it is, 0 if it is not. Eg:

2 <= 10 - Returns True (1) as 2 is less than or equals 10.


7 <= 5 - Returns False (0) as 7 is not less than or equals 5.
9 <= 9 - Returns True (1) as 9 is less than or equals 9.

Greater Than or Equals (>=)

12
Checks to see if the first item is greater than or equals the second. 1 is returned if it is, 0 if it is not. Eg:

2 >= 10 - Returns False (0) as 2 is not greater than or equals 10.


7 >= 5 - Returns True (1) as 7 is greater than or equals 5.
9 >= 9 - Returns True (1) as 9 is greater than or equals 9.

Does Not Equal ( <> )

Checks to see if the first item is not equal to the second. 1 is returned if it is not, 0 if it is. Eg:

2 <> 10 - Returns True (1) as 2 does not equal 10.


9 <> 9 - Returns False (0) as 9 does equal 9.

So, to finish, a VERY basic little number game as an example of using what we have learnt in this tutorial. Don't
worry if there are any commands that haven't been covered in the tutorial. I've commented the program and
they will be covered eventually. If you like, look those up in the DB help files by pressing F1 when in the DB
editor.

Randomize Timer(): Rem Initialise the random number generator


Start:
MyNumber=Rnd(99)+1: Rem Select a random number between 1 and 100
Do: Rem Main Program Loop
CLS: Rem Clear the screen
Print "I have thought of a number between 1 and 100. See how quickly you
can guess it!"
Print
Input "What is your guess? ",Guess
Print
If Guess < MyNumber: Rem If chosen number is lower than computer's number
Print "Your guess was too low. Try again."
Sleep 2000
Endif
If Guess > MyNumber: Rem If chosen number is higher than computer's
number
Print "Your guess was too high. Try again."
Sleep 2000
Endif
If Guess = MyNumber: Rem If chosen number is equal to computer's number
Print "Your guess was correct. Well done!!"
Print
Print "Do you want to play again (Y/N)?"
Repeat: Rem Repeat...Until loop repeats until Y or N key is pressed
I$=Upper$(Inkey$()): Rem Read the keyboard for keypresses
Until I$="Y" or I$="N"
If I$="Y": Rem If Y was pressed
Goto Start: Rem Jump to Start label at beginning of program
Else
CLS: Rem Clear the screen
Print "Goodbye...": Rem Print Goodbye message
End: Rem End the program
Endif
Endif
Loop

Further Practice:

Feel free to alter the above program to see what it does. If you like, try changing it so that:

13
1. The program asks for the players name at the start and uses the name in the in-game comments. Eg:
"Your guess was too high Peter. Try again."
2. When the user guesses correctly the program tells them how many guesses they took to do it.
3. When the player exits the game the program tells them how many games they have played along with
their quickest and slowest games - guess-wise.

A working amended version of the program will be found in Part 2 of this series of tutorials so if you want to
try to do it yourself, don't look until you have attempted it.

There is no right or wrong way to do these three tasks - some ways are just better than others. If you alter the
program to do these three things and it works, then consider yourself having passed the test.

--ooOOoo—

Part 2 - Layout, Structure And Style

Before I start with the tutorial, how did you get on with the programming task I set at the end of Part 1?

If you got a working program, then well done. Below is how I would have done it. In a program this minor,
there is little difference between any of the methods you use, so if you did it differently, then no problem.

The method I have used is not meant to be the best or definitive way to do it - it's just the way I decided to do
it. Who knows, there may be reasons why your method is better than mine!

Anyway, here's my solution to the task…

Randomize Timer(): Rem Initialise the random number generator


Input "Please Enter Your Name: ";Name$
GamesPlayed=1: Best=1000: Worst=0: Rem Start variables
Start:
MyNumber=Rnd(99)+1: Rem Select a random number between 1 and 100
GuessCount=0: Rem Set guesses at zero at the start of each game
Do: Rem Main Program Loop
CLS: Rem Clear the screen
Print "I have thought of a number between 1 and 100. See how quickly you
can guess it!"
Print
Input "What is your guess? ",Guess
Inc GuessCount
Print
If Guess < MyNumber: Rem If chosen number is lower than computer's number
Print "Your guess was too low ";Name$;". Try again."
Sleep 2000
Endif
If Guess > MyNumber: Rem If chosen number is higher than computer's
number
Print "Your guess was too high ";Name$;". Try again."
Sleep 2000
Endif
If Guess = MyNumber: Rem If chosen number is equal to computer's number
If GuessCount < Best Then Best = GuessCount
If GuessCount > Worst Then Worst = GuessCount
Print "Your guess was correct. Well done ";Name$;"!!"

14
Print "That time you took ";GuessCount;" guesses."
Print
Print "Do you want to play again (Y/N)?"
Repeat: Rem Repeat...Until loop repeats until Y or N key is pressed
I$=Upper$(Inkey$()): Rem Read the keyboard for keypresses
Until I$="Y" or I$="N"
If I$="Y": Rem If Y was pressed
Inc GamesPlayed: Rem keep count of the number of games played
Goto Start: Rem Jump to Start label at beginning of program
Else
CLS: Rem Clear the screen
Print "This session, you played ";GamesPlayed;" games."
Print "Your best attempt was ";Best;" guesses and your worst was
";Worst;" guesses."
Print
Print "Goodbye...": Rem Print Goodbye message
End: Rem End the program
Endif
Endif
Loop

Only a couple of things worth mentioning...


First of all, notice that the variables Best and Worst are initialised at 1000 and 0 respectively which seems odd.

Whenever you are recording the highest and lowest of something you should always start the variables off at
the opposite end of the scale. Here, lower number guesses are best, so we start the variable Best off at 1000.
That way, when the player has finished the first game and has taken less than 1000 guesses (pretty likely), then
Best is replaced with that number. In subsequent games, Best is replaced by the number of guesses ONLY if it
is less that the number already stored in the variable Best.

Worst works in the same way only in reverse. The first game played, the 0 in the variable Worst is replaced by
the number of guesses made and subsequent games will only be replaced again if more guesses are taken.

By the end of the game, the least number of guesses will be stored in Best and the most in Worst.

OK, so now on with Part 2 of the tutorial series…

If you remember at the end of the last tutorial I gave you two small examples of Dark BASIC code to
demonstrate the use of variables, at the same time, pointing out that they weren't much use as they had to be
re-run each time you wanted to use them. This is particularly relevant with the second example as it would be
better if we could keep re-entering different temperatures and only end the program when we are finished
with it. This is actually quite simple to do, but before we do it's important to know why it ends so abruptly.

When all DB programs are run, the computer executes each line of instructions starting with the very first line.
It then carries on with the next line in sequence continuing until there are no more lines to execute or it
reaches the command END.

At any time during the execution of your program, something called the Program Counter keeps track of the
current line and unless the instruction on it tells the program counter to jump to a different line, it drops down
to the next one and continues. When there are no more lines, the program ends. It's like reading a book. You
start with the first line on page one and when you have read the last line on the last page the book is finished.
Let's remind ourselves of the last example program from tutorial 1:

Print "Please Enter Temperature In Centigrade: ";


Input C
F = 1.8*C+32
Print C;" degrees centigrade equals "; F;" degrees Fahrenheit."

15
Here, the message asking you to enter a value is printed to the screen, the computer stops and waits for input
on the next line. When the user enters a value and presses the Enter key, the next line calculates the result
and on the final line, the result is printed to the screen. As there are no more lines, at this point the program
has nothing more to do so it ends.

There are however commands which you can use to control the order in which the lines of instructions are
carried out. Our example above doesn't use any of them so the program just zips through all the lines in order
and then ends.

GOTO

One such command is GOTO which tells the program counter to jump to a specific location in the program,
skipping all lines in between. This can be a point anywhere in the program but has to be a defined label.
A label is a single word ending with a colon. The rules for naming labels are the same as for variables. Here's an
example:
Print "Program started…"
GOTO Label1
Print "This will never be printed!"
Label1:
Print "This line appears!"

If you enter this program in and run it, you will see that the 'Program started' message is printed to the screen
followed by the 'This line appears!' message. The 'This will never be printed!' message never appears because
on the second line, the program counter is told to jump to line four, skipping line three.

This is the crudest method of controlling program execution and should be avoided whenever possible as it is
possible to create what is known as 'spaghetti code' where control leaps around your program, making it very
difficult to follow when your program grows in size.

Continuing with our book analogy, imagine if at the bottom of page 1 it said 'continues on page 96'. You turn
to page 96 and continue reading only to find at the bottom of page 96 that it says 'continues on page 12' and
so on. In theory you might never finish the book - and that's what keeps a computer program from ending.

Loops

Sometimes, you want your code to do something a certain number of times and repeating the lines isn't a
viable solution. As a silly example, you may want to print three times, the sentence 'This is printed three
times.'

Obviously, this can be done with three lines - one for each time you want the sentence to appear:

Print "This is printed three times."


Print "This is printed three times."
Print "This is printed three times."

But, what about printing something 20 or even 100 times. As you can imagine, this would involve a lot of
typing. So, to make life easier, this is where control loops come in.

There are many types of loop - all of which could be used to print our lines, but for this example the best one
to use is the For…Next loop because we know how many times we want to print the sentence.

For N=1 To 20
Print "This is printed twenty times."
Next N

16
This loop uses a variable to count from the first supplied number to the last - in this case from 1 to 20 using the
counter variable N. You can use any variable name for the counter variable as long as the name used on the
Next line and the For line are the same.

The first time the For N= line is encountered, the variable N is set to the first value (1) and the line number is
recorded.

On the next line, the text is printed. When the Next N line is reached, the value of N is incremented and
compared with the end value (20).

If it is less than the end value then the program counter jumps back up to the line number recorded earlier and
the loop is repeated. This continues until the value of the loop counting variable N matches the end value, at
which point the loop is ended and program control drops down to the line following the Next N line.

In the process, the above loop will print the text message to the screen 20 times. Obviously you can have as
many lines between the For and the Next lines - they will all be carried out the stated number of times.

For…Next loops can be used to count from any number to any other number - you don't have to start at 1. You
don't even have to count in increments of 1 either as there is an optional STEP parameter that can be used on
the end. This provides the increment for the counting loop (which defaults to 1 when Step is not used).

For example: For N=1 to 100 Step 10 will start with N equalling 1, but the next time around the loop, 10 will be
added making N equal 11. The next time it will be 21 and so on up to 91.

Notice that the highest value it reaches is 91. This is because adding another 10 to 91 would take it over 100 -
the stated end value, so when it can't add any more the loop is ended. Remember this as it might save you a
few headaches in the future!

Hint: If you wanted it to count 10, 20, 30 etc up to 100 you would use For N=10 to 100 step 10. (Starting at 0
instead of 10 would give you 0, 10, 20 and so on).

Finally, there is the ability to count backwards. This is simply done by making the start number higher than the
end number and using Step -1:

For N=20 to 1 step -1 will count from 20 down to 1.

Note: The Step -1 is required for counting backwards - even if you are counting down 1 number at a time. The
default when not using Step is positive 1 regardless of the start and end values!

Usage Notes:

This type of loop is ALWAYS carried out the stated number of times (unless a run-time error occurs inside the
loop). The counter variable always equals the whole range of values from the declared start and end range
inclusive (unless Step is used). This makes it safe to use the counting variable for other things inside the loop.

17
In the process of a For…Next loop, the value of the variable used as a counter will change. For this reason, do
not use names which have been used elsewhere in your program. As a tip, I use N, N1, N2, N3 etc ONLY as
For…Next counting variables and do not use them anywhere else in my programs.

Other Loops:

As mentioned previously, there are other types of loop and printing a message a number of times could be
done with any of them, so let's take a look at them:

Do…Loop

The Do…Loop is the most basic of the loops in DB and has no conditions so isn't normally exited from - you
would usually exit the program with the END command without leaving the loop.

Program control will go around one of these loops forever so it is primarily used to create an enclosed main
program loop in your program. This is covered later on. For now, it's enough to know that we would not use
this type of loop to do the above example task.

Repeat…Until

The Repeat…Until loop is exited from only when the condition on the Until line is met. Unlike the For…Next
loop, somewhere in this loop variables must be altered in order for the condition to be met.

Let's see an example based on the above For…Next loop example:

Counter=1
Repeat
Print "This is printed twenty times."
Inc Counter
Until Counter=21

In this example, you can see the condition on the end. A condition is basically a test which will return 0 (false)
or 1 (true). In this case the condition for exiting the loop is that the variable Counter must equal 21. The
program counter will go around in a loop forever if Counter never equals 21 so that's why we set the variable
to 1 before entering the loop and increment the variable each time we go around the loop with Inc Counter.
(Inc is short for increment and adds 1 to the named variable).

Usage Notes:

As the counter starts at 1 and the counting variable is incremented at the end of the loop (after the Print line),
the variable will be incremented to 21 after the line has been printed 20 times. That's why the exit condition
variable has to be equal to 21 - after which the loop is immediately exited and the Print line not executed a
21st time.

As this loop is repeated until a specified condition is met and the condition is tested for at the end, the lines in
this loop will always be carried out a minimum of once - regardless of whether the condition is true or false
before entering the loop.

18
While…EndWhile

This loop is essentially the Repeat…Until loop with the condition at the
start of the loop instead of the end.
Counter=1
While Counter<21
Print "This is printed twenty times."
Inc Counter
EndWhile

While even having this type of loop may not seem worth the bother, already having Repeat…Until, there is one
subtle but significant difference. As the condition is tested at the beginning of the loop, if the condition fails
then none of the code is executed - unlike Repeat…Until which has to carry out the code once before the
condition is tested for at the end.

Usage Notes:

The code in the loop is only executed if the counter variable is less than 21.

It's important to realise that this loop is not entered at all if the specified condition is not met at the start of
the loop, as such, the lines inside this loop may never be carried out at all.

So, back to the original theme - stopping our little temperature program ending until we want it to...

All we need to do is add a main Do…Loop enclosing the existing code and add a condition to allow the user to
exit. The final program might look like this:

Do
Print "Please Enter Temperature In Centigrade: ";
Input C
F = 1.8*C+32
Print C;" degrees centigrade equals "; F;" degrees Fahrenheit."
Print
Print "Convert Another Temperature (Y/N)?"
Repeat
I$=Upper$(Inkey$())
Until I$="Y" Or I$="N"
If I$="N" Then End
CLS
Loop

Notice that as mentioned previously, the main Do...Loop is never actually exited from - its main use is to stop
the program from ending until we are ready. This is done with the End command when the user says no to
converting another temperature.

OK, that's loops covered, but before we cover any more BASIC commands and get too bogged down with
them, let's take a look at how best to put them into a program that will work with optimum performance and
still be legible.

Program Layout

The worst thing you can have to do is try and fix a fault in a program which has been poorly laid out, has Goto's
everywhere leaping all over the place and has no structure. It doesn't help if it's your own code either!

So, while you are a newcomer, it's best to get into the habit of writing your programs neatly now.

19
Writing programs in DB or any other language is finicky - as no doubt you have already discovered. This is
because of something called syntax.

When you write a paragraph of text in a word processor for a friend to read, all your speeling mystaykes make
your text look bad, but your friend can still read it and understand what you mean.

Unfortunately, a computer even these days is nowhere near as powerful as the human brain and has no way to
decipher what you meant when you put ForA = 1 To 10 or put Repet instead of Repeat.

This means that you have to type everything EXACTLY as it is supposed to be - right down to every full stop,
comma or semi-colon being in the right place.

Spaces too are also important. Too many isn't usually a problem, but leave one out when it should be there
and your program line will not be recognised.

Luckily, DB will stop you running a faulty program and highlight the iffy line - even though it sometimes can't
tell you exactly what is wrong with it!

Indentation:

Few people indent their code. It takes time and doesn't make your programs run any faster so why bother?
And what is indentation anyway?

Indentation is the offsetting of lines of code in the editor. Indenting makes your code a LOT easier to follow
and thus bugs are easier to find - especially when you have more than a couple of hundred lines. You may have
noticed that all of the examples in the tutorials so far all use indentation - the code inside loops is offset from
the rest of the code. One of the more common errors in programs by new programmers is failing to close
loops. Your program may have 20 Repeat lines but only 19 Until lines and once you have a decent sized
program, these can be difficult to find - if you don't use indentation...

As a rule of thumb, I always increase the indentation of the code inside every loop by two spaces and drop
back to the left at the end of the loop. Some people use three spaces, but I find that with a few nested loops
(loops within loops) all but the shortest lines of code are off the right side of the screen!

I have always indented Open File lines too as they have to be closed and are similar to loops in that respect.

An indentation example (not real code):

Line 1
Line 2
Do
This is indented by 2 spaces
So is this
For A=1 To 10
Indented y 2 more
Ditto
Next A: Rem This drops back 2 as the For Next is closed
More lines
Repeat
For B=1 To 5
Indented 2 more
So is this
Next B
Until Z=5
Nearly finished
Loop: Rem Back to the left edge
End

20
As you can see, indentation makes all the loops stand out so missing Next, Until and EndWhile lines are easier
to pick up.

Layout:

As well as indentation, you should also adopt a suitable layout for your programs. Once again, it doesn't always
make them run any quicker (though it can), but program development time and more importantly error
tracking time can be reduced dramatically.

I have always said to people who have asked me how long it would take to write an application for them, "two
months to write it, six months to get it working properly and forever to get rid of all the bugs". Although a joke,
this isn't too far from the truth, so anything you can do to help along the last one is a bonus.

Finally, a proper layout for your programs will make them smaller and more efficient. Useless or repeated code
can be avoided and modifications are made a lot easier. So what does this involve?

First of all, your programs should have the following format:

* Initialisation
* Game Menu
* Main Program Do…Loop
* End Of Program
* Subroutines
* Functions

What Are Subroutines (sometimes called Procedures):

These are vital to keeping your programs running smoothly. You can write programs without them, but once
you have used them, you wouldn't dream of going back to being without them!

Basically, you can think of a subroutine as a little stand-alone program which you can call on at any time. The
code in a subroutine is something that can be called many times in your program - and hence prevents you
having to retype the code each time you need it.

If you needed to print ten lines of text in three different places in your program, rather than have thirty lines of
code, you would put the ten lines into a subroutine and use a single line of code to call the subroutine
whenever you needed the ten lines printed.

Alternatively a subroutine can simply be code that you may only need once, but just want to keep in a
separate location making your program tidier.

Subroutines start with a label and end with the line Return. The command GOSUB is used to call the
subroutine and as the calling line is stored, the Return bit at the end knows where to jump back to after the
code in the subroutine has been executed. For this reason, you should never exit out of a subroutine by using
GOTO, though calling another subroutine from a subroutine is OK as it too will return automatically.

On return from a subroutine, control is passed to the line immediately after the GOSUB line.

Although a subroutine is a separate block of code to the code in the main program Do…Loop, it is still classed
as part of your main program. As such, all variables in your main Do…Loop are available in a subroutine and
any alterations made to them in the subroutine are seen by your main program.

Having now covered subroutines, you now know enough to continue with our list above (if you were
wondering why I didn't start with item 1 - Initialisation).

21
Initialisation

At the start of your program, you need to initialise all the required variables, set the screen mode, maybe
create a matrix, load images and objects, turn off the mouse if necessary and all those sort of tasks. I put all of
these into a subroutine called Setup. Your program then only needs a single line which says GOSUB SETUP at
the start to set everything up rather than having all that code cluttering up the start of your program.

Main Menu

Next, you may want your program to display a menu screen with buttons for things like Options, 1 Player
Game, 2 Player Game, Start Game and Exit - rather than just starting the game.

As you need to call this menu at the end of each game, you should place it in a procedure.

Inside the procedure you should have a conditional loop like Repeat..Until which is only exited if the user clicks
on one of the Start Game buttons.

At the end of any game, you can simply call this procedure again with Gosub to display the menu screen again.

Main Program Do…Loop


Your main program should be enclosed in a Do…Loop so that when it runs, it doesn't end until you want it to.
Some coders prefer to use Repeat...Until for the main loop. Either will do - it's a matter of personal preference.

When the time comes for it to end, you can use the END statement inside a condition along the lines of 'if the
user presses the X key then end the program'.

From within the main program loop, subroutines can be called and they will always return back to continue
round the loop.

If you are writing a 3D program, then the last line inside the loop should be a Sync. More will be said about
Sync later in the tutorials, but for now all you need to know is that in most 3D programs, for optimum speed,
you will want to control the screen output yourself and issuing a Sync updates all the 3D information on the
screen. Each Sync takes time, so one placed at the end of the main loop refreshes the screen with optimal
performance.

End Of Program

This is simply the word END which stops your program from running. Placed on the line following the LOOP
line, it should never be executed as there is no way to get out of the Do…Loop to even reach it.

However, if program control ever reached this point, then it would crash through any code which follows -
something you don't want as it would cause an error if all your subroutines follow.

So, putting it in doesn't do any harm - I do it out of habit…

Subroutines

This is the place in your programs where you put all your subroutines, one after another. I tend to put a Rem
line immediately before each subroutine with a brief description of what it does.

Note: Rem is short for REMark and is purely a comment. Use them anywhere in your program to leave yourself
'notes' on what a section of code does or as a reminder to return later and alter something. Remarks have no
overhead on your programs as they are completely ignored when your program is compiled - they only exist in
the editor!

22
So, a program with tons of comments in the editor will have exactly the same size compiled exe as the same
program with none so USE THEM! Load a program written months ago and you'd be surprised how little you
will recognise - even if you did write it yourself. Lots of comments will remind you of what does what and you'll
be glad you added them.

Functions (In brief):

I'm not going to cover functions in great depth here as there are more detailed tutorials on the subject
available.

Functions are for all intents and purposes the same as subroutines but with a couple of differences.

They are blocks of code like subroutines and after being used return to the line after the one they were called
from - just like subroutines. But, functions start not with a label but with the word FUNCTION followed by the
function name and an optional parameter list in parenthesis which combined is called the function header.

Functions end with the word ENDFUNCTION and an optional variable which is used to pass data back to the
calling line. The calling line uses the function name and must include a list of parameters identical to the
function header (if any parameters are used).
This sounds quite complicated so let's see an example:

Function Pointless(A,B,A$)
L=Len(A$)
C=L*A+B
EndFunction C

OK, this aptly named function receives integer values which it places into the numeric variables A and B, and a
single string which it puts into the string variable A$. It calculates the length of the supplied string using the
Len() function and stores it in the variable L.

Next, it multiplies the value of L with the value passed to it in A and adds the value in B, storing the result in
the variable C. Finally, the function ends and passes the value stored in C back to the calling line.

So, an example of this function being called might be:

NumVar=Pointless(10,5,"Hello")

As the function returns a value, we have to use a variable to 'catch' what is returned - hence the NumVar= but
at the start. If the function returned no value, this bit could be omitted making the calling line
Pointless(10,5,"Hello").

Next comes the function name and the parameter list in parenthesis. When the three items get to the function
they are placed in A, B and A$ respectively. We are passing proper numbers and a literal string here, but we
could have used variables instead. If we did, the variable names would be irrelevant, but the variable types
must match. The same goes for the return variable type too. The only restriction is that you cannot pass arrays
to a function.

In the function, L becomes 5 (the length of the word Hello), which is multiplied by A (10) and has B (5) added to
it. The result (55) is returned by the EndFunction and is caught be the variable NumVar.

Note however that only a single item of data can be returned from a function. That's one numeric variable or
one string - no multiple variable lists like the entry parameters.

Another important difference with functions is that they use local variables (see subject Scope below). Variable
names inside functions can be the same as those outside the function but still be completely separate entities
which can co-exist together. Local variables are destroyed when exiting the function.

23
Before we go any further, I think it would be best to spend a few moments covering local variables and the
other type - global variables. I didn't cover them in the variables section in Part 1 of this series as they are only
of any relevance when you know what functions are, so I left it until now to cover the subject.

Scope

In non-technical terms, scope is where in your programs your variables can be seen. In many programming
languages you have both Local and Global variables which are declared before you use them. Where you
declare them affects where you can see them.

Global variables can be seen anywhere in your program. This sounds a bit obvious until you realise that Local
variables are local only to functions - they cannot be seen outside of the function that they are used in.

Local Variables:

DB Classic is slightly different to most programming languages as global variables don't officially exist. For
example, if in your main program you say A=10 then A can be seen throughout your program - including
procedures but excluding functions.

Inside a function, you can also have A=100 but you have to remember that this A is local and is an entirely
separate A to the one outside the function - in your main program. When you exit from the function, the
function's A variable is destroyed and you will find that the original A still contains 10 - not 100!

Likewise, if in your main program you had B=100, when you enter a function, B does not exist, so you can't use
it! That's why you have the parameter list in the function header so you can pass the variables you do need to
the function. For the same reason you have the variable to use with EndFunction so you can pass the result
from the function back to your main program. This is what local variables are all about.

Global Variables:

OK, earlier I said that in DB, global variables don't officially exist. Well they don't, but there is a work-around
you can use if necessary.

It turns out that arrays declared using DIM in your main program are sort of global in so much as you can see
them inside functions, alter their contents inside the function and the alterations are intact in your main
program when you exit the function!

This gives us a very nifty way around the fact that you can only pass a single variable back from a function.
With arrays we have no need to pass more than one as we can use arrays.

Arrays declared inside a function however are still local and are destroyed on exit. They cannot be seen on
return to the main program. Also, as they are destroyed when exiting the function, you can DIM an array at the
start of a function and call it many times without getting an 'Array Already Dimensioned' error. You don't have
to 'Undim' as you would in the main program.

This also conveniently gets around the limitation mentioned earlier that you cannot have an array in the
parameter list you pass to a function. Now you don't need to as the array can be seen and modified inside the
function anyway!

And that's program layout covered. Our skeleton program layout should therefore look something like this
(not real code - layout example only):

24
Gosub Setup
Gosub Main_Menu

Do
Rem Lines of main program here
Gosub ThisRoutine
Gosub ThatRoutine
If game ends then Gosub Main_Menu
K=ReadKeyboard(X,Y,Z)
Loop

End

Rem ***********************
Rem List of subroutines start here
Rem ***********************
Rem This routine does something
ThisRoutine:
Do Whatever
Return

Rem This routine does something else


ThatRoutine:
Do Whatever
Return

Rem This routine displays the main menu


Main_Menu:
MenuExit=0
Repeat
Display screen and buttons here
If 1 Player Game button clicked
Do whatever to set up single player game
MenuExit=1
Endif
If 2 Player Game button clicked
Do whatever to set up 2 player game
MenuExit=1
Endif
Rem If Exit button clicked then End
Until MenuExit=1
Return

Rem This routine sets up the program


Setup:
Do Whatever
Return

Rem *********************
Rem List of functions start here
Rem *********************

Function ReadKeyboard(X,Y,Z)
Do something with the keyboard here
EndFunction F

If you use this sort of layout, you should find programming a lot more fun than if you just bang out the code all
over the place - especially if it doesn't work when you hit that F5 key…

When you add another feature to your program, you simply put it in another subroutine in the subroutine

25
section and add the calling line into the main program loop. Doing this keeps the code in the main loop as
short as possible and easier to follow.

That's it for Part 2 of the Programming For Beginners tutorial series.

--ooOOoo—

Part 3 - Elementary Commands

In this part of the series, we will be looking at some of the elementary commands in Dark Basic - many of
which will work in any version of Basic and are therefore 2D only. DB's more advanced 2D and 3D commands
will be covered later in the series. For now, we are just learning how to program, so we need to keep things
simple.

If you look at a toddler’s first reading books, you won't find any Chaucer or Shakespeare - just Janet and John
stuff. It's important to remember that if you don't get to grips with the Janet and John stuff in DB (Text & 2D),
then you have no chance of understanding the Shakespeare stuff (3D) when we get to it! I'll try to group the
commands in categories of similar types, but in no particular order.

The commands in this part of the series will all be 2D, but 3D will be mentioned occasionally only if there is
anything important to say. The main thing is that a grasp of the simple commands in this tutorial will allow you
to create complex but simple complete programs in Dark Basic.

Finally, it's worth mentioning that the order in which the commands are covered may seem strange. This is
because I have tried whenever possible to cover commands in such a way as to prevent the need to jump
ahead in the text to look up something not already covered.

CLS is a good example as you can use the RGB command as an additional parameter. So, RGB is covered first
when really CLS (being one of the more basic commands) would have normally come before it.

When you write any computer program, you need a method of putting information onto the screen. A number
of commands are available for both text and graphics - some of which do the same thing, but with subtle
differences. You choose which to use depending on the circumstances.

Screen Output

A screen consists of a grid of dots - initially all set to black. Each dot (or pixel) on the screen has a co-ordinate
comprising of an X and Y value.

The X value starts at 0 on the left hand side of the screen and increases as you move right. The maximum value
for X depends on the current screen size. By default, unless you specifically change the screen size, DB
programs run in a screen size (resolution) of 640x480. This means that X runs from 0 to 639.

The Y value starts at 0 at the top of the screen and increases as you move down the screen. The maximum
value for Y also depends on the current screen size and in DB's default resolution of 640x480 Y will run from 0
to 479.

26
The X and Y maximum values are always 1 less than the actual screen resolution and specifying a location on
the screen is simply a case of providing the X and Y positions.

SET DISPLAY MODE

This command is placed at the start of your program and allows you to specify the screen mode you want your
program to run in and uses the syntax Set Display Mode Xrez,Yrez,BitDepth.

Most of the usual X/Y Windows modes are allowed, such as 800x600, 1024x768 and so on and the BitDepth
parameter is usually 16 or 32 to select the maximum number of colours available. 16 is the norm as some
graphics cards will not run at 32 bit in some screen resolutions. Personally, I tend to do everything with:

Set Display Mode 800,600,16

[Edit] This tutorial was written a few years ago when most DB users worked in this resolution. These days,
systems are a little better and 1024x768x32 (or even higher) is more common.

RGB(Rval,Gval,Bval)

RGB stands for Red, Green, Blue and is the method DB uses to specify a colour. All colours on a standard CRT
monitor are created by red, green and blue guns which light up dots on the screen.

The intensity of each of the colour components defines the colour of the dot produced. The lowest intensity
value for the three components is 0 (zero) and with red, green and blue all set to 0, you get a black dot. The
highest value for each component is 255 and with red, green and blue all set to 255, you get a white dot. By
varying the values for all three components, any desired colour can be reproduced.

The three intensity values are supplied in parenthesis in the order red, green blue. So, if you wanted full
intensity green, you would use RGB(0,255,0), whereas RGB(0,100,0) would produce a darker shade of green.

The colour created can be applied to any screen output and the command must be issued again to switch to
another colour. The colour is always selected before the command which draws to the screen - not after.
Anything already on the screen will remain the designated colour when RGB is used to select another colour.
Here are some other common colours as RGB commands:

Red - RGB(255,0,0)
Blue - RGB(0,0,255)
Magenta - RGB(255,0,255)
Yellow - RGB(255,255,0)
Cyan - RGB(0,255,255)
Grey - RGB(150,150,150)

Internally, these intensity values are converted to a colour number which you can also use rather than RGB if
you want. One advantage of doing this is that the removal of a calculation makes your program run quicker -
albeit only by a miniscule amount. Don't forget however that there may be many, many RGB calculations in
your programs and all these tiny amounts soon add up...

However few people do use colour numbers as three RGB values are infinitely more user-friendly to use.
What's easier to use for a colour: RGB(100,255,50) or a number like 32742?

27
RGBR(), RGBG() and RGBB()

If however you do have a colour number like 32742, how do you turn it into red, green and blue colour values?
Well, it's easy with the RGBR(), RGBG() and RGBB() functions where you put the colour value into parenthesis:

Red=RGBR(32742)
Green=RGBG(32742)
Blue=RGBB(32742)

This will pull out the red, green and blue intensities from our example colour value of 32742 placing the values
into the variables red, green and blue respectively, after which:

Ink 32742,0

... and

Ink RGB(Red,Green,Blue),0

will produce the same colour.

INK

Having the ability to define a colour, you now need to apply it. This is done with the Ink command which uses
the syntax:

Ink ForegroundCol,BackgroundCol

The two parameters can both be RGB values, colour values or a combination of both.

If you imagine the capital letter A is produced by dots on a grid, then the foreground colour is the colour of the
dots which form the letter A and the background colour is the colour of the dots which form the unused part
of the grid. If you have a red screen and want to print white text onto it, then you would set the foreground
text colour to white and the background colour to red with:

Ink RGB(255,255,255),RGB(255,0,0)

Once again, colour values can be used with the Ink command, but are seldom used. The only exception is the
colour black which is 0, so setting either the foreground or background to black can be done with a 0 rather
than RGB(0,0,0) - it's one less calculation for DB to carry out so it's faster. So, white text on a black screen
would be done with:

Ink RGB(255,255,255),0

CLS

The CLS command clears the 2D screen. It has no effect on 3D screens. If you have a screen with both 2D and
3D areas, then CLS will only clear the 2D portion of the screen. Used on its own, CLS clears the screen to black,
(assuming you have not changed the current ink background colour), though you can also use an optional RGB

28
command (or colour number) to clear the screen to any other desired colour. CLS RGB(255,0,0) for example
will clear the screen to red.

Text Commands:

PRINT

Print is the easiest way to get text onto the screen and can print literal strings enclosed in quotes, or variables.
The screen output appears at the current cursor position (which after a CLS, is in the top left corner of the
screen). You can however position it anywhere you want.

Some examples:

Print "The cat sat on the mat"


Print A$
Print NumVar

The first example is a literal string. You can use this to print a simple text message on the screen like "Press Any
Key To Start".

The second and third examples both print the contents of variables - not the names of the variables used (A$
and NumVar). Output appears on the screen at the current cursor position.

After printing to the screen, the cursor position drops down to the start of the next line on the screen - ready
for the next print statement. You can prevent this from happening (so you can print more on the end of the
same line) by placing a ; (semi-colon) on the end of the Print line.

Following a Print statement with a ; on the end, the next Print statement will appear on the end of the line just
printed.

Print "Somewhere"
Print "Over"
Print "The"
Print "Rainbow"

will make the following appear on the screen:

Somewhere
Over
The
Rainbow

Whereas...

Print "Somewhere";
Print "Over";
Print "The";
Print "Rainbow"

will result in:

29
SomewhereOverTheRainbow

...appearing. Notice that everything is all joined together? That's because we didn't include a space after each
word inside the quotes. Don't worry too much about that as it's very unlikely you would ever use the Print
statement like that anyway - you would just use:
PRINT "Somewhere Over The Rainbow"

Another useful variation of using PRINT is:

Print "Somewhere ";"Over ";"The ";"Rainbow"

Which at first glance doesn't appear to be of much use. However, when combined with variables allows you to
created formatted strings. Take for example:

A$="Henry VIII"
WifeCount=6
Print A$;" had ";WifeCount;" wives."

This will print onto the screen:

Henry VIII had 6 wives.

As you can see, this allows you to present personalised messages in your games. Also worth noting is that you
can also include variable formulas in Print statements too. Such as:

A$="Henry VIII"
WifeCount=6
Print "If ";A$;" had married again he would have had ";WifeCount+1;" wives."

This will print:

If Henry VIII had married again he would have had 7 wives.

There are other ways to control what appears on screen when using strings, but they are covered later in the
'More Strings' section.

SET CURSOR

As mentioned in the Print command section, Print will print your text at the current cursor position. If however
you want to print something somewhere else, you need to be able to position the cursor where you need it.
This is done with the SET CURSOR X,Y command where X is the position across the screen and Y is the position
down the screen.

Set Cursor 10,10


Print "Cursor position 10,10 is here!"

Note: The X and Y referred to here is NOT a pixel position, but a character position, so the above message does
not print at 10 pixels across and 10 pixels down. The values you can use depend on how many characters will
fit across and down the screen - i.e. the screen mode.

30
STR$()

Str$() is a function which will convert a number or numeric variable into a string. This is most commonly used
for the Text command - why it is introduced here. An example:

A=42
MeaningOfLife$=Str$(A)
Print MeaningOfLife$

This will print '42' on the screen, but it's important to realise that it is the string 42 - not the number 42! Also,
as a string, any of the many string functions can be applied to it which couldn't when it was a numeric variable.
This is a valuable feature which will become more apparent later on.

TEXT

This is a much improved version of the Print command and uses the format:
TEXT X,Y,Output$

...where X and Y are the desired pixel positions (not character positions) on screen and Output$ is the required
information you want to appear. The output part is similar to when using the Print command so you can use
literal strings or variable strings. The main difference is that you cannot use numeric variables with the Text
command as you can with Print - you need to convert them to strings first using the Str$() function.

Another difference is that when using Text to create formatted strings, the + symbol is used instead of the ;
symbol.

Also, unlike the Print command, anything placed on the screen with Text is printed in the current font face and
size - Print just uses the default system font. Our above Print example using the Text command to place the
message 100 pixels across the screen and 100 pixels down the screen would look like this:

A$="Henry VIII"
WifeCount=6
Text 100,100, A$+" had "+Str$(WifeCount)+" wives."

CENTER TEXT

A variation on the Text command is the Center Text command which will - believe it or not - print a message
centred around a given X position on the screen. The command syntax is:

Center Text X,Y,Output$

...where the parameters are the same as the normal Text command apart from X which is the screen X position
at which the text is to be centred. In other words, X is the position on screen at which the centre of your string
will be positioned. If your screen is 800x600, then for your message to be in the centre of the screen along the
X axis you would use:

Center Text 400,300,"This is in the centre of the screen"

The command does all the string length calculations for you and all the usual Text command formatting rules
apply. You must figure out the Y position yourself!

31
Graphics Commands:

As well as text output using the Print and Text commands, you also have the ability to output basic pixel
graphics. Once again, DB is aimed at the 3D games programmer, so you do not have a substantial subset of
such commands - they just aren't called for often enough.

DOT

This command lets you 'turn on' a single pixel on the screen and is basically a 'Plot' function. Using the syntax:

DOT X,Y

...the pixel at co-ordinate X,Y will appear in the current Ink colour.

BOX

This command creates a filled box and uses the syntax BOX Left,Top,Right,Bottom. In effect, Left and Top
define the X and Y co-ordinates of the box's top left corner while Right and Bottom define the X and Y co-
ordinates of the box's bottom right corner. Unlike some BASICs, in DB you do NOT define the top left corner
position then the box's width and height.

As with all 2D graphics commands, the Box command uses the current Ink colour.

LINE

This command will draw a line in the current ink colour. The syntax is:

LINE StartX,StartY,EndX,EndY

...and you simply supply the X and Y screen co-ordinates of the start and end of the line.

CIRCLE

This command will draw a circle in the current ink colour. The syntax is:

CIRCLE CentreX,CentreY,Radius

...and you simply supply the X and Y screen co-ordinates of the circle's centre along with the circle's radius in
pixels.

ELLIPSE

This command draws an ellipse in the current ink colour. The syntax is ELLIPSE
CentreX,CentreY,Radius1,Radius2 and you simply supply the X and Y screen co-ordinates of the ellipse's centre
along with the ellipse's X radius and Y radius in pixels.

POINT()

This function will return the colour number of a pixel on the screen. Using the syntax
Point(X,Y). X and Y are the required pixel's X and Y co-ordinates. The value returned is the colour value which
needs to be converted to R, G and B values to be of any real use.

32
Non-Output Commands:

The following commands do not send anything to the screen - they just alter the way things are sent.

SET TEXT OPAQUE

This command switches text transparency off and applies only to the Text command - not Print. When
transparency is off then the current text background colour is shown. If this colour is not the same as the
current screen colour then the text will appear in a coloured rectangle in the chosen Ink background colour.

SET TEXT TRANSPARENT


This command switches text transparency on and applies only to the Text command. When transparency is on,
then the text background colour is not displayed and the background screen colour shows through. Enter and
run this example...

CLS RGB(30,0,50)
Ink RGB(255,255,255),RGB(150,0,0)
Set Text Opaque
Print "1. This is text produced with the Print command"
Text 100,100,"Text produced with the Text command (Opaque)"
Set Text Transparent
Print "2. This is text produced with the Print command"
Text 100,120,"Text produced with the Text command (Transparent)"

In this example, the screen is cleared to a dark purple colour and the ink set to white foreground and red
background.

The text is set to opaque and a message is then printed to the screen using both Print and Text commands.
Notice that only the message produced with the Text command has the visible background red colour whereas
the Print command text is unaffected.

The text is then set to transparent and the colour scheme left unaltered. The messages are printed again and
this time the Text command does not show the red background colour.

You will also notice that the Text command has no effect on the screen cursor position. The message created
with the Print command appears on the next line to the last printed message - even though a Text command
has placed a message in the middle of the screen since the Print command.

More Strings:

As mentioned earlier, there are a lot of useful string formatting functions which can be used - some of which
we will cover now. Many can be used to present numeric information on the screen in a more tidy fashion. For
example, ever noticed in games where the player's score is say 3250, it appears on the screen as 0003250? The
score is always 7 digits long even though the actual score is only 4 digits!

The score is stored in a numeric variable and you can't add on the leading zeros. So, you convert the score
from a number to a string, use the string functions to add those zeros and print the resulting string onto the
screen rather than the contents of the numeric variable. Just one of the many things you can do with strings.

So let's look at what we need in order to do the score thing...

33
LEN()

Len is short for Length and as the name suggests, the Len() function will tell you the length of a string. If our
player's score (say it's 200 for example) is stored in the numeric variable Score, then we can't use a string
function on it so we have to convert it to a string first with Str$() which we covered earlier.

This is done with:

ScoreStr$=Str$(Score)

Now we have a string variable called ScoreStr$ which contains the players score value (200) as a string. We can
now use Len() to tell us how long the string is with:

ScoreLen=Len(ScoreStr$)

You will notice that Len() returns a number - not another string. This is so we can use the number in
calculations. In this case, Len() will return the value 3 as the score 200 is three characters long. We can use this
number to find out how many 0's to add to the front of the string. We want our score to always be printed
onto the screen with 7 digits, so we create a small program loop which will repeat the Len() function until the
string is the required length:

ScoreStr$=Str$(Score)
Repeat
ScoreStr$="0"+ScoreStr$
ScoreLen=Len(ScoreStr$)
Until ScoreLen=7

What this does is add 0 onto the front of our string containing 200 and then test the length of the string. If it is
less than 7 then the loop is repeated. When the last 0 is added to make ScoreLen equal to 7 then the loop is
exited.

All well and good - apart from one major problem... can you spot it?

What if the current score is say 1253843? In this case, the length of score as a string is already 7 so the loop
will add another 0 to the front making it 8 long. When you get to the Until ScoreLen=7 condition for continuing
the loop, it will not be 7 so the loop will continue adding 0's trying to reach 7 - and of course it never will!

So, what we have hit on is a prime example of when you have to choose your loops carefully. We said in an
earlier tutorial that there were different ways to create a loop and they seemed to do the same thing so why
have so many?

Let's see the above code written using a different type of loop and you should see the difference more easily:

ScoreStr$=Str$(Score)
While Len(ScoreStr$)<7
ScoreStr$="0"+ScoreStr$
EndWhile

It's shorter, so therefore faster at doing the job, but more importantly the While...EndWhile loop will not be
entered at all if the current score is already 7 characters long (or in fact longer) - unlike the Repeat...Until
version. This is because the condition for carrying out the code inside the loop is at the beginning of the loop -
not the end!

34
Knowing which type of loop to use and where, comes when you have a little more experience, but the point is
that they do all have their differences - and uses.

On the subject of speed, you will quite often hear people talking about doing something 'this way' instead of
'that way' as it's faster. Most computers these days have fast processors - measured in GigaHertz rather than
MegaHertz. My first PC had a 33MHz processor and 4MB of memory! So, you will probably think that speed is
no longer an issue. However, there are a couple of things you have to consider:

1. Not everyone has a super-fast computer. There are still a lot of older machines out there, so what runs
smoothly on your machine may not do so on everyone else's machines. This however is only important if you
intend other people to use the programs you write. If you only write stuff for yourself then you don't need to
worry about this aspect.

2. Dark Basic Classic is an interpreted language. When your program has grown quite large (as they always do)
and there is a lot going on, it will slow down even on the fastest of machines. DBPro is a compiled languages
and is therefore a lot faster and less of a problem in this respect.

So, although using one method may only be a tiny fraction quicker, all these fractions can add up and overall
make a significant difference in the speed that your program runs.

Chopping And Changing

Sometimes, you need to manipulate strings. As with all BASIC's, DB provides a number of commands to do this.
When you are learning to program in DB you start off with text only programs then move onto 2D graphics
then finally 3D graphics. Those who are completely new to programming and jump straight into 3D don't
usually last very long.

Actually, you would probably be surprised how vital the string commands can be for all types of program - 2D
and 3D as well as text programs. Formatting information printed on the screen, shuffling a pack of cards and
saving files to disk all involve working on strings...

LEFT$()

This function will pull out a number of characters (a substring) from the beginning (left end) of any string. The
syntax is:

Left$(Main$,NumChars)

...where Main$ is the string you want to extract the substring from and NumChars is the length of the required
substring. If the value used for NumChars is greater than the length of Main$ then just the available characters
are returned.

As with all DB commands, you can use literal strings where the data is entered directly into the command, or
variables. Examples:

Print Left$("Children",5)

... will print 'Child'

So, assuming that A$="ABCDEFGHIJKLMNOPQRSTUVWXYZ":

35
Print Left$(A$,5)

... will print 'ABCDE'

B$=Left$(A$,3)

... will take 'ABC' from A$ and place it in B$

A trivial example which demonstrates the sort of thing possible with Left$(). Just copy it into DB and run it!

A$="Darth Vader was the baddy in Star Wars."


B$="k is the 11th lower case letter of the alphabet."
C$="Basil is a herb."
D$="icicles are made of frozen water."
Print Left$(A$,3);Left$(B$,2);Left$(C$,3);Left$(D$,2)

RIGHT$()

This is exactly the same as Left$() but the substring is taken from the end of the main string rather than the
start. The syntax is also the same - Right$(Main$,NumChars).

Print Right$("Children",5) ... will print 'ldren'

And again assuming that A$="ABCDEFGHIJKLMNOPQRSTUVWXYZ":

Print Right$(A$,5)

... will print 'VWXYZ'

B$=Right$(A$,3)

... will take 'XYZ' from A$ and place it in B$

MID$()

The third in the collection is MID$() which allows you to pull out a substring from the middle of the main
string. To be honest though, saying 'substring' is a little misleading as the syntax is

Mid$(Main$,StartPos)

...and as you can see there is no 'length' parameter like many versions of BASIC provide - just a start position.
This means that you can only grab one character at a time.

If you need to extract more than one character to build a substring you can do so with a loop. Example time:

Print Mid$("Children",3)

... will print 'i'

Once again, assuming that A$="ABCDEFGHIJKLMNOPQRSTUVWXYZ":

36
Print Mid$(A$,5)

... will print 'E'

B$=Mid$(A$,4)

... will take 'D' from A$ and place it in B$

To grab a substring from say position 7 to 11 in A$ you would use:

A$="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Sub$=""
For N=7 To 11
Sub$=Sub$+Mid$(A$,N)
Next N

... after which Sub$ would equal 'GHIJK'. For those of you with prior knowledge of other BASIC's, this is the
same as Mid$(A$,7,5).

As you can see, using Left$(), Right$(), Mid$() and adding strings together, you can do some really clever stuff.
There are however some other goodies...

VAL()

This function will return the numeric value of a string containing a number. In effect it is the reverse of Str$()
so if you have Score=1000, A$=Str$(Score) will create a string called A$ containing '1000'. Score=Val(A$) will
then turn the '1000' string back into a numeric variable called Score - replacing what was there already.

Things You Should Know About VAL()

If the string you are getting the value of starts with anything other than a number, 0 (zero) is returned.

If the string starts with a number but also contains other non-numeric characters then VAL will return the
value of the numeric characters UP TO (but not including) the first non-numeric character. It's up to you to
check that the string you are using with Val() is in the correct format.

Examples:

Rem Numeric variable Age will contain the value 32


Age=Val("32")
Rem Numeric variable Age will contain the value 24 ('Years' ignored)
Age=Val("24 Years")
Rem Numeric variable NumVal will contain the value 0
NumVal=Val("HSGH32433")

ASC()

ASC is short for ASCII and this function will return the ASCII code of a single character. Each alpha-numeric
character capable of being printed to the screen has an ASCII code. The character 'A' for example has the ASCII
code 65, 'B' is 66 and so on. Also, remember that numbers also have ASCII codes, so Asc("7") is legitimate and
will return the ASCII code of the character '7'.

Asc() can accept a string, but only the first character will be checked. Asc("F") will return exactly the same ASCII
code as Asc("Fred") as only the F is decoded. Likewise, Asc("7") will return exactly the same ASCII code as

37
Asc("73621").
Asc() can also be useful for sorting lists of strings into alphabetical order.

CHR$()

This is the reverse of ASC() and given an ASCII code will produce the equivalent string character. Printing
CHR$(65) to the screen will result in a capital 'A'.

UPPER$() and LOWER$()

These two functions are used on strings to convert the string's contents to upper and lower case respectively.
A good use for Upper$() is when you need to compare two strings in your programs.

Say for example you were writing a program where the user has to enter a string and what your program does
next depends on what is typed. In a space game for example, your 'on-board computer' may ask for a
destination planet which the user types in. Your code may say:

Input "HAL: What is your new destination? ",Planet$


If Planet$="Neptune" Then Gosub Dest_Neptune

But, what if the user typed NEPTUNE or neptune - or any other combination of upper and lower case
characters? Well, the subroutine would never get called - and they would never get to Neptune!

You would have to have an If line for every possibility - and that's only for a single destination!

Or, you could just use Upper$()!

Input "HAL: What is your new destination? ",Planet$


If Upper$(Planet$)="NEPTUNE" Then Gosub Dest_Neptune

Now, it doesn't matter how Neptune is typed, as long as it's spelled correctly, as the upper case version will
always be 'NEPTUNE' and the subroutine will always be called. The original contents of Planet$ in this example
are unchanged - you are just creating a temporary upper case version to compare with. If you want to do a
permanent conversion, you would use:

Planet$=Upper$(Planet$)

... when the user originally enters the information.

Yet More String Functions!

If you are reading these tutorials as you are new to DB but not programming, then you may notice that DB isn't
bursting with string functions. This is because DB is primarily a game writing language and generally speaking
there isn't a great need for them in games. What you do get is usually sufficient though.

38
Other Useful Commands For Beginners

INC

Inc is short for Increment and is a quick way to do addition with variables. If we wanted to add an amount to
the variable 'A' we would normally use A=A+1 (or + any other value). This can be replaced with the following:

Inc A: Rem Increments the value currently in numeric variable A by 1


Inc A,3: Rem Increments the value currently in numeric variable A by 3

DEC

Inc is short for Decrement and is a quick way to do subtraction with variables. If we wanted to subtract an
amount from the variable 'A' we would normally use A=A-1 (or - any other value). This can be replaced with
the following:

Dec A: Rem Decrements the value currently in numeric variable A by 1


Dec A,3: Rem Decrements the value currently in numeric variable A by 3

RND()

All games rely on an amount of randomness. A card game wouldn't be any fun if the cards always came out in
the same order or the aliens came down the screen in the same place every time. So, you have a function
RND() in DB which will return a random number from a given range. Using the syntax Rnd(Value), it returns a
value between 0 and the supplied value. The number returned is inclusive, so Rnd(10) can return 0, 10 or any
number between.

The seed for the random number generator is based on a register in your PC's hardware when it is turned on
and doesn't change until you restart your PC. As such, the random numbers are the same every time you run
the program - hardly random. Run the following program a few times:

For N=1 to 5
Print Rnd(10)
Next N

You will see that each time you run it, the same 5 numbers are produced. To generate a different sequence of
numbers each time it is run, we need to re-seed the random number generator with RANDOMIZE.

RANDOMIZE

This command will re-seed the random number generator and uses the syntax:

Randomize Seed

...where Seed is the numeric seed value.

However, if the seed is a constant number then the next set of randomly generated numbers will be different,
but only the first time they are generated - from then on they will be repeated. Still not random!

So, what we need to do is use a different number for the seed each time the program is run and the PC's built
in clock is ideal for this purpose. Timer() is a DB function which will return the number of milliseconds which
have elapsed since the PC was turned on. Using this as a seed will give us a different set of random numbers
each time the program is run. Simply use:

39
Randomize Timer()

at the start of your program. To test that this works, add this line to the above example to make:

Randomize Timer()
For N=1 to 5
Print Rnd(10)
Next N

Run this a few times and you should see a different set of numbers each time.

Random Numbers In A Specific Range

OK, so Rnd(10) will give us random numbers from 0 to 10, but what if we want the numbers to be between say
100 and 150?

Well this isn't difficult. All we have to do is calculate the actual range of numbers - if we want a number
between say 100 and 150 then we use 150-100 giving us an actual range of 50. As the smallest number we
actually want to be returned is 100, that's what we add to our random number.

A=Rnd(50)+100

... will do the trick!

Getting Information Into Your Programs:

Your programs would be pretty useless if there was no user-interaction - especially games! The user may need
to enter their name, select buttons on the screen, guide a spaceship, steer a vehicle or simply answer
questions. This is done by the mouse and keyboard or a joystick.

A number of commands are available to capture this information and in keeping with the beginners tutorial
theme I'll just be covering the basic essentials for the novice programmer.

The Mouse...

As you move the mouse around the screen, it generates an X and Y value and if your mouse has a wheel, it will
create a Z value when moved. The buttons also create a value, so let's see what functions we have to use to
get these values...

MouseX(), MouseY() and MouseZ()

These three functions return the respective mouse values as integers, though it's unlikely you will need
MouseZ() at this stage of learning. The syntax is:

NumVar=MouseX()
NumVar=MouseY()
NumVar=MouseZ()

where NumVar is the integer variable you want to store the relevant mouse position:

40
MouseClick()

This function returns an integer value representing the current mouse button status. When no buttons are
being pressed this function returns 0.

The left button is given the value 1, the right button the value 2 and the middle button (if present) the value 4.

Pressing more than one button returns the total of the pressed button's values so pressing the left and right
buttons simultaneously returns 3 (1+2). Pressing the centre button with the right button returns 6 (2+4).

At first, you will only be interested in whether or not the left or right button has been pressed so you will only
need to check if the value returned is simply 1 or 2 and act accordingly.

In your programs just place the following line at the beginning of your main program loop:

Mx=MouseX(): My=MouseY(): Mc=MouseClick()

With this line, at any point in your program, Mx will equal the mouse's X position, My it's Y position and Mc the
button status.

Clickable Screen Buttons

With the combination of knowing the X and Y position of the mouse at any time, along with the button status
you can create areas of the screen designated as 'clickable' in a button fashion. All you need to do is check to
see if the mouse is within these areas and if it is, when the mouse button is pressed. An example for you to try:

Ink RGB(255,0,0),0
Box 100,100,200,120
Ink 0,0
Box 101,101,199,119
Ink RGB(255,255,255),0
Text 135,102,"Exit"
Set Text Opaque
Do
Mx=MouseX(): My=MouseY(): Mc=MouseClick()
If Mx>100 and My>100 and Mx<200 and My<120
If Mc=1
End
Else
Text 0,0,"Mouse Now Over Button. "
Endif
Else
Text 0,0,"Mouse Not Over Button. "
Endif
Loop

This creates a red button on the screen with its top left corner at X=100 Y=100 and its bottom right corner at
X=200 Y=120. The word Exit is then printed in the middle of the button and the text set to opaque so that
messages on the screen overwrite each other without messing up the screen.

In the main Do...Loop the mouse functions are called to get the X, Y and button values and we do a test to see
if the current mouse X and Y position is within the defined button area. If it is not, then the Else part of the
code is executed and the message "Mouse Not Over Button. " is displayed.

41
If the mouse is within the button area another test is made on the current value of Mc - to see if the left
mouse button is pressed (Mc=1). If it is not (Mc=0), then once again, only the Else part of the code is executed
and the message "Mouse Now Over Button. " is displayed.

If the mouse button is pressed then the condition is met and the program ends - just like the label on the
button says!

You would repeat this process so you had as many sections of code as you had buttons - each checking for
different X/Y co-ordinates and doing different tasks for each button.

Also, the buttons here are drawn with code and look very basic. There's nothing stopping you from loading in
images for your buttons and placing them on the screen. As long as you know the X/Y co-ordinate of each
button's top left corner and the width and height of each button, the process to detect which one has been
clicked on is identical to that outlined above.

HIDE MOUSE

This command simply removes the pointer from the screen when you don't want it visible. While it is hidden, it
will still return all the normal values allowing you to replace the cursor with an image of your own if you want
to.

SHOW MOUSE

This turns the mouse pointer back on.

The Keyboard...

Getting information via the keyboard has more options. The first one is INPUT, but it has its limitations so you
also have Inkey$() and Scancode(). Each are used for a specific task.

INPUT

This is the main keyboard input function you will use at first, though it does have its bad points. It's slow and
clunky and only uses the default system font and colours. It uses the syntax:

Input StringVar$ or
Input Message$,StringVar$

...and your program literally stops running until the user types something in and presses the Enter key. This
makes it OK for entering the players name on the hiscore table or at the start of the game - but little else.

Input Name$

This will halt the program and wait for the user to type in their name and press the Enter key at which point
what they typed in will be placed into the string variable Name$. The program will then carry on its merry way.

Input Age

This will do exactly the same, but this time a numeric value is expected which when correctly entered will be
stored in the numeric variable 'Age'. If a non-numeric entry is attempted, it will be ignored.

42
This raises an important point. As nothing appears on the screen, how on earth is the user expected to know
exactly what they are supposed to type in?

That's what the alternative Input method is for:

Input "Please Enter Your Name: ",Name$


Input "Please Enter Your Age: ",Age

This version of Input will print a message on the screen before waiting for the user's input so at least they
know what the program is expecting them to type. You can of course replace the literal string with a string
variable if you wish.

At some later stage, you will decide that you don't like the way that the standard Input command works. With
a bit more knowledge you will probably decide to write your own function to do it.

Inkey$()

This function is called a 'polling' function as it polls the keyboard for keypresses without stopping like Input
does. When a key is pressed, it is stored in a string variable for use. Press the b key and the letter b is stored.
Press the p key and the letter p is stored and so on.

The next time the Inkey$() function is called the previous value is lost so if you don't 'grab' it while it's there,
it's immediately replaced by something else. When no key is pressed, the variable used contains a NULL string
("").

Inkey$() only works with characters which have ASCII codes. Some keys do not return any value at all - such as
the Shift, Control and Function keys.

So a good way to use this function is to create a loop which is 'locked' until the correct key is pressed. Here's a
useful Yes/No example:

Start:
CLS
Print "Do you want to end this program (Y/N)?"
Repeat
I$=Upper$(Inkey$())
Until I$="Y" or I$="N"
If I$="Y"
End
Else
Goto Start
Endif

OK, I know I said using Goto is bad, but this is only an example to demonstrate using Inkey$() and I wanted to
keep it short. I could have written a mere half a dozen lines to avoid it, but it wouldn't have demonstrated
Inkey$() any differently. In your proper programs just try to avoid Goto OK?

The example prints the message and drops into a closed loop which cannot be exited from unless the user
presses the Y or N key. Inside the loop, Inkey$() grabs the current state of the keyboard and places the upper
case version of it in the string variable I$.

As soon as the user presses either Y or N, the condition will be met and the program drops out of the loop and

43
into the If..Else...Then test where I$ is tested to see if it equals "Y" or "N". If it is Y then the program ends. If it
is anything else (and it can only be N if it isn't Y as any other keypress would not have allowed an exit from the
loop), then the program is not exited and is allowed to continue.

How about "Press Any Key To Continue" messages?

This is just as easy using Inkey$(). We know that when no key is being pressed, then Inkey$() will return a NULL
string, so all we need to do is stay in our loop until anything but "" is returned:

Print "Press Any Key To Continue"


Repeat
I$=Inkey$()
Until I$<>""

We don't need the Upper$ bit as it doesn't matter what key is pressed. The loop will keep going until I$ doesn't
equal "" and that can only happen if a key is pressed.

"OK, clever clogs" I hear you ask, "What can you use for menus"?

Inkey$() again! Take a look at the following simple example and then we'll go through it...

Center Text 320,100,"1. Menu Item 1"


Center Text 320,120,"2. Menu Item 2"
Center Text 320,140,"3. Menu Item 3"
Center Text 320,160,"4. Menu Item 4"
Center Text 320,180,"5. Menu Item 5"
Center Text 320,200,"6. Menu Item 6"
Repeat
I$=Inkey$()
Until Asc(I$)>48 and Asc(I$)<55
CLS
If I$="1" Then Print "You selected menu option 1"
If I$="2" Then Print "You selected menu option 2"
If I$="3" Then Print "You selected menu option 3"
If I$="4" Then Print "You selected menu option 4"
If I$="5" Then Print "You selected menu option 5"
If I$="6" Then Print "You selected menu option 6"

The first six lines simply print the menu entries 1 to 6. The loop is like the previous examples with the
exception of the exit loop condition. This time, we use ASC() to test the ASCII value of the key pressed. The
number 1 key has an ASCII value of 49 and 6 has the value 54. So, we only exit the loop if the key pressed has
an ASCII value greater than 48 and less than 55 - in other words only the number keys between 1 and 6.

On exiting the loop, the screen is cleared and the value of I$ is checked and the appropriate message printed
depending on what I$ equals. In your program, you would Gosub a routine rather than print a message, but
the method is still the same.

That will do for the Inkey$() function so let's take a look at the last keyboard input method - Scancode:

Scancode

This function will return a code representing the physical key on the keyboard as opposed to what is actually
printed on the key. This code value is totally unconnected with ASCII codes. For example, with Asc(), pressing
the A key on its own will return 97 - the ASCII code for lower case 'a'. Pressing the A key with the shift key will

44
return 65 - the ASCII code for upper case 'A'.

Scancode uses the syntax NumVar=Scancode() and returns 30 when you press the A key. There is no shifted
version as the shift key has its own Scancode. Remember, the 30 represents the key itself - not what's on it.

It's also worthwhile remembering that not all keyboards across the world are the same.

For example, your program uses the Q button to accelerate your vehicle and it uses the Scancode 113 to
detect when it's pressed. On your instruction screen you say 'Press Q to go faster'.

Pierre in France however presses Q and nothing happens! That's because he uses a French keyboard on which
the top row of letters are not QWERTY, but AZERTY. His Q key is in a different place and the Scancode of his Q
key is different. I use a Spanish keyboard and it has a different layout too.

If you had used Inkey$() instead of Scancode, then the Q key would have worked on all keyboards! You may
need to use Scancode, but bear this in mind.

Keystate

As each and every key on the keyboard generates its own Scancode, then it's impossible to detect more than
one key at a time using it as the last key you press replaces the code generated by the previous key pressed -
even if it's still being held down.

To get around this, you can use Keystate() which uses the syntax:

Keystate(Keycode)

...where Keycode is the Scancode for the key you are polling. When the corresponding key is not being
pressed, Keystate returns 0 and a 1 when it is.

The cursor up key has the Scancode 200 and Space Bar is 57. If you wanted to use the cursor up key to move
forward and the space bar to fire, then Scancode on its own would not work as you would stop moving every
time you fired. With Keystate it is possible to do both at the same time using something like:

If Scancode()=200
Rem Move Forward
If Keystate(57)=1
Rem Fire
Endif
Endif

To be honest, at this point we have only scratched the surface of commands in the DB library, but the
commands covered - few as they may be, are quite sufficient to write some quite sophisticated programs.

The only thing left to cover which we haven't done already is File Access, so we'll do that in the next part of the
tutorial.

45
Part 4 - File Access

All but the most basic programs use file access. Although strictly speaking this also encompasses DB's
commands for loading media files for your games such as images, sounds and models, this part of the tutorial
series covers saving your program's data to disk and reading it back in again.

This process is required for reading INI files, saving hiscore tables or creating new file formats for your new
world editor.

The basic process is to open a file for reading or writing, read in (or write out) the data then close the file.
What you write is up to you, so long as you read the information back in the same order.

Now I know many people will argue with me, but I have decided that it's far simpler to write data as ASCII text
files when you are learning to program. The main benefit is that you can open up your files after they have
been created to see if they actually contain what you thought you had written. Some of DB's save data
commands create encrypted files which can't be opened for examination - despite any advantages they may
have.

The first thing you have to do is open a file. Assuming that we need to write a file before we are able to read it
back in, this is done with OPEN TO WRITE.

OPEN TO WRITE

This command will create a new file on your hard disk and uses the syntax:

OPEN TO WRITE Channel,Filename$

Channel is an integer number and is like a 'stream' number. Filename$ is the filename you want to use.

The channel number is used because you can open more than one channel at a time. For example, you can
open channel 1 to read and channel 2 to write simultaneously, allowing you to read from one file and write
selected parts of it to a second file at the same time. By including the channel number in all of the commands,
DB knows which file to access.

It's like connecting a pipe from DB to the file on disk. The channel number tells DB which pipe to send the data
down when writing and which pipe to take the data from when reading. As long as you number the pipe(s),
open the correct valves (READ or WRITE) before using the pipe and remember to close the valves when you
are finished, you can use as many pipes as you need. Filename$ can be a specific filename including the full
path like:

C:\Program Files\MyprogData\Mydata.dat

It can also be relative, so using just a filename like 'Mydata.dat', the file will be opened in the current project
directory (where your DB program is located).

If you have a directory called 'DATA' in the current directory and wanted to save your data to a new file in
there, you would set the filename to 'DATA\Mydata.dat'. The process will fail if the directory DATA does not
exist though. So, to open a file called 'Mydata.dat' in the current directory we would use:

Open To Write 1,"Mydata.dat"

46
This creates an empty file called "Mydata.dat" and connects our 'pipe' which is labelled '1'. But, it is very
important that the named filename DOES NOT ALREADY EXIST. If it does, then you will get an error. To avoid
this, you have the FILE EXIST() function.

FILE EXIST()

So, before creating a new file, you should always check to see if it exists already with:

If File Exist(Filename$)=1
Rem Do Something About It
Endif

Here, the File Exist() function must be given the exact filename string as is used in the Open To Write
command or you may not be checking for the existence of the file in the same location. It therefore makes
sense to use a variable for the filename - rather than entering the filename literally:

Filename$="Mydata.dat"

If the file does exist, then the File Exist() function will return 1 (true) and if it doesn't exist will return 0 (false).
So, in our example, the code between the If and Endif lines will only be carried out if the file does exist.

I put 'Do Something About It' in the above example because you have two options at this point. As you cannot
open a file to save if it already exists, it HAS to be deleted so you can re-create a new one. But, what if the file
is there and contains data which you don't want to lose?

Well we'll cover that later, but for now, we'll assume that it can just be deleted. So, we use Delete File:

If File Exist(Filename$)=1
Delete File Filename$
Endif

which can be shortened to:

If File Exist(Filename$) Then Delete File Filename$

Here, if you don't say =1, then it is 'implied' - in other words, DB assumes you are testing for true (=1). Also, as
you only have a single action to carry out - not multiple lines of code, you can add the keyword THEN and
include the action on the end of the IF line.

So, having checked for the existence of the file, deleted it if it was found and opened a new file for writing, we
now have to write our data to disk.

Normally, this data would be variables. If you were writing say a matrix editor then all of the matrix data the
user has created or altered would be in variables like MatrixWidth, MatrixHeight, TilesX and TilesZ etc. All we
need to do is write all these relevant variables to disk.

Once the file has been opened, there are a number of commands to write different types of data. These
include WRITE BYTE, WRITE FLOAT, WRITE FILE and WRITE LONG - each of which writes data in an encrypted
format.
When I say encrypted I simply mean that you can't read the data with anything other than DB's respective
READ command. Use WRITE FLOAT and you can only access the data with DB's READ FLOAT - you can't open it
with say Windows Notepad and examine the contents. I am also reliably told that the formats DB uses cannot
be loaded into other programming languages like VB either.

47
There is a way around this though, by using WRITE STRING for everything. As mentioned earlier, when you are
learning DB, then I think it's important that you are able to write some data to a file then open it in Notepad
and see if it contains what you actually thought you were writing.

The fact that all your output is strings is irrelevant - the same data is still stored and you are still learning how
to save data to disk.

So, let's see some WRITE STRING examples:

Write String 1,"This is a sample text string!"

A$="This is a sample text string!"


Write String 1,A$

OK, these both do the same thing. The first example writes the literal string enclosed in the quotes to disk, (but
not the actual quotes). You could use this method for the very first line of your file to write a header
description of the file so if anyone opened the file to look at it, they would see what the file was for. For
example, to identify them, MatEdit's MA0 matrix files all have the following first line:

MatEdit .MA0 File

Lines can be ignored by your loading routine, so you can create as big a header as you like.

The second example is what you use to write string variables. But, what if your variables are numeric - not
string?

That's not a problem, we just convert them to strings when we write them out. For example:

MatrixWidth=20000
MatrixHeight=20000
TilesX=70
TilesZ=70
FloatVar#=44.82

Write String 1,"This is the header"


Write String 1,Str$(MatrixWidth)
Write String 1,Str$(MatrixHeight)
Write String 1,Str$(TilesX)
Write String 1,Str$(TilesZ)
Write String 1,Str$(FloatVar#)

As you can see, the use of Str$() converts the numeric variables to strings before writing them. The original
variables are not altered in any way by this process. As you can see, the process also works with float (real)
numbers too. If you opened the above resulting file with Notepad you would see:

This is the header


20000
20000
70
70
44.82

Having written our data out, we need to close the file. This is done very simply with:

48
Close File Channel

...where Channel is the channel number used when opening the file.

The complete routine for our example would therefore be:

Filename$="Mydata.dat"
If File Exist(Filename$) Then Delete File Filename$
MatrixWidth=20000
MatrixHeight=20000
TilesX=70
TilesZ=70
FloatVar#=44.82
Open To Write 1,Filename$
Write String 1,"This is the header"
Write String 1,Str$(MatrixWidth)
Write String 1,Str$(MatrixHeight)
Write String 1,Str$(TilesX)
Write String 1,Str$(TilesZ)
Write String 1,Str$(FloatVar#)
Close File 1

OK, that's written an example file, but what about reading the information back in?

OPEN TO READ

This process is very similar to writing files but using Read instead of Write. It's probably easier to show you the
complete routine for reading the file generated by the above example code then discussing it afterwards:

Filename$="Mydata.dat"
If File Exist(Filename$)
Open To Read 1,Filename$
Read String 1,T$: Rem Ignore This Info
Read String 1,T$: MatrixWidth=Val(T$)
Read String 1,T$: MatrixHeight=Val(T$)
Read String 1,T$: TilesX=Val(T$)
Read String 1,T$: TilesZ=Val(T$)
Read String 1,T$: FloatVar#=Val(T$)
Close File 1
Endif

OK, first of all, we check for the existence of the file we are trying to load. To avoid errors we only open the file
if it's there. If it isn't then we don't attempt to open it. That's why all the reading code is enclosed inside the If
File Exist(Filename$) loop.

If the file does exist then we use OPEN TO READ along with READ STRING to get the data. As we know that all
the data in the file is of type string, we can use the same string variable (T$) to read each data item in and then
convert it where necessary.

There's no way to detect automatically what type of data is in a file, but as you are reading the same data that
you wrote out, you already know what each string you read in has to be converted to - if it isn't actually a
string. You just have to make sure that you load data strictly in the same order that you wrote it out or nothing
will work!

The first of our data items is a text header. As this is unwanted information, we can ignore it once it is loaded,
though it MUST be loaded as it's part of the file. Data files are sequential so in order to read say the third item

49
in the file, the first two must be loaded first. So the rule is load EVERYTHING and ignore what you don't want!

The next item of our example is MatrixWidth which is numeric, so once the string version of the value has
been loaded into the variable T$ we need to convert it to a numeric value with VAL().

After it is read in, T$ will equal "20000" so MatrixWidth=Val(T$) will convert T$ to the number 20000 and place
that value into the numeric variable MatrixWidth.

The process is repeated re-using T$ for the remaining numeric variables in the file.

The last data item is a float. Val() doesn't mind, it will still convert the string "44.82" to the numeric value 44.82
as long as you use a float type variable to receive it. FloatVar#=Val(T$) will result in FloatVar# containing 44.82
which is what we want. However if you miss off the # symbol then FloatVar=Val(T$) will result in FloatVar
equalling 44 because without the # it is an integer variable and you will lose the .82 off the end!

Finally the file is closed.

Saving Arrays:

There is a command in DB for saving arrays, but you cannot save more than one array in the same file as the
command has to be supplied with the filename. Using the method we will discuss next allows you to save all
the arrays from your program that you want - all in the same file. This is essential if you want to create your
own file format.

Arrays are no more than simple variables in blocks. Each variable in the array can be accessed by using the
array's index number and if you can access a variable, you can save it out to disk. Here's a useful example...

Hiscore Tables

Creating a hiscore table in your program is easy enough, but if it doesn't write the data to disk, the next time
the program is run, all the hiscores are lost.

So, let's assume that our game has a hiscore table which holds the top 10 hiscores and the names of the
players who scored them. For this we need two very simple arrays - Hiscore() and PlayerName$(). Hiscore() is
an integer array as the hiscores will be numeric and PlayerName$() is naturally a string array.

These are created with:

Dim Hiscore(10)
Dim PlayerName$(10)

For these tutorials, once again I am purposely ignoring the fact that element 0 exists in an array as it makes life
easier - we can refer to players/hiscores 1 to 10 rather than 0 to 9. The file on disk will be called HISCORE.DAT.

So, when your game runs it checks to see if the file HISCORE.DAT exists. If it's the very first time it has been
run, then the file will not exist so it must be created and the arrays written out to disk. At this time they will
obviously all be empty or contain 0 (zero).

At this point, the arrays written to disk are the same as in memory. The player plays the game and if their score
gets on the hiscore table, the arrays are modified. Obviously the first time the game is played, ANY score will
get onto the table so they enter their name and the data is stored in the two arrays.

50
When the game is exited, the existing file HISCORE.DAT is deleted (we already have a later version in memory)
and the new contents of the two arrays written out to the file HISCORE.DAT.

The next time the game is run and it checks to see if the file HISCORE.DAT exists, it will be there, so instead of
creating a new one, the old hiscore table is read in. Once in memory, our two arrays can be modified when a
new hiscore is attained and on exit the hiscore table is just written out again - regardless of whether or not it
has changed since last time.

Writing arrays are very simple. All we have to do is write the data in a loop which matches the size of the array.
For Next loops are ideal for this. So, to write our array Hiscore() to disk with 10 elements, we would use:

For N=1 To 10
Write String 1,Str$(Hiscore(N))
Next N

As you can see, Str$() is used as before to convert the numeric array data to string when writing it out to disk.

Reading the array back in is also just as simple:

For N=1 To 10
Read String 1,T$: Hiscore(N)=Val(T$)
Next N

When writing string arrays, there is no need to convert the data, so we skip the Str$() section and just use:

For N=1 To 10
Write String 1,PlayerName$(N)
Next N

Reading the string array back in is done with:

For N=1 To 10
Read String 1,T$: PlayerName$(N)=T$
Next N

Saving Multi-Dimensioned Arrays:

If the array you want to save is a multi-dimensioned array, then the process is identical - we just alter the loop
accordingly. To save a numeric integer array which was created with DIM MultiArray(10,5) we would use:

For Ny=1 To 5
For Nx=1 To 10
Write String 1,Str$(MultiArray(Nx,Ny))
Next Nx
Next Ny

Here, this nested loop will use Nx to write the 10 Nx array values for every Ny value in the Ny loop. So, the
contents of MultiArray() will be written using Nx from 1 to 10 with Ny=1, followed by Nx from 1 to 10 with
Ny=2 and so on until Ny=5.

51
Reading back in is the same as with single dimensioned arrays, but using exactly the same nested loop.

For Ny=1 To 5
For Nx=1 To 10
Read String 1,T$: MultiArray(Nx,Ny)=Val(T$)
Next Nx
Next Ny

OK, that's how data in arrays is saved to disk and read back in again. Once again, I will stress that it's very, very
important that you read in the information in EXACTLY the same order that it was written out. Failure to do
this can cause problems - especially when you realise that it is possible for the data you are reading in to be
fed into the wrong variables. Your program will often not error during the load process in cases like this as the
routine will load any data into any variables so long as the variable types match - they just won't work properly
and the problem could be very difficult to trace.

So back to our hiscore example...

What we have to do now is place a small routine at the beginning which checks for the hiscore data file,
creates it if it doesn't and reads it in if it does:

Dim Hiscore(10)
Dim PlayerName$(10)
Filename$="HISCORE.DAT"

If File Exist(Filename$)
Open To Read 1,Filename$
For N=1 To 10
Read String 1,T$: PlayerName$(N)=T$
Read String 1,T$: Hiscore(N)=Val(T$)
Next N
Close File 1
Else
Open To Write 1,Filename$
For N=1 To 10
Write String 1,Str$(Hiscore(N))
Write String 1,PlayerName$(N)
Next N
Close File 1
Endif

In your game, you write the code which checks the players score at the end of each game and if it's higher than
the lowest score in the hiscore table, ask for the players name, inserts the name and score into the two arrays
- pushing the bottom entry off the list.

On exiting the program, we know that the file definitely exists so we just delete it and create a new file
containing the contents of the hiscore arrays currently in memory - ready for being read in the next time the
program is run.

Delete File FileName$


Open To Write 1,Filename$
For N=1 To 10
Write String 1,Str$(Hiscore(N))
Write String 1,PlayerName$(N)
Next N
Close File 1

52
File Formats

As you have seen, you can write many different types of variables while a file is open for writing, so when
there is a lot of data to be written it's worth planning what order to write the data.

The structure of your data file is called a 'File Format' and all files created with Windows applications have one.
There's a bitmap file format, a Microsoft Word file format and so on.

The file format defines for other users the layout of your file and what information can be found where, so
they can add routines to their programs giving them the ability to load files created by your programs.

For example in a graphics file format one part of the file is the header, one is reserved for the colour palette
and another part of the file will be the data which makes up the picture. You decide where the data goes in
your own file format.

There are no fixed rules for designing a file format, just write the data out sensibly and logically. MatEdit for
example creates a .MDF file with the Build option. If you were to look at an MDF file you would just see
numbers - lots of them. Publishing the file format simply describes to others what these number are, what
variable types they are and so on.

As a rule of thumb, you should have a description of the file type at the start saying what the file is used with.
The numeric and string variables should come next and finally all the array data. Try not to have too much
unwanted information like comments scattered about the file as it complicates the load routine - you still have
to load all the useless information even though you are immediately going to discard it.

Loading Routines

If you write a program which creates a data file usable by other people you will also need to create a loading
routine in DB which is supplied with your program. This will normally be a function (or collection of functions)
which users can #Include in their programs so they can call the functions when required.

If you write a matrix editor or world editor then you want people to be able to use the creations made with
your program in their own DB programs. If you don't provide them with a simple way to do this, then they are
not going to want to use your program.

Reading Other Files

Open To Read isn't just restricted to reading files you created yourself with Open To Write. It can also be used
to read information in from other files too. As long as you know the file format, you can read data in from
graphics and text files.

One of the easiest files to read in are plain ASCII text files created with a text editor as each line is going to be a
string.
However there is a limit of 255 characters with DB's strings so if the text file you are reading in has a line
greater than 255 then the reading will end abruptly with an error. We'll ignore this point for the moment
though and return to it later...

Also, another question is 'how much data do we read in'? As we didn't create the file, we have no idea how
long the file is!

53
FILE END()

Luckily, DB gives us a function called FILE END() which uses the syntax:

File End(Channel)

...where Channel is the same as the channel used with Open To Read. This will return true (1) if the end of the
file has been reached or false (0) if there is still more data to be read in.

Using this function in a loop, we can read all of the data in the file without having to know how much is there
first. The data from a string-type file like this is usually done with a string array. You just need to dimension the
array with a large enough number of subscripts before reading in the file or an error will occur while reading.
Let's see an example:

Dim TextLines$(5000)
Filename$ ="DOCUMENT.TXT"

If File Exist(Filename$)
Open To Read 1,Filename$
LineCount=0
Repeat
Inc LineCount
Read String 1,T$: TextLines$(LineCount)=T$
Until File End(1)=1
Close File 1
Endif

This example creates a string array with 5000 elements and is thus able to read up to 5000 lines from a text
file. The filename is set to DOCUMENT.TXT and we use our usual method of placing the loading code inside an
If...Endif which checks to see if the named file exists.

The important part of this example is that we are not using a For...Next loop any longer as we don't know how
many lines there are in the file - and we therefore don't have any start and end values for this kind of loop.
Instead we use a Repeat...Until loop which uses File End() to check if the end of the text file has been reached.

The Read String line reads each piece of data into T$ and it is then placed into the string array using the
numeric counting variable LineCount. If anyone is wondering why I use:

Read String 1,T$: TextLines$(LineCount)=T$

rather than

Read String 1,TextLines$(LineCount)

it's because I have encountered problems in the past when reading array values directly. Since using a normal
string variable to read the data and then transferring the contents to an array I haven't encountered those
errors. Feel free to use whichever method you like - the end result should be the same...
As the variable we use for counting in the loop would normally be the For...Next counting variable - which
obviously is not available here - we have to increment LineCount manually each time around the loop. This is
done with Inc LineCount and the line LineCount=0 is used before entering the loop - to ensure that the
counting loop starts at 0 (in case the routine is used more than once).

This loop continues reading lines of text from the text file until there is no more lines to read and then drops

54
out of the loop. At this point, LineCount is equal to the number of lines read in from the text file. Knowing this,
we can add a For...Next loop to the end of the program which will print the lines read in to the screen:

Print LineCount;" lines read in."


Print
For N=1 To LineCount
Print TextLines$(N)
Next N

And that's all there is to reading a text file.

Line Too Long?

Going back to earlier in this tutorial, I briefly mentioned that DB will error if you try to read in a string which is
longer than 255 characters. So, what do you do if this happens?

Well basically, you switch to reading the line in a character at a time rather than a line at a time. This is quite a
bit slower than reading in a line, but as it's the only way around the problem it's better than nothing.

For MatEdit Pro's in-built help files, I needed a text file of the MatEdit documentation, but with each line short
enough to fit on the screen. The problem was that the existing docs contained quite large paragraphs and
when exported as a text file, each paragraph became one single line - most of which were a lot larger than 255
characters in length!

Below is the small program I wrote to solve the problem. What it does is read data in from the file a byte
(character) at a time in a loop until it is a given length, (or it reads in the two bytes 13 and 10 - the two values
which record the end of a line in all text files), at which point a new line is started.

The two variables EndLineTrigger and ContainsWords are worth mentioning. When ContainsWords is set to 1
then the file being read in is deemed to be a document containing words and when set to 0, just data.

EndLineTrigger is the length of the required lines after reading in the data and what it does depends on what
ContainsWords is set to. If ContainsWords is set to 0 and EndLineTrigger is set to 80 then each line is cut off at
the 80th character.

If ContainsWords is set to 1 and EndLineTrigger is set to 80 then the line is cut off at the end of whatever word
is at position 80.

When all the lines have been read in and shortened, they are written out to another file. Here's the program:

55
Set Display Mode 800,600,16
Dim Lines$(10000)
InputFile$="test.txt": Rem Name of text file with lines > 255 characters
OutputFile$="cutoff.txt": Rem Name of resulting file after lines have been
shortened
LineNum=1: EndLineTrigger=100: ContainsWords=1

Print "PLease Wait - Reading File And Truncating Lines..."


Open To Read 1,InputFile$
Repeat
READ BYTE 1,ChNum
If ChNum>=32 or ChNum=9
Lines$(LineNum)=Lines$(LineNum)+Chr$(ChNum)
Inc CharCount
If CharCount=EndLineTrigger: Rem Point at which to seek EOL
If ContainsWords=0
Rem Reading text file containing data which can be split anywhere
CharCount=0
Inc LineNum
Else
Rem Reading text file containing words which should not be split
Repeat
READ BYTE 1,ChNum
If ChNum>32
Lines$(LineNum)=Lines$(LineNum)+Chr$(ChNum)
Endif
Until ChNum<=32 or FILE END(1)=1: Rem Until we hit a space or EOL
(Chr$ 13/10)
CharCount=0
Inc LineNum
Endif
Endif
Endif
If ChNum=13: Rem EOL Reached
READ BYTE 1,Dummybyte: Rem Read in the unwanted following Chr$(10)
CharCount=0
Inc LineNum
Endif
Until FILE END(1)=1
Close File 1

CLS
Print "Writing Out New File..."
Rem Write out converted file
Open To Write 1,OutputFile$
For N=1 To LineNum
Write String 1,Lines$(N)
Next N
Close File 1

CLS
Print "Written new text file containing ";LineNum;" lines!"
End

Use this program on any text file which you can't read with the Read String method. This will convert the file
and give you a new file which can be loaded with the Read String method. The two filenames at the beginning
of the program allow you to set the input and output filenames. OK, that's it for the File Access tutorial. If you
think there's some aspect of File Access you think I've missed and would like to see covered then let me know.

56
Choosing The Correct Variables

Looking at the code posted by many users on the forums, it's clear that many of you are not sure about the
difference between float and integer variables - or when you use them.

Back when I started programming, (prior to CPU's having Maths Co-Processors if anyone else remembers
them), computers were nowhere near as fast as they are now and we had to use every trick in the book to
squeeze as much speed out of our programs as possible. This included using integer variables rather than
floating point at every opportunity.

This was because it took a lot longer for a computer to do a floating point calculation than an integer
calculation.

These days, this is still true, but as computers are so fast, it's not as critical any more. Even so, it's still pointless
using floating point variables with numbers which can only ever be integers!

More to the point, using the wrong variable type can lead to errors in calculations and difficult to trace
problems in your programs later on.

Variable Types:

The three basic variable types common to both versions of DB are String, Numeric Integer and Numeric Float
(also called Reals).

String variables have the $ symbol (dollar) on the end of their names - like MyString$.
Integer variables have nothing on the end of their names - like Score.
Float variables have a # symbol (hash) on the end of their names - like FireAngle#.

(DBPro has a number of other useful variable types, but are not included in this text so the information given
here applies to both versions of DB).

Choosing The Type Of Variable To Use:

What variable you use depends on what sort of information you need to store. If your variable is only ever
going to contain whole numbers then you should use integer variables. If you are going to need the ability to
calculate and store fractions then you should use floats.

Choosing the wrong one can cause problems. For example, run the following example in DB:

A=5
B=A/2
Print B
Wait Key

The answer is of course 2.5 but the program prints 2. This is because A and B are integer variables and
regardless of the calculation, the result becomes an integer when it's placed into an integer variable. So, you
lose the '.5' off the end.

57
To store a floating point number, you have to use a floating point variable - but there are pitfalls to be aware
of even then. Take the above example amended so that B is now a floating point variable:

A=5
B#=A/2
Print B#
Wait Key

Note that when you run it, you still get the answer 2 instead of 2.5!

This is because in the line B#=A/2 both A and 2 are both integers so the result is an integer. Even though the
variable B# is a float, the result is still turned into an integer when it's stored. This is called variable
typecasting.

So what do you do if you have an integer and need to turn it into a float?

Well, you simply make sure that the parameters in the formula you use are not all integers. For example, run
the following snippet:

A=5
B#=A/2.0
Print B#
Wait Key

Notice that as we have changed the divisor from 2 (integer) to 2.0 (float), when you run it, the answer is now
correct - 2.5!

When To Use What:

As mentioned previously, integers are faster than floats so are preferable to use when we can - we just need to
know when to use the correct type.

Working in 2D with the screen is a good example of where you should only use integers.

The screen is usually something like 800x600 or 1024x768 and the values correspond to the number of pixels
(screen dots) running across and down the screen.

In 800x600 mode the 800 pixels running across the screen start at number 0 on the left and end at 799 on the
right. Running down the screen 0 is at the top and 599 is at the bottom.

Placing something on the screen requires the X and Y position values as in:

Paste Image 1,100,100

which places the image 100 pixels across and 100 pixels down the screen.

The important thing to realise here is that a pixel co-ordinate is a whole number like 100, 250 or 399. It can
NEVER be a floating point number like 100.5 or 250.77 because you can't position anything on the screen
between two pixels.
So, any variables in any way related to 2D screen positions or moving screen objects around should be integer
variables.

58
Another area where you frequently see float variables misused is with the mouse.

The mouse can only ever return whole numbers, so store the results in integers - not floats. There's nothing
stopping you from using those integer variables later in calculations which result in float values if you need to -
just remember the bit above about how to force calculations involving integers to return float values.

In 3D however, we aren't talking about pixels any more, but 'units' - because they are not a fixed size like
pixels. It's not easy to get your head around the concept at first, but 3D units are relative. 1 unit doesn't equal
a centimetre, a metre, an inch or a mile - it depends on the size of the objects in the scene to convey 'size'.

For example, think of two matrices - one of them 100000x100000 units and the other 100x100 units in size. If
all the objects are scaled down on the smaller matrix, it can look the same size to the camera as the larger one
- despite the hugely differing unit sizes of the matrices. A tree 20 units high would look enormous on the
smaller matrix but miniscule on the larger one.

So, the size of your objects defines how big a 3D unit appears and it has nothing to do with the actual number
of 3D units.

What's more, they are not whole units - you can have fractional parts of a unit. Objects can be placed at
locations like 100.2, 10.3, 100.7 and moved 0.1 units in any direction.

As such, you would use float variables to store these values.

So, in summary, only use float variables when you have to and your programs will be faster - and you'll avoid
many problems when your programs grow bigger.

59
Dark Basic Functions

Whereas a subroutine, (often called a 'procedure'), starts with a label and ends with a RETURN and is called
using GOSUB, functions are a somewhat different beast. Although similar to look at code-wise as subroutines,
functions in Dark Basic have three main differences:

1. Local Variables

All variables in a function are local as opposed to global. In programming terms global means that they are
visible to all of your program, whereas local means that they are only visible inside a function they are used in.
The visibility of variables is called the variable's 'scope'.

In Dark Basic Classic however, proper global variables don't really exist officially as they cannot be declared,
(unless you are using an IDE which supports them). DBPro does have global variables.

A normal variable in your main program can be seen inside a procedure, but not inside a function. So, although
I'll refer to them as global variables, DBC users should think of them as being 'semi-global'.

Another thing to remember about local variables is that they can co-exist at the same time as other variables
with the same name.

This is very important to remember as not knowing this can lead to some very hard to find bugs in your
programs.

For example, if you set the variable A to equal 10 in your main program with A=10, then call a function in
which you say A=20, when you exit the function what will A equal?

If you said 20 then you got it wrong! The answer is 10 because inside the function, the variable A would be a
newly created local variable called A - entirely separate from the previously created variable A outside the
function. When you exit from the function, you revert back to the original variable A, which is of course is still
equal to 10.

This feature can be very useful, but if you are used to programming in other languages you can be lured into a
trap, so beware.

Variables in BASIC do not need to be declared at the start of your program like in Delphi or C - they exist from
the moment you refer to them. For example, if you write a DB program with just one line which says PRINT
MYVAR then DB will quite happily initialise the variable containing the value 0 and print 0 on the screen. You
don't have to tell DB beforehand that you are going to use the variable MYVAR in your program and that it is
going to be an integer variable.

In other languages, local variables are 'destructive', which means that when you exit a function, any local
variables are destroyed.

The next time you visit the function, the variables are created again as entirely new entities. However, in Dark
Basic local variables in functions are 'non-destructive' which means that when you return to a function, the
variables still exist and contain whatever values they did when you were last there. This 'feature' can be very
useful if you know it's there, but an annoying source of difficult to trace bugs if you don't!

60
2. Entry & Exit Parameters

Functions can be used to do a specific task without any external information and then exit without returning
any information. If no entry parameters are required, empty parenthesis can be used when calling the function
and in the function header - or they can be omitted entirely. DB will accept either format.

Alternatively, functions can do a task calculated on the information supplied and then return information. One
example of a return value would be a success value. A 1 returned would denote that the function's task was
completed successfully whereas a 0 could denote that something went wrong.

As described above, all variables in a function are local, so you need a method to pass information from your
main program to the function and this is done by calling the function with the required variables in a
'parameter list' which is enclosed in parenthesis '()'. The function itself must have the exact same parameter
list to accept the same variables. So, you would have functions something like these:

No Entry Or Exit Parameters:

Function StartScreen
CLS RGB(100,0,100)
Ink RGB(255,255,255),0
Print "Screen Now Cleared And Ready For Use!"
EndFunction

Using Entry And Exit Parameters:

Function MyFunction(Var1,Var2,Var3,StringVar$)
SLen=Len(StringVar$)
RetVal=Var1*Var2*Var3+SLen
EndFunction RetVal

It's fairly obvious here that the function called MyFunction() is passed a parameter list of four variables - the
first three being integer numbers and the fourth a string. These variable names are used inside the function as
local variables to calculate the value of RetVal. RetVal is then returned back to the calling function. The actual
calculation is of course nonsense, but demonstrates how things work.

So, how are functions called? Well, it depends on whether you need to pass parameters or not and whether
your function returns anything. The first example above neither requires or returns anything so it is called
with:

StartScreen()

The second function needs three integer variables and a string so they are passed in the parameter list. The
actual variable names used in the call need NOT be the same as used in the parameter list of the function
header as the variable's contents are placed into local variables when they get there. Think of it as passing the
contents of each variable to the function - not the variables themselves.

The function also returns the contents of the variable RetVal, so we must take that into account when calling
the function. This is done by using a variable of the same type in the call. As our example function
MyFunction() returns an integer, we use the following call:

ValueBack=MyFunction(MinLen,MaxLen,Score,"Elephant")

61
After calling the function, the variable ValueBack will contain the returned contents of RetVal from the
function. The value returned can be an integer number, a real number or a string, but the subtle point is that in
Dark Basic, functions can only return a single value which can be a bit limiting.

The answer in many programming languages is the declaration of true global variables which can also be
accessed within functions.

Sadly, you don't have the ability to do this in Dark Basic Classic, though as it happens, the work-around is on a
similar theme - using arrays. Remember though, using arrays is only a work-around and there are limitations.
You can't pass arrays to functions or return them.

It was discovered a long time ago that in DB Classic, arrays behave like proper global variables and an array
declared at the start of a program can be seen, used and modified inside a function and is still intact upon
exiting the function. Arrays declared inside a function remain local though and not accessible outside it. So
using arrays, a function can return multiple values - or the equivalent anyway.

3. #Include Files

Functions can be used in Include files - something widely misunderstood by the newcomer to programming
with Dark Basic.

In a nutshell, the idea is that they prevent you from having to write certain sections of often used code over
and over again each time you write a new program. A good example is the keyboard/mouse control code
which you use for controlling the game character or ship in your games. If all the actions for moving and firing
etc. are all done with functions, then they can all be saved in a single file called, let's say Control.dba for
example.

The next time you write a program which uses the same control method, all you have to do is put the
Control.dba file into the same folder as your new program and use the following line as the first line of your
program:

#Include "Control.dba"

When the program is run, all the functions in the #Include file become available - without you having to re-
code them again. A big time saver - especially if you have functions to cover all possible control input methods.
You'd never have to write a piece of input code again! You must remember however, that Include files can
ONLY contain lists of function and no code can reside outside of each function header and its associated
EndFunction. To the best of my knowledge, the REM statement is the only exception allowed.

Why Use Functions?

Well speed-wise, there is little or no difference between using a procedure and a function, so the main reason
for using them would be for the ability to use local variables or the future grouping together of them in a
#include file. (In #Include files you can ONLY have functions - nothing else). In fact in some cases, a procedure
is better due to the lack of the local variable problem. Some users will swear by using functions for everything,
but my advice is to go with whichever you are happiest with.

There are many more tutorials and example code snippets on TGPF - my game programming forums. Click on
the link below - it's free to join and everyone's welcome!

62
Everything you wanted to know about strings

You wouldn't think so at first glance, but strings are quite important in every DB game you will write. After all,
what good is a game which doesn't put messages onto the screen, let you talk to other characters or let you
type your name into a highscore table. They all use strings!

In DB, all strings are enclosed in double quotes ("") and are stored in string variables which are defined by
having a dollar sign ($) on the end of them - as in the variable PlayerName$.

Strings can be cut up, joined together (concatenation), searched, jumbled up and have sections in them
replaced with something else.

In this tutorial we'll be taking a look how to do all of these things. But first, a bit of background info...

In DB, strings have a maximum length of 255 characters. Add one more character on the end and you'll get a
String Overflow error and DB will die.

A character can be any alphanumeric symbol your computer can produce - either printable or not. A very
simple example would be:

A$="My Name Is Fred"

When you put this in your program, DB will take everything inside the quotes and place them inside the string
variable A$. The actual quotes are not stored - they are just markers so DB knows where the string starts and
ends.

The section in quotes is called a 'string literal' as opposed to A$ which is a string variable.

You can add strings together:

A$ = "ABC"
B$ = "DEF"
C$ = A$+B$
Print C$

This will print "ABCDEF" to the screen.

You can type the sentence 'My Name Is Fred' as it's an English sentence, however there may be times that you
want to create a string of non-printable characters that aren't available on your keyboard.
There are a number of string-based commands in DB, which we'll cover later. One of them is CHR$() which
we'll look at now...

Take our above example 'My Name Is Fred'. The first letter is a capital M. This, like the rest of the alphabet is
an ASCII character and as such has an ASCII code.

The ASCII code for A is 65, B is 66, C is 67 and so on. As it happens, the code for M is 77 and if you tell DB to
Print CHR$(77), then it will print an M on the screen.

Armed with a list of ASCII codes you could build up a string with CHR$():

63
A$ = CHR$(77)+CHR$(121)+CHR$(32)+CHR$(78)+CHR$(97)+CHR$(109)+CHR$(101)

All these CHR$()'s place 'My Name' into A$ and in this instance is a pointless exercise. However, as mentioned
a moment ago, you might want a string built up of characters that you can't type in. In which case you could
use this method:

A$=Chr$(240)+Chr$(241)+Chr$(242)+Chr$(243)+Chr$(244)
Print A$

Each character in a string takes up one byte of memory and as a byte can store 256 numbers (0-255), a
standard ASCII character set only has room for 256 characters.

To interrogate a character and find out what it's ASCII code is, you can use the ASC() function:

ASC(String) or ASC(String Variable) will return the ASCII code of a character so:

Print ASC("A")

...will print 65 on the screen.

ASC is meant to be used only on single characters as shown above, but you can actually use it with strings of
more than one character - though in these cases only the first character is tested. Eg;

Print ASC("Alan")
Print ASC("Andy")
Print ASC("Arthur")

will all print the number 65. However, this can still be useful for a rudimentary string sort - based on the first
character of each string.

The simple code below will sort strings, but it can't be done with normal string variables - they have to be in a
list using string arrays. Don't panic though - they are really easy when you get to know how they work...

String Arrays

A string like Character1Name$ is a single entity. In a program, what if you had 3 characters with names?

You would need Character1Name$, Character2Name$ and Character3Name$ to store them. You would also
need a separate line of code referring to them for every occasion in your program.

What if you had 100, 200 or even more characters? The answer is that normal strings would be impossible -
you would have to use string arrays.

Imagine an apartment block with 10 apartments. In reception on the wall there's a row of mailboxes
numbered 1 to 10 for each of the apartments.

The postman puts the mail for apartment 4 in box 4 and when the guy from apartment 4 comes down, he
simply opens mailbox 4 for his mail. If all 10 mailboxes are collectively called 'Mailbox$' and the letters inside
the boxes is our string data, then in effect we are describing a string array.

The postie putting letters into mailbox 4, in DB terms is doing:

64
Mailbox$(4) = Letters$

And when the guy from apartment 4 gets his mail:

Owner$ = Mailbox$(4)

In DB, we just have to say how many mailboxes we require before we can start using them. This is called
DIMensioning an array and with our mailbox example it would be:

DIM Mailbox$(9)

Wait a minute!... Nine? You said 10 mailboxes!...

Yes - that's correct I did. But, unlike our apartment block, DB has an apartment number 0. So for 10 actual
boxes, we only need to DIM 9 which gives us boxes 0-9 - 10 in total.

You can however use DIM Mailbox$(10) and ignore box 0 altogether if you find it easier. Just pretend that no-
one lives in that apartment!

So what's the point of all these boxes?...

Well, the number in parenthesis () is called an index and as always when programming, you can replace a
number with a variable. This means that instead of using Mailbox$(2) or Mailbox$(9), you can refer to
Mailbox$(X). In turn, that means that you can use them in loops...

Imagine printing the contents of 5 'mailboxes' the 'old' way:

Print Mailbox1$
Print Mailbox2$
Print Mailbox3$
Print Mailbox4$
Print Mailbox5$

Now again with 100 mailboxes!

No thanks... Try this instead:

For X = 1 To 100
Print Mailbox$(X)
Next X

Now you see the power of arrays!

Actually, what we've been talking about so far is a 'single-dimension array' - just one row of boxes and one
index number.

However you can also have 'multi-dimensioned arrays' which are best thought of as being like a wall of lockers
in a changing room. Say there is a stack of lockers ten wide and six high - sixty in total.

The lockers running across might be numbered 1 to 10 and the rows running down labelled A, B, C and so on to
F.

If your stuff is in locker 5D, you need to count across to 5, then count 4 down to D to get to it.

65
In DB, this translates to an array like Locker$(X,Y) where X is counting across and Y is counting down. Locker 5D
would be;

Locker$(5,4)

Anyway, back to the issue in hand - sorting...

Having our strings in an array means that we have an orderly way to present the new list after we've sorted it -
in a loop using a variable for the array index number. Let’s set up our example array:

Dim Names$(10)

Names$(1) = "Geoff"
Names$(2) = "Tim"
Names$(3) = "Alison"
Names$(4) = "Pete"
Names$(5) = "Chris"
Names$(6) = "Barrie"
Names$(7) = "Nigel"
Names$(8) = "Rosie"
Names$(9) = "Simon"
Names$(10) = "Kevin"

(For simplicity, I've chosen to ignore element 0 of the array).

OK, they aren't sorted at the moment. All we have to do is look at the ASCII codes of the first characters of
strings 1 and 2.

If the first string's ASCII code is greater than the second then it's higher in the alphabet and we need to swap
their positions. We now repeat the process with strings 2 and 3, then 3 and 4 and so on.

We use a 'DidWeSwap' flag and set it each time we have to do a swap when we run through the loop. If we run
through the loop and a swap wasn't done, the flag isn't set and we know the strings are now all in the correct
order. Now for the code (which is added to the end of the above snippet):

True=1: False=0
Repeat
SwappedString = False
For N=1 To 9
Str1 = ASC(Names$(N))
Str2 = ASC(Names$(N+1))
If Str1 > Str2
Temp$ = Names$(N)
Names$(N) = Names$(N+1)
Names$(N+1) = Temp$
SwappedString = True
Endif
Next N
Until SwappedString = False

For N=1 To 10
Print Names$(N)
Next N
Wait Key
As you can see, a complete run though the array is within the Repeat..Until loop. We reset the SwappedString
flag (to False) at the start of each run through this loop and check to see if it's been set at the end.

66
If it has been set (to True) then we've not finished so repeat the loop again. If it's still False then no swaps were
made during that pass and the list is sorted - so drop out of the loop.

The only other thing of note is the use of the string variable Temp$. This is used during the swap process
because we don't want to lose string 1 when we copy string 2 into it. We copy string 1 into Temp$, string 2 into
string 1 and then Temp$ into string 2.

The last For..Next loop prints out the 10 strings in the array and if you run the program you will see that all the
names are in sorted in alphabetical order.

This simple sort routine is called a bubble sort because strings in the list that are not in their correct positions
'bubble' to the top. Of all of the sorting methods, this one is probably the easiest to program, but least
efficient.

At this point let's introduce a few more DB string commands before we need them...

Left$(), Mid$(), Right$() and Len()

Left$(StringVar$,NumChars) will extract NumChars characters from StringVar$ starting at the left. So:

A$="ABCDEFGHIJK"
Print Left$(A$,5)

...will print 'ABCDE' on the screen - the leftmost 5 characters.

Right$(StringVar$,NumChars) will extract NumChars characters from StringVar$ starting at the right. So:

A$="ABCDEFGHIJK"
Print Right$(A$,5)

...will print 'GHIJK' on the screen - the rightmost 5 characters.

Mid$(StringVar$,CharPos) will extract the character at position Charpos in StringVar$. So

A$="ABCDEFGHIJK"
Print Mid$(A$,6)

...will print 'F' on the screen - the character at position 6.

Len(StringVar$) will return the length of StringVar$. So

A$="ABCDEFGHIJK"
Print Len(A$)

...will print '11' on the screen - the length of A$.

How about writing word puzzle games like anagrams?

Using the above commands we can take a string and jumble all the letters in it:

GameWord$ = "elephant"
ShowWord$ = ""
Randomize Timer()

67
WordLen = Len(GameWord$)
For N=1 To WordLen
RandChar = Rnd(Len(GameWord$)-1)+1
ShowWord$ = ShowWord$ + Mid$(GameWord$,RandChar)
GameWord$ = Left$(GameWord$,RandChar-1)+Right$(GameWord$,Len(GameWord$)-
RandChar)
Next N

Print ShowWord$
Wait Key

For this example, we set GameWord$ as the word to scramble, (though in a real situation, words could be
selected at random from a pre-loaded text file, or read from Data lines).

After randomizing the random number generator, we get the length of the word and store it in WordLen.

The main For..Next loop counts from 1 to the length of the word - 8 with the word elephant. The RandChar line
picks a random number which is always between 1 and the number of letters in GameWord$. It's done like this
because in the following lines, the length of GameWord$ will change.

After selecting the random number, the character at that position in GameWord$ is added to ShowWord$.

Finally, the chosen letter is removed from GameWord$ so it won't be chosen again.

The loop is repeated so, by the end of the loop, GameWord$ has been reduced from 8 characters to an empty
string and ShowWord$ has gone from an empty string to being 8 characters long - but a jumbled version of the
word.

You could then display the word and time how long it takes the user to enter the correct word - awarding
more points, the quicker it's done.

How about highscore tables then...

A highscore table also uses string array lists - usually two of them: one for the name and one for the score.

The only tricky part of creating a hiscore list is deciding where to insert an entry at the end of a game. So let's
cover the theory before tackling the code...

OK, we start with the name and score arrays both empty. When the game has ended and we have a final score,
we simply loop through the score array starting at the bottom comparing the player's score with the score in
the array.

If the array entry is smaller than the player's score then we continue round the loop.

If however the array entry is greater than the player's score - or if the hiscore table is empty and we reach the
top of the array list - then we have reached the required 'slot'. All we need to do is shuffle all the entries below
it down one in both arrays - with the last entries on both lists 'dropping off the end'.

All that is required then is to feed our new player information into the array at the calculated position.

68
Once a highscore table is created it's saved to disk and loaded when required.
So how is this done in DB?...

DIM HiScoreName$(10)
DIM HiScoreValue$(10)

PlayerScore = 3500
PlayerName$ = "TDK_Man"

Rem Fill Array With Existing Scores


For N=1 To 10
HiScoreName$(N) = "PlayerName"
HiScoreValue$(N) = Str$((11-N)*1000)
Next N

Rem *******************************************
N=11
Repeat
Dec N
ArrayScore = VAL(HiScoreValue$(N))
Until ArrayScore > PlayerScore or N=0
Rem N is now the slot above the one we actually want (which is N+1)
Rem so we shuffle all it and all below it down to make room
For I=10 To N+2 Step -1
HiScoreName$(I) = HiScoreName$(I-1)
HiScoreValue$(I) = HiScoreValue$(I-1)
Next I
Rem Slot N+1 is now free so fill it
HiScoreName$(N+1) = PlayerName$
HiScoreValue$(N+1) = Str$(PlayerScore)
Rem *******************************************

Rem Now print out the new list of 10 names


For N=1 To 10
Print Str$(N)+". "+HiScoreName$(N) + " - " + HiScoreValue$(N)
Next N

Wait Key

The important code is inside the Rem lines of stars.

The Repeat..Until loop starts at the bottom of the hiscore list reading the score values in the array until the
score found is greater than the score just made by the player - or the counting loop variable N = 0 (at which
point all the array has been checked).

If a higher number is found then the loop is exited with N containing the number of the array element of the
slot directly above the one we need to put our score info into. So, we need to move all the elements of the
array down one - including the one we need to fill.

So entry 10 gets replaced with entry 9, entry 9 gets replaced with entry 8 and so on until we reach the slot we
want.

If we want to use slot 4 for example, that slot will be N+1 so the last move in the For..Next loop will be moving
the contents of N+1 into N+2 - leaving slot N+1 (4) free.

Finally we place the players name and score into the respective arrays at position N+1 - and in your game,

69
write them to a file on disk.

You can alter the value on the PlayerScore = 3500 line before running the program to confirm that the correct
slot is always selected.

Armed with the commands we've seen in this tutorial, we can also do some pretty unusual stuff with strings.

To end this tutorial, here's a novel way to tackle a task in many games with enemies or characters which have
health points that can vary as you play. All you have to do is look at strings in a slightly different way...

As a string is simply a long joined list of numbers - just like a numeric array, you can treat them as such - you
just need to remember that being a byte, the maximum size for the numbers is 255.

The following example may not be the best way to do what it demonstrates, but it serves nicely as an example
of the process using strings - which you can adapt to other things.

So, say you have 10 enemies in your game and their health values vary from 100 (full health) to 0 (dead). At
the start of your program, you use:

EnemyHealth$="dddddddddd"

What!!?? I hear you say... Well, d just happens to have the value of 100 in the ASCII table and in the above line
there are 10 d's - one for each of the enemies.

So what do we do with it?

Well, our string currently contains 10 d's - each having the value of 100. The first 'd' belongs to enemy 1, the
second to enemy 2, the third to enemy 3 and so on.

Say you give enemy 3 a smack in the eye and his health is reduced by 3 points.

All we have to do is get the current ASCII value of the third character in our health string, reduce it by 3, turn it
back into a character and put it back into the string.

EnemyHealth$ = "dddddddddd"
EnemyNumber = 3
HitPoints = 3
EnemyHealthVal = ASC(Mid$(EnemyHealth$,EnemyNumber))
Before$ = Left$(EnemyHealth$,EnemyNumber-1)
After$ = Right$(EnemyHealth$,Len(EnemyHealth$)-EnemyNumber)
Dec EnemyHealthVal,HitPoints
EnemyHealth$ = Before$ + Chr$(EnemyHealthVal) + After$
Print EnemyHealth$
Wait Key

I've kept the coding as simple as possible so you can follow it and see exactly what it's doing.

The first three lines simply set up the variables.

The fourth line look more complicated than it actually is. All it does is use Mid$() to get the character in
EnemyHealth$ which corresponds to our enemy (number 3). When we have it, we convert it to a number using
ASC() - this gives us 100 and store it in EnemyHealthVal.

70
The fifth line grabs the first two characters of EnemyHealth$ into Before$. Remember, we are only interested
in the third character (enemy). If EnemyNumber is 3 then using EnemyNumber -1 with Left$ will grab the first
2 characters.

The next line needs to grab all of the characters after the third one and store them in After$. It does this by
using RIght$().

Len(EnemyHealth$) returns 10 (the length of the whole string) and deducting the number of our enemy (3)
gives us the number of characters to grab from the right using Right$(). In this case, 10-3 gives us 7 characters -
which we store in After$.

Next, we deduct HitPoints (3) from EnemyHealthVal (100) which gives us 97.

The last important line builds up the new EnemyHealth$ by adding together: Before$ (the first two characters),
the new health of enemy 3 which we convert back to a string with Chr$(EnemyHealthVal) and After$ (the
remaining seven characters).

This equates to:

"dd"+"a"+"ddddddd"

which you will see if you run the above snippet.

I'm not suggesting that this is either the best or fastest method to use for this particular task - I'm just using it
as an example of this particular way of using strings.

I use the same method to deal from a shuffled pack of cards in card games by creating four string variables -
one for each suit (H, C, D and S). For hearts, the string would be:

Hearts$ = "1H2H3H4H5H6H7H8H9HTHJHQHKH"

Each card is two characters - the second being the suit and the first being the numbers 1 to 9, T for ten, J for
Jack, Q for Queen and finally K for king. The other three suits are stored as:

Clubs$ = "1C2C3C4C5C6C7C8C9CTCJCQCKC"
Diamonds$ = "1D2D3D4D5D6D7D8D9DTDJDQDKD"
Spades$ = "1S2S3S4S5S6S7S8S9STSJSQSKS"

Next we join them together to form a pack:

Pack$ = Hearts$+Clubs$+Diamonds$+Spades$

The length of Pack$ is 104 characters long (52 cards of 2 characters each).

When we deal from the pack, we pick a random number based on the length of Pack$. Multiplying that
random number by 2 and adding 1 gives us the position in Pack$ of the two characters for that card. Eg:

With a new pack = (104/2)-1 = 51


Get random number between 0 and 51 - let's say the number 8 comes up (the ninth card).
(8*2)+1=17
The 17th character in Pack$ is 9 and the 18th character is H so the card dealt is 9H - the 9 of Hearts.

71
Once dealt, we use the method used above to remove the 17th and 18th characters from the pack so the card
can't be dealt again.

The next time a random number is required, the length of Pack$ has been reduced by 2 so it's now:

(102/2)-1 = 50 - which gets a random number between 0 and 50.

When all the pack has been dealt, restore Pack$ by adding the four suit strings together again and off you go...
Well that's it for this tutorial on strings. I hope it's covered all the topics you wanted it to.

72
Timer Tutorial

One subject you often see questions about on forums is that of timers. These may be for showing an on-screen
display of either time left or time elapsed, though timers have many other uses.

This tutorial will show the seasoned DB user nothing new, but is instead aimed at the new programmer and
looks at the way timers work and how they can be used in your programs. Feel free to copy any of the code in
this tutorial into DB and run it.

All PC's have built-in timers which place values into registers for programmers to access. DB has a function
called Timer() which accesses the computers timer register and returns values in one thousandths of a second
increments.

To convert these values to seconds, we simply have to divide them by 1000. If you need finer timings than one
second intervals, then you can divide by 100, 10 or not divide by anything at all to return 10th, 100th and
1000th of a second increments respectively.

The value can obviously be stored in a variable, so if for example, you use:

RetVal=Timer()

...then the current value of the PC's timer is stored in the variable RetVal.

This value can then be used for a multitude of tasks including calling procedures after a set amount of time (as
a way to make your programs run the same speed on all spec machines), on-screen clocks & timers or any
other timed events in your programs, (maybe it goes dark after playing for an hour, or a plane flies overhead
every 15 minutes).

You are also not restricted to a single timer either. You can have as many independent timers as you like in
your programs by using different variables. For example:

T1=Timer()
T2=Timer()
T3=Timer()

Will give you three timers which can be used for timing three separate events.

Copy and paste the following code into DB and run it:

Set Text Opaque


Do
Text 0,0,Str$(Timer())
Loop

The value you see is the contents of the timer register and it is continually changing, and fast! - even when
your program is not running! This value is not a lot of use, so modify the code as shown below.

Set Text Opaque


Do
Text 0,0,Str$(Timer()/1000)
Loop

73
Now the value changes, but ticks over at a more useful once per second. It's still not totally useful as it will
display a random value on every machine. To fix this, we need to grab this value into a variable and deduct it
from the value of every subsequent use of Timer(). The result is a second counter that starts at 0 (zero):

Set Text Opaque


T=Timer()
Do
Text 0,0,Str$((Timer()-T)/1000)
Loop

ELAPSED TIME

To create an 'elapsed time' display, the basic programming procedure is as follows:

1. Grab the current value of Timer() into a 'start time' variable


2. In a loop, read updated values of Timer() into a 'current time' variable
3. Subtract the 'start time' value from the 'current time' value
4. Divide the result by 1000 to give the number of seconds elapsed.

In DB, the code for a 30 second timer would look something like this:

Rem Simple 30 Second Clock


Set Text Opaque
Ink RGB(255,255,255),0
T=Timer()
Repeat
Elapsed=(Timer()-T)/1000
Text 0,0,Str$(Elapsed)+" "
Until Elapsed=30: Rem change this value to alter the length of the timer

In your own programs, 'T=Timer()' is placed just before entering your main program loop and the line
'Elapsed=(Timer()-T)/1000' is placed somewhere inside your main loop with an If Elapsed= clause immediately
following it:

Rem Continuous Seconds Counter


T=Timer()
Do: Rem Main Program Loop
Elapsed=(Timer()-T)/1000
If Elapsed=60
Inc MinutesPassed: Rem Or do whatever you need to do in your program
Elapsed=0
T=Timer()
Endif
Rem The rest of your main loop program here
Loop

Basically, this program counts the number of elapsed seconds in the variable 'Elapsed' and then checks to see
if that value equals 60 (1 minute). If it does, the timer goes back to zero and continues indefinitely.

When the timer hits 60 seconds, (or whatever value you set), then what your program does is up to you. In the
above example, the variable MinutesPassed is incremented - effectively counting the number of minutes
elapsed. The program could then be set do do something specific when MinutesPassed equals a specific
number of minutes.

Your program could just as easily call a function or subroutine when Elapsed reaches a given value.

74
Once the target value has been reached and the required task completed, you need to reset the timer. So, in
our example above, we set the variable Elapsed to equal zero.

The next part of the program is the bit that most new programmers trip up with:

Having reset the variable Elapsed to zero, the formula 'Elapsed=(Timer()-T)/1000' is still using the original start
value stored in 'T' and will therefore continue calculating the elapsed time from when the program was first
run.

So, the start value variable needs updating with a new start time. We do this by putting another T=Timer() line
inside the If 'Elapsed=' block of code. The value of Elapsed will then calculate the number of seconds elapsed
from this point instead of the old one - ie from zero again.

COUNTING TIME DOWN

Counting down is essentially the same as counting up, so if say you want to give the user of your program a set
amount of time to complete a task, then a slightly modified version of the first example is all that is required:

Rem Simple 30 Second Countdown Timer


Set Text Opaque
Ink RGB(255,255,255),0
Seconds=30: Rem change this value to alter the length of the timer
T=Timer()
Repeat
Elapsed=(Timer()-T)/1000
TimeLeft=Seconds-Elapsed
Text 0,0,Str$(TimeLeft)+" "
Until TimeLeft=0

The only differences in this example are the use of a variable called Seconds which contains the number of
seconds to count down and the line 'TimeLeft=Seconds-Elapsed' which subtracts the elapsed time from the
number of seconds in the level, placing the result in the variable 'TimeLeft'.

For example, when 10 seconds have elapsed, TimeLeft equals 30-10, or 20 seconds. When Timeleft gets to
zero then the example program ends.

A PROPER CLOCK DISPLAY

If you want a digital clock display, then it really is quite simple and only needs a single timer.

Rem Digital Clock Example


Set Text Opaque
T=Timer()
Do: Rem Main Program Loop
Seconds=(Timer()-T)/1000
If Seconds>=60
Inc Minutes
If Minutes>=60
Inc Hours
If Hours>=24
Hours=0
Endif
Minutes=0
Endif
Seconds=0

75
T=Timer()
Endif
Hrs$=Str$(Hours)
If Hours<10 Then Hrs$="0"+Hrs$
Min$=Str$(Minutes)
If Minutes<10 Then Min$="0"+Min$
Sec$=Str$(Seconds)
If Seconds<10 Then Sec$="0"+Sec$
Text 0,0,Hrs$+":"+Min$+":"+Sec$+" "
Loop

In this example, the timer is used simply to get the seconds elapsed. When the value of the variable 'seconds'
hits 60, it is reset to zero and the variable 'minutes' is incremented. In the same way, when 'minutes' hits 60, it
too is reset to zero and 'hours' is incremented. 'Hours' is reset to zero when it hits 24 (not 60) as there are 24
hours in the day.

The rest of the program converts the numeric variables to strings with STR$() and formats the strings with a
leading "0" if less than 10. The time is then printed to the screen.

If you have been able to follow the examples in this tutorial, you should have a good working knowledge of
how timers work and can go away and implement your own ideas using Timer().

76
Dark Basic Matrix Primer

Note: Dark Basic Pro has additional matrix options that DB Classic does not have. In order to keep this tutorial
compatible with both versions, these are not covered in this tutorial.

What Is a Matrix?

Not to be confused with a mathematical matrix, a matrix in DB is the floor or terrain in your programs and
although fairly simple to master, a couple of aspects are quite difficult for the novice to get to grips with. Apart
from that, when you do know what you are doing, they are painfully long-winded to create manually, (as you
will see later) - hence the popularity of matrix editors. Very few people type in all the Dark Basic commands to
create a matrix - it would take days! The actual matrix is a simple grid which you can think of as being similar to
a chessboard. Each square can be painted with a texture and each of its four corners raised or lowered to
create hills and valleys.

Dark Basic gives you the ability to create your 'chessboard' with any number of squares (tiles) across and down
that you like, as well as the physical width and depth that you want in pixels. A matrix in DB can have up to
10,000 polygons (triangles), so with each square tile consisting of two polygons, simple maths tells us that a
matrix cannot have more than 5,000 tiles. This means you can create a matrix 1 tile deep by 5,000 tiles wide,
or 10 deep and 500 wide, or 100 deep and 50 wide, or any combination up to 5,000 tiles maximum - you get
the idea. So, the biggest square matrix you can have in DB is 70x70 tiles which equal 4,900 tiles or 9,800
polygons - within our 10,000 limit. If the matrix was one tile bigger - 71x71, this would add up to 10,082
polygons and not be allowed - just in case you were wondering why DB has the strange number 70x70 as a
maximum!

Most matrices (the plural of matrix, not matrixes), are produced square - mainly because the maths is easier.
Plus, textures are also designed to look best on a square matrix tile, and a matrix 10 tiles wide and 5 tiles deep
but having the same pixel width and height would have rectangular tiles - not square.

To correct this, you need to calculate the pixel width in relation to the height. With a square tile-sized matrix,
no calculations are necessary as the pixel width and height is always the same.

If you want bigger landscapes than 70x70 tiles, you can create more than one matrix in your program and join
them together, though if you have too many on screen at the same time, DB will start to slow down on lower
spec machines. There are clever ways around this which we'll look at later.

77
The matrix is created with the command: MAKE MATRIX Mn,Pw,Pd,Tx,Tz where Mn is the matrix number, Pw
& Pd is the pixel width and depth and Tx & Tz are the tiles across and down. When you create a matrix you
tend to think of it most as being viewed from above in 2 dimensions - as shown in figure 1. The matrix is
actually 3 dimensional so the X axis runs from left to right and it's actually the Z axis that runs from bottom to
top - NOT the Y axis. Hence the variable Tz instead of Ty in the example above.

MAKE MATRIX 1,5000,5000,4,4

This will create the matrix in figure 1 and the bottom left corner is always placed at 0,0,0 in 3D space. You can
of course move it, but it isn't advised until you are more experienced. Looking at figure 1 again, you will see
that there are two sets of numbers - white and purple. The larger white numbers are the X and Z values you
use when referring to tiles when texturing. For this 4x4 tile matrix, the numbers along the X and Z axis run
from 0 to 3. In your DB code, your matrix tile width and height would be stored in variables such as TileWidth
and TileHeight, so when texturing, you would use a loop like FOR N=0 TO TileWidth-1. That way, should the
tiles across variable change, the loop will still texture all tiles.

The smaller purple numbers are used when altering the matrix height and as there are two points along both
axis for every matrix tile, there has to be an extra co-ordinate to handle this. As such, for a 4x4 tile matrix,
height co-ordinates run from 0 to 4 and the corresponding DB loop would be something like FOR N=0 TO
TileWidth (dropping the -1). When a matrix is first created each of these height values is set to zero. Positive
values raise the point and negative values lower it.

Let There Be Height

Raising the height of any part of a matrix means altering the value of one of the tile intersect points, (corners).
You cannot raise any other part of the matrix - the middle of a tile for example. Altering the height of one of
the tiles means altering one or more of the four associated tile corner points. Setting all four points of a single
tile to the same value will raise or lower the tile but keep it flat.

The DB command you use is SET MATRIX HEIGHT Mn, X, Z, H where Mn is the matrix number, X and Z are the
intersection co-ords (the smaller purple numbers in fig 1) and H is the required height of that point. So, If we
wanted to raise the tile highlighted with a red circle in fig 1, we need four commands - one for each of the four
corner points:

78
You will see that the 'red blob' tile's bottom left corner is 2 across (X Axis) and 1 up (Z Axis), so the command
for raising that corner to a height of 20.0 would be:

SET MATRIX HEIGHT 1, 2, 1, 20.0: Rem Bottom Left Tile Corner

The remaining three lines for the other three corner points would be:

SET MATRIX HEIGHT 1, 3, 1, 20.0: Rem Bottom Right


SET MATRIX HEIGHT 1, 3, 2, 20.0: Rem Top Right
SET MATRIX HEIGHT 1, 2, 2, 20.0: Rem Top Left

The height value should be a real (floating point) number, so you need to remember to put the .0 on the end.
In practice integers do work, but you might as well get into the habit of doing it correctly now so that if at a
later date a version of DB enforces these rules, you won't have to go back and alter all of your code.

So all that typing and all we have done is raised a single tile up a bit! Imagine the work involved in a 70x70 tile
matrix! How would you decide what height values to use? By now you should be starting to realise the value of
a matrix editor...
Another useful matrix height command to know about is RANDOMIZE MATRIX Mn, MaxHeight which will set
the height values of ALL intersection points of a matrix with a single command. The height used is a random
value between 0 and whatever you enter for the value MaxHeight.

IMPORTANT! After using any matrix command, it is important to remember to tell DB to refresh the matrix on
the screen. This is done with UPDATE MATRIX Mn where Mn as usual is the matrix number. Failure to do this
after altering anything on your matrix will result in no change on your screen!

A Splash Of Colour

All the work done so far will have been done on a wireframe matrix. To make the matrix a little more colourful
and realistic, you need to texture the tiles you have created. This is done with SET MATRIX TILE Mn, X, Z,
TextureNum and isn't helped by the confusing help files that come with DB that say "SET MATRIX TILE Matrix
Number, X, Z, Tile Number".

To be able to use this command, it is very important that you have told DB to prepare your texture image
ready for use with PREPARE MATRIX TEXTURE Mn, ImageVal, Across, Down - where ImageVal is the texture's
image number and Across/Down is the texture grid size. So let's split all of that down into more easily followed
sections:

79
A texture like the one above is a graphic image and can in theory be any size, but the smaller they are (and if
they are square), then the faster DB will run.

Example sizes can be 32x32, 64x64, 128x128 or 256x256, (sizes in pixels). 512x512 can be used, but some
graphics cards will start to struggle. Older Voodoo cards don't like anything over 256x256 or they throw a
wobbler. The most commonly used size is 128x128 as the smaller the texture, the worse the quality. Having
said that, if anyone remembers Equilibrium, that had an excellent quality matrix and that only used 32x32
textures! DB comes with a good selection of textures and there are thousands available on the web - including
a small collection on this web site.

OK so far. Now for the rules which cause the problems...

First of all, each matrix can only have one single texture image associated with it. What!!?? Only one texture?
No - only one texture IMAGE, though that image can have lots of individual textures in it. (see image below).
This is done by placing them in a grid just like the old chessboard pattern again. This is when the texture size
starts to get a little more important.

128x128 for individual textures is a happy medium between size and quality, but 16 textures of that size on a
4x4 grid in a texture image would be 512x512 pixels. Most new graphics cards would have the speed and
memory to handle this size of texture image, but remember a little earlier I said that the Voodoo graphics
cards are OK unless the texture image is greater than 256x256? You just have to bear in mind that nice, big,
high quality textures can prevent an awful lot of users from using your programs!
OK, so what can you do? Easy - just reduce the size of your textures. A 256x256 texture image can contain 64
textures of 32x32 on an 8x8 grid and you'd be surprised how good they can look. Go up to textures 64x64 in
size and you can get 16 textures on a 4x4 grid with double the quality. You can always go for the sod 'em
attitude and not even worry about whether others can use your programs or not!

Next problem - making your texture. Your texture needs to be square, as too does the grid of textures in it. You
need to know how many textures you need to fit into the image and make the grid big enough to fit them in -
but keeping the recommended grid sizes of 2x2, 4x4, 8x8 or 16x16. It doesn't matter if some slots on the grid
aren't used.

Note: These are recommended grid sizes, though I have also used others, like 3x3 and 6x6 and had no problems
whatsoever. I think that the important thing is that they are square grids - ie NOT 3x4 or 4x5. Use what you
like, but bear this in mind if you have texture glitches or problems.

For example, let's say you have 5 textures. A 2x2 grid will only hold 4 textures so you would use the next one
up - 4x4 and only use the first 5 slots. The same size texture image could be used for up to 16 textures before
having to move up to the next size. On this 4x4 grid image, each texture could be 64x64 and still fit in a
256x256 pixel image.

80
So, in summary, you need to decide on your texture size, which depends on the size you want your image to
be and what grid size to use - which in turn depends on how many textures you use. Then, you have to create
the image all using the correct sizes - either by hand in a paint program like Paintshop Pro, or via code in Dark
Basic. If you add more textures as they are required, you have to edit the image - after possibly recalculating
all the sizes!

All in all, it's not a straight forward process and once again it adds to the argument that you can't beat a matrix
editor. For example, MatEdit allows you to have up to 100 textures in the texture palette and when you use
the Build option, it does all the calculations and creates the texture image to the correct grid dimensions and
texture sizes to fit - but only includes the textures from the palette that you actually used and not those you
didn't!

OK, so let's say you now have your texture image with 4 seperate textures in a 2x2 grid. What next? Well, the
texturing process is reasonably straight forward from here on. The textures are usually stored in a BMP file and
we need to get them into an IMAGE, so we need to use LOAD IMAGE "ImageName", ImageNum. If we want to
use the 2x2 example texture image above (and it was called texture.bmp) we would use:
LOAD IMAGE "texture.bmp", 1

Note the image number is 1 - it's needed in the next stage. Now we have the texture in an image we can use:

PREPARE MATRIX TEXTURE 1, 1, 2, 2

The first 1 is the matrix number, the second is the image number we loaded texture.bmp into. The last two
numbers are the grid size X and Y for the textures in the image. Here we have 4 images on a 2x2 grid, so the
values are 2 and 2. Basically, this command tells DB to prepare the image for the matrix by cutting it up into
two images horizontally and two images vertically. The four resulting textures are numbered 1 to 4 and stored
in memory ready for when you need them. Obviously, if your texture image was an 8x8 grid of textures, you
would use the value 8 instead of 2.

Now the image has been prepared, we can now use DB's matrix commands to 'paint' the matrix.
If you want the whole matrix textured with the same texture, you can use FILL MATRIX Mn, Height, TileNum
which will also set all the tiles to a given height at the same time with a single command. TileNum will be a
number from 1 to x where x is the number of available textures in the texture image you loaded.

Note: Many new users don't realise that if you use PREPARE MATRIX TEXTURE with an image containing just
one single texture, then FILL MATRIX is automatically called by DB and the whole matrix will be textured -
without you asking for it! If like our example, you load a texture image which contains more than one texture,
PREPARE MATRIX TEXTURE does nothing to the actual matrix.

If you want to texture individual tiles on the matrix, the process is very similar to the previously used method
to alter the matrix heights - you just need to remember that there is one less co-ordinate to deal with.

Let's refer back to fig 1 for a moment below, but this time from a texturing perspective.

81
This time we are going to texture the tile with the blue blob using the water texture from our 2x2 texture
image. We will do this using SET MATRIX TILE Mn, X, Z, TextureNum. Looking at fig 1, we can see that using the
larger white numbers, that particular tile is at X=2, Z=2 and looking at the texture image, water is texture
number 4. We know we are using matrix number 1, so this gives us:

SET MATRIX TILE 1, 2, 2, 4

Once again, you can see that creating a good looking terrain using this method would be a slow laborious
process. However, there are ways to speed up the texturing process without a matrix editor. One method is to
create your matrix grid on paper and list your textures - each one numbered. If you 'colour in' your matrix by
putting the number of the texture you want in each grid square, you can transfer all the numbers to DATA
statements then read all the texture values in a loop.

Let's assume a 5x5 matrix which has 6 used textures. Your sketch would look something like the one on the
here.

Feeding the texture numbers into the matrix with a loop like FOR...NEXT usually counts from 0, so with this in
mind, we need to build our data statement up reading from left to right working our way up the matrix
starting at the bottom - remember, the bottom left corner tile is 0,0.

So, our data statement (placed at the end of the program) would look like this:

DATA 2,2,3,3,2,2,3,4,5,3,2,3,4,5,3,1,2,3,3,6,1,1,2,6,6

and the program code would look something like this:

TileWidth=5: Rem Matrix Tiles Width


TileHeight=5: Rem Matrix Tiles Height
FOR Nz=0 To TileHeight-1
FOR Nx=0 To TileWidth-1
Read TextureNum
SET MATRIX TILE 1, Nx, Nz, TextureNum

82
Next Nx
Next Nz
Update Matrix 1

Rem Data Statements At End Of Program


DATA 2,2,3,3,2,2,3,4,5,3,2,3,4,5,3,1,2,3,3,6,1,1,2,6,6

You could use the same loop method to make matrix height setting easier as well, but there's no easy way to
know what values to use for each height like you can with colouring textures with pen and paper. If you are
clever though, you can create an algorithm with SIN/COS to calculate the heights to create nice hills etc.
Alternatively, you can do what many people do - write your own simple matrix editor if there isn't one out
there that suits your purposes.

As if you didn't know already, MatEdit is available from the downloads page on my web site and if you want to
read about some of the other things it makes so easy for you, check out the MatEdit page on there.

Clever Stuff

OK, it's been such a long tutorial, you may have forgotten that a little earlier I mentioned that there were
clever ways to get around the 70x70 tile matrix limit. In the downloads section of my web site, there's a small
demo where you can wander around a landscape which is 300x300 tiles square! I haven't tried it myself, but
I'm informed by someone who has, that you can wander in the same direction for half an hour and still not hit
the edge!

MatEdit's Monster Matrix option was restricted to 600x600 so that the whole terrain area would be visible on-
screen in 800x600 screen mode. The fact is that the real maximum size of your terrain using the method I
designed is limited only by the amount of memory you have. My main PC has 1.5 Gigabyte of memory and in
theory I could create a terrain of such a size that it could take weeks - not hours - to walk from one side to the
other!

So how is this done? Well, it's quite a simple idea really and a number of other DB users have improved on the
idea since I first did it. So let's cover the theory first so you can go away and try to write something yourself...

The Theory

Normally, you control your character and he moves across the matrix landscape, the camera in tow. Another,
slightly more difficult method is to place the character at 0,0,0 in space and reposition the matrix so that the
character looks like it's standing in the centre of it. When the character is moved, it may animate and look like
it is moving, but it doesn't - the matrix does!

In fact there is a command called SHIFT MATRIX which lets you scroll the matrix rows and/or columns of tiles
up, down, left or right - without actually moving the matrix itself. Rows and columns which are scrolled 'off'
the matrix re-appear automatically at the opposite edge of the matrix. Unfortunately, shifting the matrix by
such huge amounts looks incredibly jerky and totally crap. So, what you do is physically move the matrix a
small amount a number of times until it reaches the point that the next row or column of tiles is reached, at
which point you put the matrix back at its starting position and use the SHIFT MATRIX command.

If this is done carefully, it looks like the character is smoothly walking over the matrix. What's more, as the
matrix is continuously scrolling with the SHIFT MATRIX command, if it has been designed correctly, you never
reach the edge of the matrix whichever direction you travel. The result is a matrix which goes on forever - but
it certainly isn't a huge matrix - the biggest it can be is 70x70 tiles and with the same landmarks encountered

83
at regular intervals, the player will soon notice what is happening. There is an example of a never-ending
matrix in the downloads section.

My idea was to create a 600x600 array with all the height data in it and another one with all the texture data in
it. I didn't bother for MatEdit, (as it's only a matrix editor, not a world editor), but it would be a fairly simple
process to create another array containing object position data.

On screen you have a single small matrix which actually needs to be no bigger than 10x10 tiles if fog is used
effectively, though any size could be chosen. The same method of character control is used as described above
by moving the matrix and then using SHIFT MATRIX, but the big difference being that just before the SHIFT
MATRIX is used, the relevant tile row or column is completely replaced from the arrays. The routines keep
track of the character's direction so it knows which rows/columns to update and the terrain doesn't repeat. Do
the same thing with 100,000x100,000 arrays and you still only have a 10x10 tile matrix on screen, so there's no
additional slowdown or lag when you are playing, just an amazingly large playing area!

84
Dark Basic 3D Collision (DBC & DBP)

Very few games can be written without some form of collision. You need a way to stop your character from
walking through walls/other characters or dropping through the floor he/she is walking on. This is what the
collision commands are for.

In a nutshell, the DB collision commands simply alert you when certain objects bump into each other. This is in
3D by the way - sprites in 2D also have collision, but sprites are the topic of another tutorial.

Before we start though, I have to explain that you will find a number of third party aids to help with collision -
like Sparky's external dll, because it's widely reported that DB's in-built collision commands are neither all that
accurate nor particularly fast when put to the test. I've not had cause yet to stretch them that far, so I cannot
confirm this from personal experience but have to believe that this is probably true.

However, having said that, I think it's important to get to know how the built-in commands are used. Try them.
Once you understand how they work, implementing a third party solution is made that much easier. What's
more, you may even find that the DB collision commands are actually more than enough for your needs -
without you having to go away and try and learn a new set of commands.

The two basic collision commands in DB are:

OBJECT HIT(ObjNumA, ObjNumB)


OBJECT COLLISION(ObjNumA, ObjNumB)

The first command alerts you if object A bumps into object B, whereas the second lets you know if object A is
overlapping object B.

The two parameters ObjNumA and ObjNumB are your main control character's object number (A) and the
object you are testing for collision with (B) - both are integers.

Copy and paste the following code in the DB classic editor and run it. Use the cursor keys in this top-down view
to run the red cube into the white one.

Sync On: CLS 0


Sync Rate 60
AutoCam Off
Hide Mouse
Make Matrix 1,2000,2000,20,20
Create Bitmap 1,128,128
CLS RGB(0,128,0)
Get Image 1,0,0,128,128
Set Current Bitmap 0
Delete Bitmap 1
Prepare Matrix Texture 1,1,1,1
Make Object Cube 1,3: Rem Our Cube
Position Object 1,1000,3,990
Color Object 1,RGB(255,0,0)
Make Object Cube 2,3
Position Object 2,1000,3,1000
Position Camera 1000,50.0,980
Point Camera 1000,3,980
Do
If UpKey()=1 Then Move Object 1,0.1

85
If DownKey()=1 Then Move Object 1,-0.1
If LeftKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)-2.0)
If RightKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)+2.0)
ObjPosX#=Object Position X(1)
ObjPosY#=Object Position Y(1)
ObjPosZ#=Object Position Z(1)
Position Camera ObjPosX#,30.0,ObjPosZ#
If OBJECT HIT(1,2)=1
Color Object 2,0
Sleep 20
Repeat
Until OBJECT HIT(1,2)=0
Color Object 2,RGB(255,255,255)
Endif
Sync
Loop
End

You will have noticed that when you bump into the white cube, it flashes black. In the code, you can see that
the colour of the stationary cube is set when OBJECT HIT(1,2) returns a 1. The Repeat...Until loop immediately
after this executes continuously until OBJECT HIT(1,2) returns a 0, at which point the stationary cube is
returned back to white.

However when the program is running and you hit the other cube, it turns black then immediately returns to
white and you continue passing through the other cube.

Passing through the other cube is what should happen because we haven't yet written any code to handle
what happens when a collision does occur, (it's not automatic).

As for the colour flashing, that's because OBJECT HIT does just that - it detects the first contact between two
objects. It isn't triggered again while the two objects are colliding - you need to move your cube completely
away from the other to end the collision and then bump back into it again to trigger another hit.

So how do we know if a collision is still occurring after the initial hit?

Enter the OBJECT COLLISION command...

This is similar to OBJECT HIT, but returns a 1 if two objects are overlapping. Try running the following program:

Sync On: CLS 0


Sync Rate 60
AutoCam Off
Hide Mouse
Make Matrix 1,2000,2000,20,20
Create Bitmap 1,128,128
CLS RGB(0,128,0)
Get Image 1,0,0,128,128
Set Current Bitmap 0
Delete Bitmap 1
Prepare Matrix Texture 1,1,1,1
Make Object Cube 1,3: Rem Our Cube
Position Object 1,1000,3,990
Color Object 1,RGB(255,0,0)
Make Object Cube 2,3
Position Object 2,1000,3,1000
Position Camera 1000,50.0,980

86
Point Camera 1000,3,980
Do
If UpKey()=1 Then Move Object 1,0.1
If DownKey()=1 Then Move Object 1,-0.1
If LeftKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)-2.0)
If RightKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)+2.0)
ObjPosX#=Object Position X(1)
ObjPosY#=Object Position Y(1)
ObjPosZ#=Object Position Z(1)
Position Camera ObjPosX#,30.0,ObjPosZ#
If OBJECT HIT(1,2)=1
Color Object 2,0
FirstHit=1
Endif
If FirstHit=1
C=OBJECT COLLISION(1,2)
If C=0 Then Color Object 2,RGB(255,255,255): FirstHit=0
Endif
Sync
Loop
End

It's basically the same as the first example apart from inside the main Do..Loop. This time, when you bump into
the stationary cube, it stays black until you move off it. This is because once the initial 'hit' is recorded, we set
a flag (change the value of a variable) to denote that a collision has occurred. In this case, we set the variable
FirstHit to 1.

When this is set to 1, the code in the If FirstHit=1 block is carried out each time around the loop. Before the
initial collision, FirstHit was set to 0 (zero), so it was ignored.

In the If FirstHit=1 block, we test with OBJECT COLLISION. While objects 1 and 2 are overlapping, this will
always return a 1. The next line checks to see if it returned 0 and when it does, (only when the objects are not
colliding), it returns the other cube back to white and turns the flag back off so the If FirstHit block isn't
executed again unnecessarily.

So now, we know how to detect a collision between two objects. But, have you noticed the downside?

Here we are just testing for collisions between objects 1 (our cube) and 2 using OBJECT HIT(1,2) and OBJECT
COLLISION(1,2). But what if we have another object - or dozens more objects? Won't we need a line of code
for each of them?

Well, no actually, we don't. Remember the ObjNumA and ObjNumB from earlier?

A useful little option is to use a 0 (zero) for ObjNumB, making it HIT(1,0) and COLLISION(1,0). With this option,
the command no longer returns a 0 or a 1, but instead returns zero or the number of the object that object 1
(our cube) is colliding with! Now that's a bit more useful don't you think?...

So let's see how to detect collision with many objects when you don't know what object numbers they are...

Sync On: CLS 0


Sync Rate 60
AutoCam Off
Hide Mouse
Randomize Timer()
Make Matrix 1,2000,2000,20,20

87
Create Bitmap 1,128,128
CLS RGB(0,128,0)
Get Image 1,0,0,128,128
Set Current Bitmap 0
Delete Bitmap 1
Prepare Matrix Texture 1,1,1,1
Make Object Cube 1,3
Position Object 1,1000,3,990
Color Object 1,RGB(255,0,0)
For N=2 To 11
Make Object Cube N,3
Position Object N,1000+(Rnd(60)-30),3,1000+(Rnd(60)-30)
Next N
Position Camera 1000,60.0,980
Point Camera 1000,3,980
Do
If UpKey()=1 Then Move Object 1,0.4
If DownKey()=1 Then Move Object 1,-0.4
If LeftKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)-2.0)
If RightKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)+2.0)
ObjPosX#=Object Position X(1)
ObjPosY#=Object Position Y(1)
ObjPosZ#=Object Position Z(1)
Position Camera ObjPosX#,60.0,ObjPosZ#
H=OBJECT HIT(1,0)
If H>0: Rem collision detected
Color Object H,0
NowColliding=H: Rem Set flag to number of object we are colliding with
Endif
If NowColliding>0
Rem Test to see if we are still colliding
C=OBJECT COLLISION(1,NowColliding)
Rem And if not, recolour cube to white and turn off flag
If C=0 Then Color Object NowColliding,RGB(255,255,255): NowColliding=0
Endif
Sync
Loop
End

Important Note: In this example, if two objects are close to each other, then it's possible to still be colliding with
one object when you hit another. In such a case, the hit on the second object is not detected. This is because if
OBJECT COLLISION() is returning any value other than 0 then OBJECT HIT() is not tested for. It doesn't matter
that the object you hit is different to the one you are currently colliding with.

This is normal and won't be a problem when we actually come to handling the collision later.

So we now know how to detect when we hit another object - regardless of object number. Next we have to
deal with it because as you have seen, detecting the other object doesn't stop you moving through it.

What method we use depends on the method used to move the object we are controlling. There are two main
ways to move a 3D object around:

The first is to turn it in the required direction, then use the Move command to move it the required amount in
the direction it is facing.

The second is to use variables for the X, Y and Z position and use Position Object along with the Rotate
commands to move the object about.

88
Our examples so far use the first method. This makes it easy to handle basic object collision because we can
simply use the Move command to move back to the very last position before by using a minus value.

Here's the next example...

Sync On: CLS 0


Sync Rate 60
AutoCam Off
Hide Mouse
Make Matrix 1,2000,2000,20,20
Create Bitmap 1,128,128
CLS RGB(0,128,0)
Get Image 1,0,0,128,128
Set Current Bitmap 0
Delete Bitmap 1
Prepare Matrix Texture 1,1,1,1
Make Object Cube 1,3
Position Object 1,1000,3,990
Color Object 1,RGB(255,0,0)
Make Object Box 2,80,20,3: Rem Create a wall
Position Object 2,1000,3.0,1000
Ghost Object On 2
Position Camera 1000,50.0,950
Point Camera 1000,3,990
Speed#=0
Do
If UpKey()=1 Then Speed#=0.4: Move Object 1,Speed#
If DownKey()=1 Then Speed#=0-.4: Move Object 1,Speed#
If LeftKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)-2.0)
If RightKey()=1 Then YRotate Object 1,WrapValue(Object Angle Y(1)+2.0)
ObjPosX#=Object Position X(1)
ObjPosZ#=Object Position Z(1)
Position Camera ObjPosX#,50.0,ObjPosZ#-40
Point Camera ObjPosX#,3.0,ObjPosZ#
If OBJECT COLLISION(1,0)>0: Rem collision detected
Repeat
Move Object 1,0-Speed#
Position Camera Object Position X(1),50.0,Object Position Z(1)-40
Point Camera Object Position X(1),3.0,Object Position Z(1)
Until OBJECT COLLISION(1,0)=0
Endif
Sync
Loop
End

You will notice first of all that this example does not use OBJECT HIT(). You don't have to if you know that all
the objects in your program are simply objects that you cannot pass through. If this is the case - like this
program - you just stop your object from passing through any them.

However, if some of your objects are not solid, then you would use OBJECT HIT() to detect the number of the
object you just collided with, before deciding how to deal with the collision.

Also, the actual collision effect used here when the cube hits the wall isn't the best for a game. When a game
character hits a wall at an angle, the game's continuity and smooth running is interrupted if the character
comes to a dead halt. The alternative is 'Sliding Collision' which is no more realistic when you think about it -
when was the last time you ran into a brick wall and 'slid' along it - but it does look and feel much better in a
game.

89
The changes to this program from the last one are a little more substantial too.

First of all, we create a wall as object number 2. We also place the camera a little further 'South' of our cube so
we can see the wall at a better angle.

We also create a new variable called Speed# with which to move our cube around.

The reason for this is that when a collision is detected, we have to move the cube backwards, away from the
wall, but only if we are moving forwards! The cursor down key lets us back away from the wall, but what if we
bump into the wall while moving backwards? Well, when the collision is detected we would move the cube
backwards further into the wall.

So, to prevent this from happening, we use a variable to store the current direction. When we press cursor up,
Speed# is set to a positive number so the cube moves forwards. When we press cursor down, Speed# is set to
a negative number so the cube moves backwards.

The advantage of this is that it doesn't matter what direction the cube is heading, moving it 0-Speed# ALWAYS
moves it in the opposite direction by the same amount.

So, in the example, in the loop we test to see if a collision has been triggered with:

If OBJECT COLLISION(1,0)>0

If our cube hits anything, the code in this block will be executed. In the event of a collision therefore, we use a
Repeat...Until loop moving the cube back with Move Object 1,0-Speed#, which as mentioned previously moves
the cube back. As this is in a repeat loop, it's repeated until the cube is no longer in collision with the wall, at
which point the loop is exited.

This method isn't perfect because at certain angles, you can clip the wall with the corner of the cube when
rotating it - in which case, the backing out routine kicks in and you are instantly 'transported' to the other end
of the wall! For this reason, there's a little more work involved, but it's better to use the X, Y and Z variables for
moving objects around if you need collision rather than Move.

So let's now take a look at Sliding Collision...

Sliding collision isn't that much different to what we've just been doing.

The basic difference is that we have to use collision boxes. We don't just move back to the last known good
position, we use other DB commands to tell us where to go back to.

Collision boxes are invisible 'wrappers' that you apply to objects. These wrappers can give you feedback on
collision and you simply size them to fit the object you are applying them to. If you move the object, then the
collision box moves with it.

The new command we are going to use is MAKE OBJECT COLLISION BOX Object Number,x1,y1,z1,x2,y2,z2,flag
and looks rather complicated with all those parameters. But believe me it's not really. Let's go through them...

90
Object Number

Obviously this is the number of the object that you are creating the collision box for.

X1,Y1,Z1

OK, remember that I just said that when you move an object, then the collision box moves with it? That's
because the collision box is attached to the object number. As an object can be placed anywhere in your world
with the Position Object X,Y,Z command, we can't use 'real' X,Y,Z world location co-ords to define the collision
box because if we moved the object, the collision box would still be at the old location.

So, instead, when you define a collision box, you use X,Y,Z OFFSETS which assume that the object is at point
0,0,0 in space. This is a neat method as we can use negative and positive values which remain true wherever
the object is placed.

OK, an example. Imagine a box or wall 100 wide (X), 50 tall (Y) and 10 Deep (Z). We make the object with Make
Object Box 1,100,50,10 and the CENTRE of this box is positioned at 0,0,0 in space.

Therefore, the X offset to reach the left edge of the box along the X axis is -50 and this is used for the value X1.
We repeat the process for the Y and Z axis. With a box 50 tall, Y1 is -25 and with a depth of 10 then Z1 is -5.

Notice that the values are the negative equivalent of the boxes X, Y and Z dimensions divided by 2. Eg: Width
(X) = 100/2 = 50 which makes -50.

X2,Y2,Z2

If we follow the X axis from the centre 'til we reach the right edge of the box we find that it is at 50, so this is
used for the X2 parameter. In the same fashion, Y2 is 25 and Z2 is 5. Notice this time that if X1 = -50 then X2 is
50 and that if Y1 is -25 then Y2 will be 25 and so on.

Here's a small image to show you what exactly is happening:

In the image, our object (the wall) is green. The X, Y and Z world axis are the black lines and where they all
intersect, is position 0,0,0. As you can see, this is exactly in the centre of our wall. The dimensions of the wall
are shown in blue.

If the X axis is 0 at the centre of the object, the values run negative to the left and positive to the right.

91
The red distances are those used for the offset parameters. The important thing to realise using this method is
that as mentioned earlier, once these offsets are defined, even if the wall was moved to another X,Y,Z location,
the offsets would still define the collision box correctly, whereas real X,Y,Z locations would not.

The origin for the offsets is based on the X,Y,Z world position of the Object whose number you supply. Easy
eh?

Let's see the code:

Set Display Mode 800,600,16


Autocam Off
Sync On: CLS 0
Sync Rate 60
Hide mouse
Make Matrix 1,2000,2000,20,20
Create Bitmap 1,128,128
CLS RGB(0,128,0)
Get Image 1,0,0,128,128
Set Current Bitmap 0
Delete Bitmap 1
Prepare Matrix Texture 1,1,1,1
Position Matrix 1,-1000,0,-1000
Make Object Cube 1,5
Color Object 1,RGB(255,0,0)
Make Object Collision Box 1,-2.5,-2.5,-2.5,2.5,2.5,2.5,0
XPos#=0: YPos#=2.5: ZPos#=0
Position Object 1,0,2.5,0
Make Object Box 2,150,50,10
Position Object 2,0,25,100
Make Object Collision Box 2,-75,-25,-5,75,25,5,0
Do
If Upkey()=1 Then Move Object 1,2
If Downkey()=1 Then Move Object 1,-2
If Leftkey()=1 Then Yrotate Object 1,Wrapvalue(Object Angle Y(1)-4)
If Rightkey()=1 Then Yrotate Object 1,Wrapvalue(Object Angle Y(1)+4)
XPos#=Object Position X(1)
YPos#=Object Position Y(1)
ZPos#=Object Position Z(1)
Rem The sliding collision bit
If Object Collision(1,0)>0
Dec XPos#,Get Object Collision X()
Dec YPos#,Get Object Collision Y()
Dec ZPos#,Get Object Collision Z()
Endif
Position Object 1,XPos#,YPos#,ZPos#
Angle#=Object Angle y(1)
CamDist#=40.0 : CamHeight#=YPos#+10.0 : Camsmooth#=3.5
Set Camera To Follow
XPos#,YPos#,ZPos#,Angle#,CamDist#,CamHeight#,CamSmooth#,0
Sync
Loop

When the program is running, in the main loop, we check for the cursor keys and move the cube in the
required direction or rotate it and then grab the object's new position into the variables XPos#, YPos# and
ZPos#.

We then test for a collision with Object Collision(1,0) and if the result is anything but a zero then we've hit
something. The value returned is the number of the object we are banging our head against. We next use

92
three functions we've not seen before: Get Object Collision X(), Get Object Collision Y() and Get Object
Collision Z().

If you cast your mind back to when I was describing how you use the Make Object Collision Box command, I
mentioned that the last parameter was called flag and that if you set it to 1 then the collision box rotated with
the object. Remember? Well, if you set that flag to zero, you can use the Get Object Collision functions.

These return the sliding collision values which we use to back up our object. Remember though that they ONLY
return data if that little flag is set to 0!

So, armed with the amount to back up in the X, Y and Z directions, we decrement them from our object's X, Y
and Z positions and use Position Object to place the object in its new position. The rest of the code simply
handles the camera to follow the cube.

Well, that's the end of this tutorial on Simple DB Collision and armed with this new knowledge, you can
investigate the many other collision commands DB supplies you with.

93
2D Shooting - A Basic Introduction (DBC & DBP)

Many games in both 2D and 3D require the ability to shoot - something no more difficult than locating where a
projectile is being fired from, where it is being aimed, and then plotting it's course between the two points.

In 3D games, which I'll cover in a different tutorial, most beginners make the same mistake as I did - the fatal
one of not thinking about what happens in real life.

I have to admit that when I started with DB I spent many hours trying to make a bullet move fast enough to
look right. But, that's the problem...

When you fire a bullet from a gun or a sniper rifle, the bullet moves that fast you don't see it. So why waste
time in your programs trying to show what you shouldn't be able to see anyway?

First though, we'll cover the less realistic environment of the 2D shooting problem.

2D Shooting

In a 2D program the bullet can be a sprite, an image, or even an ASCII character. Whatever the type of bullet,
the process is as follows:

1. Place the bullet in front of the gun barrel


2. Calculate the next position of the bullet
3. Replace the background for the old bullet position
4. Place the bullet in the new position
5. Check to see if hit the enemy or left the screen
6. If 'no' to both questions at number 5, repeat from number 2
7. End firing routine

The actual X and Y positions of the bullet on the screen are held in variables so we can alter them and move
the bullet. Let's try a very simple example of the type that most beginners usually make. To keep it simple, we
will use the Text X,Y command and ASCII characters - remember the method is basically the same with images
and sprites.

Note: The example programs have been written to demonstrate the process as clearly as possible - not to
demonstrate good programming practices. Once you understand how these programs work, you can optimize
and improve on them.

Set Display Mode 800,600,16


Sync On: CLS 0
Sync Rate 60
Hide Mouse
Set Text Opaque
BasePosX = 400
Text BasePosX,580,"M"
Rem Main Program Loop
Do
If Leftkey()=1 Then Dec BasePosX,4
If BasePosX<0 Then BasePosX=0
If Rightkey()=1 Then Inc BasePosX,4
If BasePosX>788 Then BasePosX=788
CLS

94
Text BasePosX,580,"M"
If Spacekey()=1 Then Gosub MoveBullet
Sync
Loop

MoveBullet:
BulletPosX = BasePosX + 5
BulletPosY = 580-16
For N=BulletPosY To 0 Step -5
Text BulletPosX,N,"|"
Sleep 1
Text BulletPosX,N," "
Rem Check to see if we hit anything here and if we did
Rem handle it.
Next N
Return

Essentially, BasePosX is integer variable used to store the horizontal position on the screen and when the left
and right cursor keys are pressed, we decrement or increment this variable, clear the screen and print our base
(the letter M) in the new position.

When the space bar is pressed, we gosub the MoveBullet procedure.

In that, we have a loop which calculates the bullet start position based on the current base position then goes
around a loop decrementing the bullet's Y position, printing and erasing the bullet on the screen.

But, if you run the above code, you'll see there's a major problem: When you fire, the base freezes while the
bullet is on the screen.

That's because this little program isn't the correct way to do this. But because we learn from our mistakes,
seeing the wrong way to do something makes the right way easier to understand.

To solve this problem, we really need to get rid of the For N=BulletPosY To 0 Step -5 loop which 'traps' our
program while the bullet is moved.

This is done by calling the MoveBullet procedure EVERY time we go through the main program loop and move
the bullet a bit each time. To do this we have to keep track of it's current position manually as we no longer
have the variable N from the For...Next loop.

At this point, the keen-minded of you may have a question: "If we do that, won't the bullet be firing all the
time"?

Well the answer initially is yes. So, we use something called a 'flag'. A flag simply conveys a true/false message
and in programming terms is a variable which we use to store whether something has been done or not.
Usually this would be by setting the variable to 0 (zero) if it hasn't and 1 if it has. (Some languages have
Boolean variables which can be set to True or False, but as DB Classic does not, in this tutorial we'll just use a
normal integer variable set to 0 or 1 for the same result).

So, when the space bar is pressed, we set a flag to 1 to say a bullet has been fired and it remains 1 until the
bullet no longer exists - at which point the flag is set back to 0 (zero).

In our main loop, we gosub the MoveBullet procedure only if this flag is set to 1. Easy eh?

95
But another problem then creeps in. If we keep pressing the space key, each time we do, the bullet currently
moving up the screen starts again from the beginning!

Stopping this is easy. All we do is tell the program to ignore the space bar if a bullet is in flight. As we already
have a flag which equals 1 when a bullet is fired, we just tell the program to ignore the space bar unless the
bullet flag equals zero!

So let's modify our above example...

Set Display Mode 800,600,16


Sync On: CLS 0
Sync Rate 60
Hide Mouse
Set Text Opaque
BasePosX = 400
Text BasePosX,580,"M"
Rem Main Program Loop
Do
If Leftkey()=1 Then Dec BasePosX,4
If BasePosX<0 Then BasePosX=0
If Rightkey()=1 Then Inc BasePosX,4
If BasePosX>788 Then BasePosX=788
CLS
Text BasePosX,580,"M"
If Spacekey()=1 and FiredBullet=0
FiredBullet=1
BulletPosX = BasePosX + 5
BulletPosY = 580-16
Endif
If FiredBullet=1
Gosub MoveBullet
Else
Sleep 1
Endif
Sync
Loop

MoveBullet:
Dec BulletPosY,5
Text BulletPosX,BulletPosY,"|"
Sleep 1
Text BulletPosX,BulletPosY," "
Rem Check to see if we hit anything here and if we did
Rem handle it. Reset flag if bullet is done with
If BulletPosY<=0 Then FiredBullet=0
Return

In this example, when the space bar is pressed, so long as there isn't a bullet already in flight, we calculate the
bullet start position and turn on the 'fired bullet' flag.

Next, if the flag has been set, we gosub the MoveBullet procedure which moves the bullet a bit then
immediately returns to allow the user to move the base.

You'll also notice that there is also an 'Else' section. This is because when you fire a bullet, a delay is used in the
procedure (Sleep 1) so the bullet can be seen. This isn't good practice, but keeps the example nice and simple
for the tutorial.

96
This also means that when there is no bullet being fired the program runs quicker. The result is that the base
moves at two speeds - one when no bullet is visible and slower when one is.

So, this If..Else..Endif section basically says 'if there is a bullet on screen then don't use a delay - if there isn't
then do use a delay'. In a nutshell it means that the base moves at roughly the same speed whether firing or
not.

OK, that covers simple 2D firing, but "wait!" I hear you cry... "you can only fire a single bullet and I want more!"

Well, for multiple firing the process is just the same, but you need separate variables to store the X and Y
screen positions of EVERY bullet.

This task is exactly what arrays were designed for so if you don't fully understand arrays, go and read the
tutorial which covers them now, then come back when you know how they work. In this next section I have to
assume that arrays are not a mystery to you as I make no attempt at explaining them.

Arrays are covered in my Beginners Guide To Programming Part 1 tutorial.

Multiple Bullets

In the last example it gets a little more complicated as we need to keep essentially the same method, but
handle more than one bullet in the MoveBullet procedure, when we don't know how many bullets there are -
if any! This is done with arrays. We also add another procedure for the creation of a new bullet, but more on
that after the example code.

Before we start, we also need to decide on a maximum number of bullets to display at any one time. Also, our
flag from the last example can no longer be a single flag - there has to be one for each possible bullet.

So we create a variable for the maximum number of bullets and use this value to DIMension arrays to hold
each bullets X and Y position and represent each bullet's 'fired or not' flag.

In the MoveBullet procedure, we go back to using a loop and use the flag array to decide if the respective
bullet should be displayed. Let's see the code:

Set Display Mode 800,600,16


Sync On: CLS 0
Sync Rate 60
Hide Mouse
Set Text Opaque
BasePosX = 400
Text BasePosX,580,"M"

MaxBullets = 20
Dim FiredBullet(MaxBullets)
Dim BulletPosX(MaxBullets)
Dim BulletPosY(MaxBullets)

Rem Main Program Loop


Do
If Leftkey()=1 Then Dec BasePosX,8
If BasePosX<0 Then BasePosX=0
If Rightkey()=1 Then Inc BasePosX,8
If BasePosX>788 Then BasePosX=788

97
CLS: Text BasePosX,580,"M"
If Spacekey()=1 Then Gosub AddBullet
If BulletCount > 0 Then Gosub MoveBullet
Text 0,0,"Bullets On Screen: "+Str$(BulletCount)+" "
Sync
Sleep 1
Loop

MoveBullet:
For N = 1 To MaxBullets
If FiredBullet(N)=1
BulletPosY(N)=BulletPosY(N)-20
Text BulletPosX(N),BulletPosY(N),"|"
Rem Check For Bullet Going Off Top Of Screen Here And Deal With It
If BulletPosY(N) <= 0
FiredBullet(N) = 0
Dec BulletCount
Endif
Rem Check For Hit Enemy Here And Deal With It
Endif
Next N
Return

AddBullet:
For N = 1 To MaxBullets
If FiredBullet(N)=0
FiredBullet(N)=1
BulletPosX(N) = BasePosX + 5
BulletPosY(N) = 580-16
Inc BulletCount
Exit
Endif
Next N
Return

You can alter the maximum number of bullets by changing the value of MaxBullets. If this is set to 20, then
when you have fired 20 bullets, you can't fire any more until at least one has hit something or gone off the top
of the screen.

There are also a couple of changes in the Main Loop worth mentioning...

Note: Remember the machine guns in war movies where the ammunition is on a 'belt' and is fed into the
weapon from a box on the floor next to it? Well, for this explanation, think of the array as one of those ammo
belts, but as a continuous loop containing 'MaxBullets' bullets. Each bullet is in a slot and once a bullet has
been fired, it's slot is empty and a new bullet can be clipped into the belt at that position.

First of all, when the space key is pressed, we now Gosub AddBullet where we loop through every element of
the FiredBullet() array looking for an empty slot. If you remember, when bullet 1 is in use, then FiredBullet(1)
equals 1 and when it is not, it equals 0.

When we press space, this routine finds the first empty slot in the array, turns the FiredBullet() flag on (sets it
to 1), stores that bullet's X and Y start position and increments the variable which holds how many bullets are
currently in use.

The next line, the Exit, basically exits the For..Next loop at that point without unnecessarily running through
the whole loop. For example, if it was the first bullet and there were a maximum of 10 bullets, the first slot (1)

98
is used and continuing the loop testing the other 9 would be a waste of time. So, placing an Exit inside the
block when an empty slot is found saves our program a lot of wasted testing.

OK, at this point, BulletCount equals 1 so in the main loop, Gosub MoveBullet is called. This is another similar
loop but this time, the array flag is used to say whether the bullet is drawn or not.

If the bullet goes off the top of the screen or hits something (that's the bit you can write yourself), then we set
the array flag back to 0 to show that the slot is available for use again and decrement the BulletCount variable.

If the space is pressed again while another bullet is on screen, then as we are using arrays, another one is
created and as this is done in a For..Next loop which counts from 1 to the maximum number of bullets you can
have (MaxBullets), you can never create more than the required number of bullets, (or as we are using arrays,
ever get the dreaded 'index out of bounds' error)!

99
BitMap Font Tutorial

by Craig Chipperfield

Bitmap fonts; what are they and what can you do with them? Well, before we look at bitmap fonts it is
important to look at ‘normal', or True-Type fonts.

DarkBASIC Professional has a number of text commands which can be found in the help file under the section
... wait for it ... Text Commands. Using these commands we can load in any font that is in the windows/fonts
directory and set its point size, set it to bold, underline or italic and we can change its colour. Other than that,
there is little else we can do to change the appearance of the fonts. If we want to make fonts like in the title of
this tutorial, that look like they are made of metal, fire and neon lights, or indeed any appearance other than
plain, single coloured text then we need to use bitmap fonts.

So, what are Bitmap fonts? A bitmap font is a collection of images that represent text characters, and because
they are images they can be made to look as graphically pretty as we like. The advantage of using them is, of
course, the ‘prettiness'. The disadvantage is that they are a little trickier to set up and use but don't worry, this
tutorial (and the accompanying code) will make things much easier for you.

There are two ways we could display bitmap fonts.

1. Using Sprites
2. Using Textured 3D plains

This tutorial will deal with the first method but it is easy to take the
code and change it if you would rather have 3D plains.

Now that we have got our bitmap fonts and we have established that
we are going to display them using sprites there are four steps to
getting them on screen. So, you see, it is a little more complicated
than a simple TEXT or PRINT command to use true-type fonts but I
think you will agree; the end results are worth it.

Anyway, those four steps:

1) Load the font file into DBP


2) Cut the fonts into single character images
3) Trim the wasted space to the left and right of each character.
4) Get them on screen

Depending on which bitmap fonts you are using, it is not always


necessary to go through step 3. Many bitmap font files have each
character the same width but I think that it looks ugly to have a ‘W'
the same width as an ‘I', which is why all my fonts use the trimming
method.

If you don't want to know the technical details of how the bitmap fonts are loaded and displayed but instead
you just want to get them into your project and working, then skip ahead to the end of this tutorial where I will
tell you the basics of how to use them.

You still here? In that case, you obviously want to know how this all works. So, let's look at those four steps in
a little more detail.

100
Step 1: Loading the font file.
The font file is a simple image file. So, to load it, all we have to do is use the LOAD IMAGE command.

Step 2: Cutting up the fonts into single images.


The best method of doing this is to use memblocks so that we can retain any transparency in the image. Firstly
transfer the font image into a memblock, then copy an area of the memblock into a second one and make an
image of that. Repeat those steps until all the characters have been made.

You can see how this is done if you download the code but don't worry if you don't understand memblocks. At
the end of this tutorial I will provide very simple steps to get bitmap fonts into your projects without having to
understand any of it ... brilliant!

Step 3: Trimming the wasted space from each side of the characters.
Again we will use memblocks for this and look at each character in turn. We can look at each column of pixels
from the left side and if there are no non-transparent pixels, we can safely say that we have found our first bit
of wasted space. We continue with that until we do hit a non-transparent pixel and then repeat from the right.
Instead of actually trimming the image file down, we simply store a variable to represent the left edge of the
character and another variable for the width of the character. We can use these values later in the display
code to correctly position each character.

In the code provided; steps 1-3 are all carried out with a single command:

fontStyle = LoadBMfont( filename )

fontStyle is a variable used to represent each style of bitmap font. Using a method like that means that we can
load in as many bitmap fonts as we like and easily display the one we want simply by referring to it by name.

Step 4: Displaying the fonts.


To display the fonts, we need to find the next available sprite number, create a sprite for each character, store
the sprite number for the first character and store the length of the string. This is all taken care of in the
provided function and is best explained by reading the code.

How to use the supplied functions

(This is the bit to jump to if you have just skipped the techno waffle)

InitialiseBMfonts()

You will need to call this at the start of your project but after any
display setting commands. It sets up variables and arrays ready for use
in the other Bitmap font functions.

fontStyle = LoadBMfont( filename )

This function loads a Bitmap font, cuts the individual images and trims
the waste space from the edges.
fontStyle is a variable that you can use to reference a Bitmap style,
which is very helpful if you load in more than one Bitmap font.

stringID = DrawBMfont( X, Y, string, fontStyle, Kern )

Used to display the Bitmap font on screen.


stringID is a reference to the Bitmap string. You can use this reference
to show, hide etc. the entire string in one go without having to

101
reference each sprite image individually. X & Y are the screen co-ordinates of the top left of the string.
String is the string to display on screen. fontStyle is the reference to the previously loaded Bitmap font.
Kern establishes how much space to leave between each character.

hideBMfont( BMstring )

showBMfont( BMstring )

scaleBMfont( BMstring, scale )

sizeBMfont( BMstring, sizeX, sizeY )

The above functions will use the associated sprite commands on all of your bitmap string simultaneously.

positionBMfont( BMstring, X, Y )

This will position a previously created bitmap string at the X and Y screen co-ordinates specified.

alterBMFont(BMstring, X, Y, S as string, fontStyle, Kern )

This function will change any or all aspects of your bitmap font string.
BMstring is the ID of a previously created bitmap string.
X is the screen X co-ordinate to display the string.
Y is the screen Y co-ordinate to display the string.
S is the string to display. It doesn't have to be the same as it was, you can use this function to change the
contents. Ideal if you bitmap string is constantly changing (like an ingame score for example).
fontStyle As in the DrawBMfont function this variable is the style of Bitmap font to use and MUST already be
loaded. It doesn't have to be the same as it was though and you can use this function to change the style of
font used. Kern Again, Kern is used to establish the x distance between each character. This function can be
used to alter it.

102
Space Invader Tutorial

Building a Framework
This month we're going start a small series of tutorials that will see us create a complete game - Space
Invaders. It's a lot more complex than Pong, but not too involved to cloud the tutorial with intricate
mathematical formulas and highly advanced techniques. It also covers a lot of the concepts we've studied over
the past few months, including asset management, timing and various other topics. There's no room for chit-
chat, so let’s get started!

Design, Design, Design!


I cannot over-emphasise how important designing your game is before you start. Get it right now, and the rest
of the journey is far easier. Picking a game that is so familiar will help to illustrate the design process, as we
know what to expect. Everyone finds their own comfortable way of fleshing out the game idea, but right now
you'll have to go with mine.

Step 1 is to "brainstorm", "mindmap", or whatever the current buzzword happens to be. Here is a map of all
the components we need, in no particular order of importance, but in a way that will aid us as we progress.

This map contains concepts, game components, methods, and resources. Icons help me identify the different
aspects. This is far from structured, but all of the ideas are thrown onto the page for reference. It's also
important to visualise the look and feel of the game too. With Space Invaders it's not difficult, but it will still
help to ascertain positioning and style.

103
Structure
Maintainability is a very important notion, and once again it is important to get this right at the start of the
programming process, not part way through. Rather than delve into the details of good programming practise,
we'll dive straight in and see it in action. Functions are the perfect partner when writing making it easy to read,
and easy to maintain. Subroutines also have their place, but functions have one major advantage - they are
self-contained. Later on down the line, when things don't go to plan (an inevitable fact of programming), being
able to isolate individual functions to debug will dramatically improve your ability to locate, fix and move on
swiftly.

The flow of the program can be defined in functions. Let's start by thinking about the broader picture:

Initialise()
Menu()
PlayGame()
Exit()

We have our 4 broadest areas, now let's take one of these, PlayGame(), and break it down a little further:

InitialiseGame()
PlayLevel()
EndGame()

Again, let's break down InitialiseGame() to a third level:

LoadObjects()
PositionObjects()
LoadSounds()
InitialiseData()

And so we can proceed, taking each section, and breaking it down into its smaller components. It's very logical,
it matches the way the human mind processes information and it produces a very neat and easy-to-follow
process. This top-down approach makes designing on-the-fly very easy too. As we progress, wherever there is
a complex or repetitive routine, it can be added as another unit of code in the form of a function. At this point
in the process, it isn't necessary to create the complete structure, the design allows us to improvise as we
code. As we will see later, it is also a fantastic way to test ideas on a basic level, and build them up into fully-
fledged game pieces later.

Let's take the first step in writing the code. Download the tutorial file, install and view example 1. It is our
framework for the game. We will add much more as we progress. Note that the number of comments at this
stage outweigh the lines of actual code. It is essential that everything is well documented for your own benefit;
even the most finely tuned of minds need relieving of the mundane task of remembering, to allow the creative
process to evolve unhindered.

104
Game Data
Behind the action of any game is a mass of data; object information, scores, lives, positions, and a great deal
more. When building business applications, it is the data that drives and feeds the application design. Likewise,
it is important that we maintain game information in an orderly and efficient manner.

Now is the time to put the data structures into place. We will take advantage of User Defined Types to collate
associated information, such as player and opponent variables. We will use arrays to make a scalable solution.
Scalability of the data is the key to allowing a game to be easily modified. This has been discussed in a previous
article, which may be worth reviewing at this point.

Once again, we could discuss the theories and concepts around data storage and manipulation ad infinitum,
but instead take a look at the Stage 2 example, which now includes the data types and arrays that we initially
require to build our game. We've accommodated the general operation, player, invaders, bases and everything
else we see the need to monitor and control.

Standing on 2 Feet

We still have a long way to go before there's anything visible to show for our efforts. But believe me, all of this
effort will be worthwhile in the long run. What we need to do now is start the application breathing. We
already know where to start, the Init() function is in our framework and that is the focus of our attention now.
Let's set up the environment (Sync rate, screen size etc), and put some default data in there for the player and
invaders. Putting the data in the initialisation routine makes it extremely easy to locate later when the game
needs adjusting and fine-tuning. Stage 3 includes all of these components, and also a static backdrop which
will be a permanent feature of the end product. Remember, the initialisation isn't necessarily finished right
now, but only time will reveal what else we require.

At this stage, the program can be compiled to prove the code is syntactically correct. The logic can also be
tested to a certain extent, by running the compiled program. The result should be a simple backdrop, ready to
complement the game to come.

The Story So Far


The amount of effort to date, in relation to the observable rewards are poor to say the least. But in terms of
the total effort required to complete our Space Invaders tribute, the task is well under way. Our delayed
gratification will be rewarded with a fast transition from very little to a fully fledged, easy to maintain game in
a very short space of time. Let's recap on the steps taken so far:

105
 Design - The game idea, components, supporting structures and data have been mapped out in visual
form.
 Visuals - There is a rough draft of the way the game will be presented, including gameplay area,
scoring system, player, enemies, and bases.
 Program Structure - The game flow has been mapped out into the initial layers of a well-structured
program. Coding the game will follow a natural, logical progression, with navigation and expansion of
the code simply and effectively implemented.
 Data Structure - The supporting data components are not only in place, but populated with the raw
information needed to start the engine rolling slowly forwards.

The Plan Ahead


What is next in the creation of our game? Visually, we need to build our player, enemies, bases and interface,
our presentation to the player. Programmatically, it's time to put the logic in place that will allow a simple
progression, bringing to life each part of the game one by one. We'll be using a method that provides the
flexibility to test each component in turn, ensuring the development is smooth and precise at every step.

In the meantime, the complete, compiled version of the program is included in the download to give you a
taste of what we will achieve in the forthcoming instalments.

Building Blocks
Having put our framework into place, we will now start to build the game itself. Solid, working logic is the
prime aim at this point, proving that our concept is workable and codable. To this end, we will not be adding
fantastic, well polished models, astounding sounds and out-of-this world effects. We will, however, bear in
mind that this is the ultimate goal, and will create the code in a way that allows us to replace simple building
blocks with the finished article at a later stage.

Rapid Development

As discussed in the competition programming article, building your game from the anchor-point of a menu is
good practise. We will use a simple graphic and add a couple of keyboard-driven options. In our well-
structured functionalised program, I know we already have a menu() function ready and waiting. In fact, it's
already working, it simply doesn't do anything visible or useful just yet.

Stage 4 shows our menu in place, albeit in draft form. Study this small piece of the game carefully, and you will
understand how the rest of the game will be built up part by part. The options are very easily added by
directing the program to the relevant function. Notice especially that our game is already working, and adding
the menu is so effortless. Our previous hard work is already paying off, and we are coding in self-contained
functions that are unaffected by anything else. Our top-down approach facilitates the calling of different parts
of the overall application, and simply continuing to run as normal on the return.

106
Option 1, playing the game, does nothing but redirect to the currently empty GamePlay() function and return
again. A small delay has been added for effect at this stage. The menu is simply but effectively excluded from
the screen, and included again afterwards. Option 2, ending the application, similarly calls the deinitialisation
function and exits in the simplest way possible.

Create and Destroy


Now we are really moving. It's time to create our game objects and place them on the screen. At the same
time, the code to destroy the objects will also be written. Doing these two tasks in parallel ensures the final
game is efficient and requires far less debugging! The functions GamePlayInit() and GamePlayDeinit() are
already in position and are already being called in the appropriate places in the GamePlay() function, as set out
in the original framework. We simply have to add the relevant code. Stage 5 does exactly this. Open the
provided example and compare the 2 functions, noticing that everything that is created is also destroyed. We
are using the data set up earlier in the invader arrays and player variables to quickly generate and position the
objects. Each object has also been introduced into the collision system.

Again, the program is simply ready to compile and run, with no additional effort. The most obvious
observation is that at the moment, we have only basic boxes representing the objects. Remember, our aim at
this stage is to prove everything fits into place. We could, if necessary, return now to the Initialisation function
and make adjustments to the positioning of the various entities quickly and with very little effort.

In just a few minutes, the world has been populated with the game objects.

Adding a state machine


The actual game itself is a collection of actions and reactions. Depending on the current "state", each action
will or will not happen. For example, when the player is alive, the movement action can happen; when the
player is dead, it can't. By creating what is known as a Finite State Machine, we can very simply implement a
structure where every action is called at the right time. At the end of the state (or cycle), the decision is made
to stay in the same state, or switch to another one, triggered by the events that have taken place. Some states
may last for some time, while others may come and go in an instant.

This system isn't as difficult to implement as it may seem; in fact there is a programmatic method for
implementing this method - the SELECT/CASE clauses. 8 states have been identified:

 Game Start

107
 Level Start
 Player Alive
 Player Dead
 Life End
 Life Begin
 Level End
 Game End

In each state, the function calls to the necessary actions are placed. For example, when the player is alive we
must checkKeys() for player input, and checkInvaderBullets() for a hit. We must also animateInvaders(), which
will also happen when the player is dead and also when the level is starting. MoveInvaders() is unique to this
state however. As you can see, we can quickly build up a picture of what needs to happen and when.

Stage 6, which you can open and review, has this arrangement added to the GamePlay() function. In order to
make this complete, a further level of functions have been added to our Top-Down design. The functions
aren't all populated with code just yet, but bringing the game to life will once again be simple and methodical.
The naming of the functions also make the flow easy to follow.

The moveInvaders() function is actually populated, to give you a feel for this process. You can follow the state
from the game starting and initialising a few variables, to the level starting, initialising more data and waiting a
few seconds to start the gameplay. Then, the "Alive" state is reached, the moveInvaders() function is triggered
and the game starts to move!

Right now, we have the simple beginnings of the game. The engine is moving from state to state. Actions,
although very sparse, are being triggered. Most importantly, this skeleton engine can now be easily populated
with more actions.

The Plan Ahead


Everything is now in place to build our game features, and to build them rapidly. In the next instalment, all the
various components, from movement, to collision detection, invader weaponry and sound effects will be
literally dropped into place. At each stage, we will be able to compile and immediately see the results, even
though the full game is not completed.

Big Changes, Fast!


One of the distinct advantages of putting in the effort to build a framework into which our game can be
dropped piece by piece, is the speed at which it comes together in the final stages. It can inspire new features
and functionality, simply because the process is now so simple!

Hopefully the pattern we are following is becoming clearer, and big changes will happen fast now. Work
through the following stages, and investigate the changes at each step. Each stage is much more densely
populated with actions, including acting on keypresses and firing at the invaders. Don't worry about the
intricacies of each action, but study how the state engine has been built up using functions, and especially how
each function is designed to act solely on one cycle of the program. The engine does the cycling, the actions
react to one moment in time. Note also that the results of actions change the state of the engine.

108
Stage 7 - The player movement keys are added in checkKeys(). The movement is just the small step the player
takes in one cycle. It is also immediately obvious that the base speed is too slow. It is quickly located in the
Init() function, where we set up our data, and remedied. The function also checks if the spacebar is pressed to
fire, and sets the bullet position and state accordingly. We now have moving invaders, and a moving player.
The bullet is initiated, but no more.

Stage 8 - Here, we have implemented a new function, checkBullets(). It checks the state of both player and
invader bullets, implements the collision with other valid objects, and triggers new invader bullets at random
intervals. It does not yet handle the destruction of entities. Again, this highlighted the need to increase the
invader bullet value in the Init() routine. It also displayed an unwanted feature, and that is the bullets passing
through lower invaders. This was quickly fixed by disabling the Z-depth of the bullets, effectively forcing them
to the front without having to make big changes in the positioning of objects in our game. Again, it is easily
located in the GameInit() function, where we create our 3D objects.

Stage 9 - The Mothership has been actioned by using a new function, checkMothership(). We create a random
factor in causing the mothership to appear, apply the movement (on a single cycle basis), and check when it
disappears. There's not much else to do, as the checkBullets() function actually determines the destruction of
the ship by the player. Again, the original estimate of speed was too low, and this has been adjusted in the
Init() routine. We will continue to fine-tune the characteristics of the game as we progress.

Given that the entire code to bring the ship to life is a mere 15 lines, this stage also implements the
objectsReposition() code. Although the primary role of this function is to put all the objects in their starting
positions, it also sets the states of the objects too, and their visibility. The animation of the invaders has also
been very simply added in the invadersAnimate() function.

Stage 10 - Now we have added the destruction of invaders, and the Mothership. This has necessitated an
additional variable to count the number of hits, and ensure we know when the full array of invaders have been
eliminated. This in turn will force a new state, to end the Level, and ultimately to start a new level. Destruction
of invaders occurs in the checkBullet() function, in the code we have already established. Now we have
additional information on the number of destroyed invaders, another traditional feature of Space Invaders can
be implemented in the moveInvaders() function. The speed can be increased as the number of successful hits
increases, and this has been done.

Stage 11 - Now is the time to add destruction of the player, again in the checkBullets() function. We have
already detected collision with the player, it just doesn't do anything yet. Once added, the progression of
states will expand to include the Dead state, and of course the game can also end. While we are adding this
essential operation, and it is necessary to add a variable to the Player type to register the number of lives left,

109
we'll also add a variable to record the score. We can register the score at each invader and Mothership hit. To
prove it is working, a simple text output to the screen is implemented for now.

A further modification that has come to light only now is that the 2 states Dead and End of Life are in fact a
duplication, and only complicate what is otherwise a much simpler process. So the End of Life state has been
removed. This is a prime example of the rapid development process, where the original design is fluid and
susceptible to change. It also accepts change quite willingly.

Stage 12 - We are getting close to a completed game. Models can now replace the placeholder primitive
objects, and unwanted items such as the collision boxes can be hidden. This is the point at which the game
visually changes into something worth presenting. In addition, the collision of invaders with the bases is added,
and this action ends the game by setting the player state to Dead, and reduces the lives to zero. This action
takes place in the invadersMove() function, the most appropriate place to test for the objects descending onto
the bases. To add interest to the player object, it now rotates as it moves. This is naturally located in the
checkKeys() function, where we test for and move the player in response to the user input.

Stage 13 - Now we will add the sound effects. This involves several areas:

 Constants - to define the sound numbers


 Loading and unloading the sounds in the GamePlayInit() and GamePlayDeinit() functions.
 Triggering the sounds at the different points - firing a new bullet (checkKeys()), hitting a target and
being hit (checkBullets()), and the appearance of the Mothership (checkMothership())

Stage 14 - This final step simply adds feedback in the form of the score and the number of remaining lives. As
this is a permanent feature of the in-game display, it is not placed in the SELECT clause, but just prior to the
screen update. Also added is the final score, before the game returns to the menu. Simple High Score
functionality retains the best attempt to date, and is loaded and saved as necessary, in the init() function and
the Game End state

Conclusion
Believe it or not, we have just completed our game! It may not be the most sophisticated version of Space
Invaders ever created, but there's nothing to stop it being updated, improved and finely polished. It's written
in a way that makes the maintenance simple, even as far as adding new in-game features by adding self-
contained functions. There's potential for explosions, particle effects and cut-scene sequences. You could add
power-ups and special features without too much difficulty, and by following the same process as we have
used so far.

110
Zork Tutorial - Part One

by Pluto

Creating the Map and Moving Around

Zork is perhaps the most famous of all text adventures. It has been the gold standard of the genre and highly
regarded. I spent countless hours playing this game and many more trying to figure out how to program it. In
this tutorial I am going to show you how to make your own zork game!

We are going to recreate a section of the original Zork, which will include the house and the areas just outside
of the house. You will learn how to handle objects, inventory, containers, and parse the commands. So, let's
get started!

The first step is to make the map that is the locations in the game. In the original Zork the player started out at
'West of House', so this is where we will start. Go to the link below and you will see a map of zork. This is the
reference we will use to create our map.

http://infocom.elsewhere.org/gallery/zork1_invisiclues/map-2-3.jpg

We are going to make seven of those locations. If you look at the zork map you can see where those locations
are at and get an idea of how they are situated. -Our first line of code will make an array with space for seven
location names:

DIM LOCATION$(7)

-Next we read in those names into an array:

FOR I = 1 TO 7
READ LOCATION$(I)
NEXT I

DATA "West of House"


DATA "North of House"
DATA "Behind House"
DATA "South of House"
DATA "Kitchen"
DATA "Living Room"
DATA "Attic"

The array above has assigned a number for each location. 1 = "west of house", 2 = "North of House" etc. We
know the player will start out at the "west of house" location which is location number 1. -So we need to make
a variable that represents the player's location and assign it the number 1.

PLR_LOC = 1: REM 1 Represents 'WEST OF HOUSE'

Now, if we were to print LOCATION$(PLR_LOC), we would see 'west of house'. To verify this just run the code
we have written so far:

111
REM Project: Zork Tutorial
REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM

REM LOCATIONS
DIM LOCATION$(7)

FOR I = 1 TO 7
READ LOCATION$(I)
NEXT I

DATA "West of House"


DATA "North of House"
DATA "Behind House"
DATA "South of House"
DATA "Kitchen"
DATA "Living Room"
DATA "Attic"

REM PLAYER STARTING LOCATION


PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE

PRINT LOCATION$(PLR_LOC)

REM MAIN GAME LOOP


DO

SYNC
LOOP
REM END MAIN LOOP

Change the PLR_LOC variable to any number between 1 and 7 and rerun the program and you will see the
location has changed accordingly.

Ok, we have our locations but how to get the player movement? First we have to map out which way the
player is allowed to move. For instance, the player cannot go east if he is at the 'west of house' location. How
does the program know this? Simple, you tell it!

One thing to remember about Zork, the original designers were not consistent with their directions. In the real
world, if you take five steps due north, you can take five steps due south to get back to where you started
from. This is not so in Zork. In many cases the player can go north but cannot go south to get back! It is
strange, but that's the land of Zork.

We will now define 6 directions of travel for each location on the map. The six directions are, north, south,
east, west, up and down.

Let us start with north.

We create a north array that will have a north number for each of the 7 map locations. This north number tells
us if the player is allowed to move north from that location.

112
DIM NORTH(7)

FOR I = 1 TO 7
READ NORTH(I)
NEXT I

DATA 2,0,2,0,0,0,0

So, if the player is at location 1, we can look up NORTH(1).


If NORTH(1) = 2, then that means the player is allowed to move north and the player's new location is 2, which
happens to be 'North of House'

NORTH(1) can equal any of the 7 map locations. It's just a number that tells us what the player's new location
is that he just moved to. If we assign a ZERO to NORTH(1) then the zero tells us that the player is not allowed
to go north while located at 'west of house'.

So let's see how this would work.

The player is at location 1 which is 'west of house'. He decides to go north. Now we look at the NORTH array to
see if he is allowed.

What is NORTH(PLR_LOC) equal to? Well, the PLR_LOC variable is equal to 1, and that is the player's location.
So, NORTH(PLR_LOC) is the same as NORTH(1). NORTH(1) is equal to what? What number did we assign
NORTH(1) in our array?

We assigned it the number 2. So, NORTH(1) is equal to 2. That means the player is allowed to move north and
we assign the player's new location as 2. Here is the code that does that:

PLR_LOC = NORTH(PLR_LOC)

Now that we know how to define the map movement, let us add the rest of the arrays for the other directions.

REM SOUTH PATHS FOR EACH LOCATION


DIM SOUTH(7)
FOR I = 1 TO 7
READ SOUTH(I)
NEXT I

DATA 4,0,4,0,0,0,0

REM EAST PATHS FOR EACH LOCATION


DIM EAST(7)
FOR I = 1 TO 7
READ EAST(I)
NEXT I

DATA 0,3,0,3,3,5,0

REM WEST PATHS FOR EACH LOCATION


DIM WEST(7)
FOR I = 1 TO 7
READ WEST(I)
NEXT I

DATA 0,1,5,1,6,0,0

113
REM UP PATHS FOR EACH LOCATION
DIM UP(7)
FOR I = 1 TO 7
READ UP(I)
NEXT I

DATA 0,0,0,0,7,0,0

REM DOWN PATHS FOR EACH LOCATION


DIM DOWN(7)
FOR I = 1 TO 7
READ DOWN(I)
NEXT I

DATA 0,0,0,0,0,0,5

The next step is to put this movement into our main loop. Before we enter the main game loop we should
display the player's position on the screen:

PRINT LOCATION$(PLR_LOC)

Now, let's code the main game loop.

REM MAIN GAME LOOP


DO

REM GET PLAYER COMMAND


INPUT "> ", CMD$

REM PROCESS COMMAND


GOSUB PARSE

SYNC
LOOP
REM END MAIN LOOP

The first step inside of the main loop is an INPUT statement that gets the player's instructions. The next step
calls a subroutine to parse those instructions, and do whatever the instructions tell us.

These two steps are the core of the game engine. Since we are working with map and movement in this part of
the tutorial, the commands will be kept simple. I will cover the parsing, more complicated command sentences
in a later part of the tutorial. For right now, the commands we will accept are 'n' - north, 's' - south, 'e' - east,
'w' - west, 'u' - up, 'd' - down, and 'l' - look.

Here is the PARSE subroutine. Look at the first condition, which is 'n'. If the player enters the letter n, the IF
statement checks the NORTH(PLR_LOC) variable. If it does NOT equal 0 then the player is allowed to move
north and we assign the new location number to the PLR_LOC variable and we set the PLR_MOVE variable to 1
which tells us that the player has moved. If the player cannot move north then the PLR_MOVE variable does
not get changed and remains equal to 0. Since PLR_MOVE = 0 we will return a message telling the player he
cannot go that direction.

114
REM PARSE PLAYER COMMAND
PARSE:
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF NORTH(PLR_LOC) <> 0
PLR_LOC = NORTH(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "s"
IF SOUTH(PLR_LOC) <> 0
PLR_LOC = SOUTH(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "e"
IF EAST(PLR_LOC) <> 0
PLR_LOC = EAST(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "w"
IF WEST(PLR_LOC) <> 0
PLR_LOC = WEST(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "u"
IF UP(PLR_LOC) <> 0
PLR_LOC = UP(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "d"
IF DOWN(PLR_LOC) <> 0
PLR_LOC = DOWN(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "l"
PLR_MOVE = 1
ENDCASE
ENDSELECT
IF PLR_MOVE = 1

REM DISPLAY NEW LOCATION


GOSUB CRNT_LOC

ELSE
REM NOT ALLOWED TO GO THAT DIRECTION
PRINT "You cannot go that way."
ENDIF
RETURN
REM END SUB

So, now you know how to code the map locations, and the player movement. I have included the full code for
this part of the tutorial. In this code you will notice that I have added location descriptions and a screen reset
counter inside of the main loop which clears the screen and brings the cursor back to the top of the screen
after a few command entries. This is not needed but helps keep the screen from getting too cluttered.

115
Remember, the best way to learn is to play with this code. Change some of the north numbers or other
directional numbers. You can make your own paths for the player if you choose to. Currently, this code follows
the same movement as the original Zork I game. In the next tutorial I will show you how to create objects and
player inventory.

Full Code for Part I:

REM Project: Zork Tutorial Part I


REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM

REM LOCATIONS
DIM LOCATION$(7)

FOR I = 1 TO 7
READ LOCATION$(I)
NEXT I

DATA "West of House"


DATA "North of House"
DATA "Behind House"
DATA "South of House"
DATA "Kitchen"
DATA "Living Room"
DATA "Attic"

REM PLAYER STARTING LOCATION


PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE

REM DIRECTIONS
REM NORTH PATHS FOR EACH LOCATION
DIM NORTH(7)
FOR I = 1 TO 7
READ NORTH(I)
NEXT I

DATA 2,0,2,0,0,0,0

REM SOUTH PATHS FOR EACH LOCATION


DIM SOUTH(7)
FOR I = 1 TO 7
READ SOUTH(I)
NEXT I

DATA 4,0,4,0,0,0,0

REM EAST PATHS FOR EACH LOCATION


DIM EAST(7)
FOR I = 1 TO 7
READ EAST(I)
NEXT I

DATA 0,3,0,3,3,5,0

REM WEST PATHS FOR EACH LOCATION


DIM WEST(7)
FOR I = 1 TO 7
READ WEST(I)

116
NEXT I

DATA 0,1,5,1,6,0,0

REM UP PATHS FOR EACH LOCATION


DIM UP(7)
FOR I = 1 TO 7
READ UP(I)
NEXT I

DATA 0,0,0,0,7,0,0

REM DOWN PATHS FOR EACH LOCATION


DIM DOWN(7)
FOR I = 1 TO 7
READ DOWN(I)
NEXT I

DATA 0,0,0,0,0,0,5

REM DISPLAY CURRENT LOCATION


GOSUB CRNT_LOC

REM DISPLAY CURRENT LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM MAIN GAME LOOP


DO

REM LINE SPACING


PRINT

REM GET PLAYER COMMAND


INPUT "> ", CMD$

REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS


CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF

REM PARSE COMMAND


GOSUB PARSE

REM LINE SPACING


PRINT

SYNC
LOOP
REM END MAIN LOOP

REM DISPLAY CURRENT LOCATION


CRNT_LOC:
PRINT LOCATION$(PLR_LOC)
RETURN
REM END GOSUB

REM DISPLAY CURRENT LOCATION

117
CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house,
with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no
door here, and all"
PRINT "the windows are boarded up. To the north a narrow path
winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the
forest to the east. In"
PRINT "one corner of the house there is a small window which is
slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no
door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to
have been used"
PRINT "recently for the preparation of food. A passage leads to
the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads
down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east,
a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be
nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading
down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB

REM PARSE PLAYER COMMAND


PARSE:
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF NORTH(PLR_LOC) <> 0
PLR_LOC = NORTH(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "s"
IF SOUTH(PLR_LOC) <> 0
PLR_LOC = SOUTH(PLR_LOC)
PLR_MOVE = 1

118
ENDIF
ENDCASE
CASE "e"
IF EAST(PLR_LOC) <> 0
PLR_LOC = EAST(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "w"
IF WEST(PLR_LOC) <> 0
PLR_LOC = WEST(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "u"
IF UP(PLR_LOC) <> 0
PLR_LOC = UP(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "d"
IF DOWN(PLR_LOC) <> 0
PLR_LOC = DOWN(PLR_LOC)
PLR_MOVE = 1
ENDIF
ENDCASE
CASE "l"
PLR_MOVE = 1
ENDCASE
ENDSELECT
IF PLR_MOVE = 1

REM DISPLAY NEW LOCATION

GOSUB CRNT_LOC

REM DISPLAY NEW LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

ELSE
REM NOT ALLOWED TO GO THAT DIRECTION
PRINT "You cannot go that way."
ENDIF
RETURN
REM END SUB

UPDATE I

There is a more convenient method of programming the map locations and directions. It would be nice if we
could group all the location data together. As IanM mentioned, it makes for less arrays and changing or adding
locations is more convenient. This is not easily done in older BASIC languages, but as it turns out there is a nice
way to do this in DBPro. You use typed arrays. Basically, what you will do is create your own type. The good
thing about this is that your type definition can be made up of several variables.

So here is what our type definition might look like:

119
type Loc
loc_name as string
north as integer
south as integer
east as integer
west as integer
up as integer
down as integer
endtype

The type we created is named Loc, and we can make an array of Locs, just like you would make an array of
integers or strings.

dim loc_array(7) as Loc

Next we read in all the grouped data:

for i = 1 to 7
read loc_array(i).loc_name
read loc_array(i).north
read loc_array(i).south
read loc_array(i).east
read loc_array(i).west
read loc_array(i).up
read loc_array(i).down
next i

data "West of House", 2,3,0,0,0,0


data "North of House", 0,2,0,7,0,0
data "East of House", 0,0,3,0,0,0
data "South of House", 2,2,0,0,0,0
data "Kitchen", 0,0,0,4,0,0
data "Trophy Room", 0,1,0,0,0,0
data "Attic", 0,0,0,0,0,5

Let's look at the first location of the data:

data "West of House", 2,3,0,0,0,0

This is location #1 so if we were to print the location name:

PRINT loc_array(1).loc_name

It would print 'West of House'. If we were to print the north number we would code:

PRINT loc_array(i).north

This would display the #2. Again, let's look at the data statement:

data "West of House", 2,3,0,0,0,0

Those numbers following 'West of House' represent north, south, east, west, up, and down.

Thus:

120
2,3,0,0,0,0 is north=2, south=3, east=0, west=0, up=0, down=0

So, at the 'west of house' location if the player goes north he will move to location #2. If player goes south he
will move to location #3. The player cannot go east, west, up or down because those directions are equal to 0.

Let's look at that data one more time. Each location has the location name and the directional numbers.

data "West of House", 2,3,0,0,0,0


data "North of House", 0,2,0,7,0,0
data "East of House", 0,0,3,0,0,0
data "South of House", 2,2,0,0,0,0
data "Kitchen", 0,0,0,4,0,0
data "Trophy Room", 0,1,0,0,0,0
data "Attic", 0,0,0,0,0,5

Now, all our location data is in one place so we don't have to scroll through too many arrays to make any
changes. Here is a small code snippet that you can run and it prints the location from the typed array we just
made. I will change the program code by replacing the simple arrays we used in the beginning with this typed
array.

type Loc
loc_name as string
north as float
south as float
east as float
west as float
up as float
down as float
endtype

dim loc_array(7) as Loc

for i = 1 to 7
read loc_array(i).loc_name
read loc_array(i).north
read loc_array(i).south
read loc_array(i).east
read loc_array(i).west
read loc_array(i).up
read loc_array(i).down
next i

data "West of House", 2,2,0,0,0,0


data "North of House", 0,2,0,7,0,0
data "East of House", 0,0,3,0,0,0
data "South of House", 2,2,0,0,0,0
data "Kitchen", 0,0,0,4,0,0
data "Trophy Room", 0,1,0,0,0,0
data "Attic", 0,0,0,0,0,5

print loc_array(1).loc_name
print loc_array(1).east

do

sync
loop

121
Of course, there are even more variations of this as well and I'm not going to go into everyone of them here. If
you have trouble with this then I suggest you stick with the simple array in the original tutorial until you get
more comfortable with arrays in general.

Another thing to mention is the number of locations. We know it is 7 but what if we add another location?
Then we have to find every array and change the number. Instead of doing that, create a constant variable
such as MAX_LOC = 7, and change array(7) to array(MAX_LOC). Now, anytime you want to add a location you
only have to change the MAX_LOC constant one time.

I will have more updates to this part I soon. I will add some more shine to it. Then we will move on to part II.

FINAL UPDATE TO PART I

You may have noticed while moving around in the game that when you try to go a direction that is off limits
our program displays the message 'You can't go that way." This is fine but you can make it more interesting if
you tell the player why he can't go that way. One message might be, 'The bridge is out.' In Zork you will find
that the game is sprinkled with such messages, so we will include those messages in our program.

If you'll recall, we used an array to look up the directional number. If it was equal to 0 then that told us that
direction was off limits. If it was a positive number then the player's location would change to that number, a
new location. We can expand on this idea and use negative numbers to represent different messages telling
the player he can't go that way. For instance:

0 = 'You can't go that way.'


-1 = 'The door is nailed shut.'
-2 = 'The gate is locked'
etc...

Zero is still the generic message of 'You can't go that way.' So, our array data may look something like this:

data 0,-2,2,0,3,1,4

or a typed array could be:

data "North of House", 0,-2,3,1,0,0

Any number 0 or less(negative numbers) means the player is not allowed to go that direction and the number
tells us what message to use. Any positive number means the player can move that direction and his position
(PLR_LOC) changes to that positive number.

Now that you know the idea behind this, let us put it into action. First let us declare an array for our messages:

REM PLAYER CAN'T GO THAT WAY MESSAGES


MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I

DATA "You can't go that way."


DATA "The door is boarded and you can't remove the boards."

122
DATA "The windows are all boarded."
DATA "The door is nailed shut."

The messages are the same as Zork I, around the house area.

Notice the messages will be represented in numeric order like this:

NOGO$(0) - "You can't go that way."


NOGO$(1) - "The door is boarded and you can't remove the boards."
NOGO$(2) - "The windows are all boarded."
NOGO$(3) - "The door is nailed shut."

These are our 4 messages.

We use the number 0 and negative numbers to identify them.

0, -1, -2, -3

We have to change these to positive numbers after we get them so that we can match them up to the NOGO$
messages.

For instance:

Change -3 to 3 so that we can put it in the NOGO$ array like this:

PRINT NOGO$(3)

This would display:

'The door is nailed shut.'

What we can do is use the ABS function which gives us the absolute value of the number and then match that
up with our message array. So, ABS(-3) is the same as 3.

Now that we have a positive number we can use it on our message array to print the matching message.

First we use the ABS function:

So, if NORTH(1) = -3

MESSAGE_INDEX = ABS( NORTH(1) )

Same as:

MESSAGE_INDEX = 3

Now, we can use it as our index for the message array NOGO$

PRINT NOGO$( MESSAGE_INDEX )

123
Is the same as:

PRINT NOGO$( 3 )

Remember what message NOGO$(3) is? Let's list them:

NOGO$(0) - "You can't go that way."


NOGO$(1) - "The door is boarded and you can't remove the boards."
NOGO$(2) - "The windows are all boarded."
NOGO$(3) - "The door is nailed shut."

This concludes this part of the tutorial, as I will not make any new updates for this first part. If you are having
trouble with this part of it don't worry, you can always just use the standard message in the original program.
Of course you may think of other ways to make them yourself if you choose.

I have added the final program, complete for PART I of the tutorial. It includes the message codes and I have
replaced the simple location arrays with typed arrays. I have included lots of comments to help make it clear.
You will notice that the code has not changed that much, so you know most of it already and the rest of it is
pretty simple.

REM Project: Zork Tutorial


REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM

REM MAXIMUM NUMBER OF LOCATIONS


MAX_LOC = 7

REM CREATE A TYPE DEFINITION FOR THE LOCATION VARIABLES


type LOC
LOCATION as string
NORTH as integer
SOUTH as integer
EAST as integer
WEST as integer
UP as integer
DOWN as integer
endtype

REM CREATE AN ARRAY OF LOCATIONS


dim LOC_ARRAY(MAX_LOC) as LOC

REM POPULATE THE ARRAY WITH MAP LOCATION DATA


for i = 1 to MAX_LOC
read loc_array(i).LOCATION
read loc_array(i).NORTH
read loc_array(i).SOUTH
read loc_array(i).EAST
read loc_array(i).WEST
read loc_array(i).UP
read loc_array(i).DOWN
next i

REM LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN


data "West of House", 2,4,-1,0,0,0

124
data "North of House", 0,-2,3,1,0,0
data "East of House", 2,4,0,5,0,0
data "South of House", -2,0,3,1,0,0
data "Kitchen", 0,0,3,6,7,0
data "Living Room", 0,0,5,-3,0,0
data "Attic", 0,0,0,0,0,5

REM PLAYER STARTING LOCATION


PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE

REM PLAYER CAN'T GO THAT WAY MESSAGES


MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I

REM MESSAGES 0 - 3
DATA "You can't go that way."
DATA "The door is boarded and you can't remove the boards."
DATA "The windows are all boarded."
DATA "The door is nailed shut."

REM DISPLAY CURRENT LOCATION


GOSUB CRNT_LOC

REM DISPLAY CURRENT LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM MAIN GAME LOOP


DO

REM LINE SPACING


PRINT

REM GET PLAYER COMMAND


INPUT "> ", CMD$

REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS


CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF

REM PROCESS COMMAND


GOSUB PARSE

REM LINE SPACING


PRINT

SYNC
LOOP
REM END MAIN LOOP

REM DISPLAY CURRENT LOCATION


CRNT_LOC:
PRINT LOC_ARRAY(PLR_LOC).LOCATION
RETURN
REM END GOSUB

125
REM DISPLAY CURRENT LOCATION
CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house,
with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no
door here, and all"
PRINT "the windows are boarded up. To the north a narrow path
winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the
forest to the east. In"
PRINT "one corner of the house there is a small window which is
slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no
door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to
have been used"
PRINT "recently for the preparation of food. A passage leads to
the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads
down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east,
a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be
nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading
down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB

REM PARSE PLAYER COMMAND


PARSE:
MOV_MSG = 0
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF LOC_ARRAY(PLR_LOC).NORTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).NORTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).NORTH)
ENDIF
ENDCASE

126
CASE "s"
IF LOC_ARRAY(PLR_LOC).SOUTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).SOUTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).SOUTH)
ENDIF
ENDCASE
CASE "e"
IF LOC_ARRAY(PLR_LOC).EAST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).EAST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).EAST)
ENDIF
ENDCASE
CASE "w"
IF LOC_ARRAY(PLR_LOC).WEST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).WEST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).WEST)
ENDIF
ENDCASE
CASE "u"
IF LOC_ARRAY(PLR_LOC).UP > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).UP
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).UP)
ENDIF
ENDCASE
CASE "d"
IF LOC_ARRAY(PLR_LOC).DOWN > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).DOWN
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).DOWN)
ENDIF
ENDCASE
CASE "l"
PLR_MOVE = 1
ENDCASE
ENDSELECT
IF PLR_MOVE = 1

REM DISPLAY NEW LOCATION


GOSUB CRNT_LOC

REM DISPLAY NEW LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC
ELSE

REM DISPLAY APPROPRIATE MESSAGE THAT PLAYER CANNOT GO THAT DIRECTION


PRINT NOGO$(MOV_MSG)

ENDIF
RETURN
REM END SUB

127
Zork Tutorial - Part II - OBJECTS AND INVENTORY

In the first tutorial we created the game world with its locations, movement and a simple command parser.

Now we will learn how to create objects and place them in the game. We will also make the player's inventory.

For now, we will only make 2 objects, the mailbox and the leaflet located at the 'west of house' location. We
will add the rest at a later time.

Objects, like a mailbox have attributes, information about them. For instance, the location of the object and
the name of the object are all attributes. There are many attributes that an object can have. Some objects can
be opened like the mailbox while others cannot. Some objects can be taken by the player, while others cannot.
We need to give each object certain attributes that we will include in the game.

For now, we will start with just 2 attributes and add others later. These two attributes are the object's name
and its location.

First we tell the program how many objects there are in the game.
let's make a variable called OBJ_MAX. This is equal to the number of objects in the game, which we will set to
2 for now.

OBJ_MAX = 2

Next, we will make an array type for the objects.

First we create the array definition:

REM CREATE A TYPE DEFINITION FOR OBJECTS


type OBJ
NAME as string
LOCATION as integer
endtype

So, we created a new type and called it OBJ. It has two variables, NAME and LOCATION. Those are the object
attributes. Next we create an array that will contain each object and it's attributes.

DIM OBJ_ARRAY(MAX_OBJ) as OBJ

Now, let's read in the attribute information into the array:

REM POPULATE THE ARRAY WITH OBJECT DATA


FOR I = 1 to MAX_OBJ
READ OBJ_ARRAY(I).NAME
READ OBJ_ARRAY(I).LOCATION
NEXT I

DATA "small mailbox", 1


DATA "leaflet", 2

As you can see, it is fairly simple. There is the name of the object followed by its location. So, object #1 is the
'small mailbox' and it is located at location #1 which is the starting position 'west of house'

128
You will notice that I placed the leaflet at a different location, #2 which is 'north of house'

To display each object we would code a print statement with the array name. For instance, to display the name
of the first object, the 'small mailbox' we would code this:

PRINT OBJ_ARRAY(1).NAME

To display the location number of the leaflet we would code:

PRINT OBJ_ARRAY(2).LOCATION

Let's try it. Here is the code to print object information (attributes). Go ahead and run this to see it in action:

REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME


MAX_OBJ = 2

REM CREATE A TYPE DEFINITION FOR OBJECTS


type OBJ
NAME as string
LOCATION as integer
endtype

REM CREATE AN ARRAY OF OBJECTS


dim OBJ_ARRAY(MAX_OBJ) as OBJ

REM POPULATE THE ARRAY WITH OBJECT DATA


for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
next i

REM NAME LOCATION


data "small mailbox", 1 : rem Located at west of house
data "leaflet", 2 : rem not on the map yet (hidden inside mailbox

PRINT OBJ_ARRAY(1).NAME
PRINT OBJ_ARRAY(2).LOCATION

do

sync
loop

Now that we have the objects defined we know how to display them we will go ahead and put that code in our
game.
We will display the name of the object right after the location and the description. So it will look something
like this:

West of House
You are standing in an open field west of a white house, with a boarded front door.

There is a small mailbox here.

To display the objects at the player's location we need to search through our object list to see if the object's
location number is the same as the player's location number. We will create a for-next loop which will look at

129
each object one at a time starting with object #1. If the location of the object = location of player we will
display the name of the object. Thus:

IF OBJ_ARRAY(1).LOCATION = PLR_LOC

Next, print the name of the object:

PRINT OBJ_ARRAY(1).NAME

Instead of just displaying the name of the object we can spice it up a bit. For instance, in Zork I, it usually says:

'There is a small mailbox here.'

So, we will add those words too:

PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."

Now, here is the FOR-NEXT loop that checks each object and display's it if it is at the player's location:

REM LIST ANY OBJECTS AT PLAYER LOCATION


FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).LOCATION = PLR_LOC
PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."
ENDIF
NEXT I

I have added all the above code into the game. Take a look at it and you will see it is a simple addition.
Run this code and you will see the mailbox at the starting location. If you move north, you will see the leaflet.
We will discuss player inventory in the next update. Have fun.

REM Project: Zork Tutorial


REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM

REM MAXIMUM NUMBER OF LOCATIONS


MAX_LOC = 7

REM CREATE A TYPE DEFINITION FOR THE LOCATION VARIABLES


type LOC
LOCATION as string
NORTH as integer
SOUTH as integer
EAST as integer
WEST as integer
UP as integer
DOWN as integer
endtype

REM CREATE AN ARRAY OF LOCATIONS


dim LOC_ARRAY(MAX_LOC) as LOC

130
REM POPULATE THE ARRAY WITH MAP LOCATION DATA
for i = 1 to MAX_LOC
read LOC_ARRAY(i).LOCATION
read LOC_ARRAY(i).NORTH
read LOC_ARRAY(i).SOUTH
read LOC_ARRAY(i).EAST
read LOC_ARRAY(i).WEST
read LOC_ARRAY(i).UP
read LOC_ARRAY(i).DOWN
next i

REM LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN


data "West of House", 2,4,-1,0,0,0
data "North of House", 0,-2,3,1,0,0
data "East of House", 2,4,0,5,0,0
data "South of House", -2,0,3,1,0,0
data "Kitchen", 0,0,3,6,7,0
data "Living Room", 0,0,5,-3,0,0
data "Attic", 0,0,0,0,0,5

REM OBJECTS

REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME


MAX_OBJ = 2

REM CREATE A TYPE DEFINITION FOR OBJECTS


type OBJ
NAME as string
LOCATION as integer
endtype

REM CREATE AN ARRAY OF OBJECTS


dim OBJ_ARRAY(MAX_OBJ) as OBJ

REM POPULATE THE ARRAY WITH OBJECT DATA


for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
next i

REM NAME LOCATION


data "small mailbox", 1 : rem Located at west of house
data "leaflet", 2 : rem not on the map yet (hidden inside mailbox

REM PLAYER STARTING LOCATION


PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE

REM PLAYER CAN'T GO THAT WAY MESSAGES


MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I

REM MESSAGES 0 - 3
DATA "You can't go that way."
DATA "The door is boarded and you can't remove the boards."
DATA "The windows are all boarded."
DATA "The door is nailed shut."

131
REM DISPLAY CURRENT LOCATION
GOSUB CRNT_LOC

REM DISPLAY CURRENT LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM LINE SPACING


PRINT

REM DISPLAY OBJECTS AT PLAYER LOCATION


GOSUB DISPLAY_OBJECTS

REM MAIN GAME LOOP


DO

REM LINE SPACING


PRINT

REM GET PLAYER COMMAND


INPUT "> ", CMD$

REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS


CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF

REM PROCESS COMMAND


GOSUB PARSE

REM LINE SPACING


PRINT

SYNC
LOOP
REM END MAIN LOOP

REM DISPLAY CURRENT LOCATION


CRNT_LOC:
PRINT LOC_ARRAY(PLR_LOC).LOCATION
RETURN
REM END GOSUB

REM DISPLAY CURRENT LOCATION


CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house,
with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no
door here, and all"
PRINT "the windows are boarded up. To the north a narrow path
winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the
forest to the east. In"

132
PRINT "one corner of the house there is a small window which is
slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no
door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to
have been used"
PRINT "recently for the preparation of food. A passage leads to
the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads
down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east,
a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be
nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading
down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB

REM DISPLAY OBJECTS


DISPLAY_OBJECTS:

REM LIST ANY OBJECTS AT PLAYER LOCATION


FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).LOCATION = PLR_LOC
PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."
ENDIF
NEXT I
RETURN
REM END SUB

REM PARSE PLAYER COMMAND


PARSE:
MOV_MSG = 0
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF LOC_ARRAY(PLR_LOC).NORTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).NORTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).NORTH)
ENDIF
ENDCASE
CASE "s"
IF LOC_ARRAY(PLR_LOC).SOUTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).SOUTH

133
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).SOUTH)
ENDIF
ENDCASE
CASE "e"
IF LOC_ARRAY(PLR_LOC).EAST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).EAST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).EAST)
ENDIF
ENDCASE
CASE "w"
IF LOC_ARRAY(PLR_LOC).WEST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).WEST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).WEST)
ENDIF
ENDCASE
CASE "u"
IF LOC_ARRAY(PLR_LOC).UP > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).UP
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).UP)
ENDIF
ENDCASE
CASE "d"
IF LOC_ARRAY(PLR_LOC).DOWN > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).DOWN
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).DOWN)
ENDIF
ENDCASE
CASE "l"
PLR_MOVE = 1
ENDCASE
ENDSELECT
IF PLR_MOVE = 1

REM DISPLAY NEW LOCATION


GOSUB CRNT_LOC

REM DISPLAY NEW LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM LINE SPACING


PRINT

REM DISPLAY OBJECTS AT PLAYER LOCATION


GOSUB DISPLAY_OBJECTS

ELSE

REM DISPLAY APPROPRIATE MESSAGE THAT PLAYER CANNOT GO THAT DIRECTION


PRINT NOGO$(MOV_MSG)

ENDIF

134
RETURN
REM END SUB

UPDATE I

INVENTORY

As you learned earlier, objects have attributes such as what their name is and where they are located. We will
add another attribute to our objects. This attribute lets us know if the object is with the player. The number 0
tells us that the object is not with the player. The number 1 tells us that the player has the object in his
inventory. So, let's add that to the object array that we created earlier.

Here is the original object type:

type OBJ
NAME as string
LOCATION as integer
endtype

Now we will add the third attribute and call it INVENTORY.

Add it to the OBJ type:

type OBJ
NAME as string
LOCATION as integer
INVENTORY as integer
endtype

Then add it here where we read in the object data:

for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
read OBJ_ARRAY(i).INVENTORY
next i

Now we need to add either a 1 or a 0 for each object, a 0 if object is not with player and a 1 if object is.
For the mailbox we will assign it a 0 and the leaflet a 1, so the player will be carrying the leaflet. Here is the
original object data, with NAME and LOCATION attribute.

REM NAME, LOCATION


data "small mailbox", 1
data "leaflet", 0

Now, here it is after we add the third attribute of INVENTORY.

REM NAME, LOCATION, INVENTORY


data "small mailbox", 1, 0
data "leaflet", 0, 1

135
And here is the complete object array that we created:

REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME


MAX_OBJ = 2

REM CREATE A TYPE DEFINITION FOR OBJECTS


type OBJ
NAME as string
LOCATION as integer
INVENTORY as integer
endtype

REM CREATE AN ARRAY OF OBJECTS


dim OBJ_ARRAY(MAX_OBJ) as OBJ

REM POPULATE THE ARRAY WITH OBJECT DATA


for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
read OBJ_ARRAY(i).INVENTORY
next i

REM NAME, LOCATION, INVENTORY


data "small mailbox", 1, 0
data "leaflet", 0, 1

We still only have two objects but now each object has three attributes. You can create any attribute like this,
it's that easy. For instance, if you wanted to add weight to objects you could make another number which
would represent how much the object weighs. You could call it the WEIGHT attribute.

Now that we have added the INVENTORY attribute we will add a new command to the parser. Whenever the
player enters the letter 'i' we will display whatever he is carrying in his inventory. Of course we will check each
object and see if it's INVENTORY variable is = 1 and if it is then we list that on the screen as
the player's inventory. So we would code:

IF OBJ_ARRAY(I).INVENTORY = 1

Then

PRINT OBJ_ARRAY(I).NAME

If 1 then print object name, pretty simple. But what if the player is carrying a bunch of objects? What we need
to do is make our trusty FOR-NEXT loop and go through each object one at a time and check each INVENTORY
attribute. Like this:

FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).INVENTORY = 1
PRINT OBJ_ARRAY(I).NAME
ENDIF
NEXT I

And if the player is not carrying anything? In that case we will reply with the standard Zork reply:

136
'You are empty-handed.'

To do this we can make a counter variable named INV_CNT. Each time we find an object in the player's
inventory we simply add 1 to the INV_CNT.

When we find the very first object that's in the player's inventory we can display the message:

'You are carrying:'

We print that message just once, only starting with the first object. Once we print that message then we print
the name of the object. The remaining objects in the inventory will be printed below that, one at a time. So, it
would look like this:

You are carrying:


leaflet
sword
bottle

So, let's look at the code:

First check the object to see if it's in player inventory:

IF OBJ_ARRAY(I).INVENTORY = 1

If it is, then increment the INV_CNT (add 1 to INV_CNT)

INC INV_CNT

Next, check to see if this is the first object and display the 'You are carrying:' message. We will know if it's the
first object because the INV_CNT will equal 1.

IF INV_CNT = 1 THEN PRINT "You are carrying:"

Next display the name of the object.

PRINT OBJ_ARRAY(I).NAME

After we finish checking every object and we don't find any in the inventory then we display the 'You are
empty handed.' message. If the INV_CNT is 0 then there is nothing in inventory, so that's the variable we need
to check:

IF INV_CNT = 0
PRINT "You are empty-handed."

Now, we put all of that code together and it looks like this:

REM INVENTORY
CASE "i"
INV_CNT = 0
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).INVENTORY = 1

137
INC INV_CNT
IF INV_CNT = 1 THEN PRINT "You are carrying:"
PRINT OBJ_ARRAY(I).NAME
ENDIF
NEXT I
IF INV_CNT = 0
PRINT "You are empty-handed."
ENDIF
RETURN
ENDCASE

Now, I'm going to add a simple TAKE and DROP command to the parser. You will only be able to take and drop
the leaflet for now.

In the latter part of the tutorial I will go more indepth on the parser and we will learn how to make a Zork
parser, but that will have to wait till later. Once we have a working Zork parser we will be able to add the full
TAKE and DROP commands for all objects. What you learn here will put to use when we get the full parser
going.

So what do we have to change when the player takes the leaflet?

Well, we need to change the leaflet INVENTORY attribute from 0 to 1, because the 1 means the player has the
object. But we also need to remove the object from the ground at wherever the player is located. So we have
to change the object's LOCATION attribute from a 1 to a 0.

The leaflet object is object #2. So we need to code:

OBJ_ARRAY(2).INVENTORY = 1 i.e - in player's inventory now

OBJ_ARRAY(2).LOCATION = 0 i.e. - no longer on ground at location

Before we do that we should check to see if the object is at the player's current location, because if the object
is in the kitchen and the player is outside the house we don't want him to be able to take it! So, we code:

IF OBJ_ARRAY(2).LOCATION = PLR_LOC

The simple parser code would look something like this:

CASE "take leaflet"


IF OBJ_ARRAY(2).LOCATION = PLR_LOC
OBJ_ARRAY(2).INVENTORY = 1
OBJ_ARRAY(2).LOCATION = 0
PRINT "Taken."
ELSE
PRINT "You can't see any leaflet here!"
ENDIF
RETURN
ENDCASE

Now, to drop the leaflet you just reverse this and check to see if the player has the object because you don't
want him dropping something he doesn't have.

Here is the full code of everything we have covered so far. Run this code and go north and take the leaflet.

138
Now look at your inventory 'i' command and you will see it. Try dropping it somewhere and enter the look
command 'l' to see if the leaflet you dropped is there.

REM Project: Zork Tutorial


REM Created: 5/19/2008 7:37:05 PM
REM
REM ***** Main Source File *****
REM

REM MAXIMUM NUMBER OF LOCATIONS


MAX_LOC = 7

REM CREATE A TYPE DEFINITION FOR THE LOCATION VARIABLES


type LOC
LOCATION as string
NORTH as integer
SOUTH as integer
EAST as integer
WEST as integer
UP as integer
DOWN as integer
endtype

REM CREATE AN ARRAY OF LOCATIONS


dim LOC_ARRAY(MAX_LOC) as LOC

REM POPULATE THE ARRAY WITH MAP LOCATION DATA


for i = 1 to MAX_LOC
read LOC_ARRAY(i).LOCATION
read LOC_ARRAY(i).NORTH
read LOC_ARRAY(i).SOUTH
read LOC_ARRAY(i).EAST
read LOC_ARRAY(i).WEST
read LOC_ARRAY(i).UP
read LOC_ARRAY(i).DOWN
next i

REM LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN


data "West of House", 2,4,-1,0,0,0
data "North of House", 0,-2,3,1,0,0
data "East of House", 2,4,0,5,0,0
data "South of House", -2,0,3,1,0,0
data "Kitchen", 0,0,3,6,7,0
data "Living Room", 0,0,5,-3,0,0
data "Attic", 0,0,0,0,0,5

REM OBJECTS

REM DECLARE MAXIMUM NUMBER OF OBJECTS IN GAME


MAX_OBJ = 2

REM CREATE A TYPE DEFINITION FOR OBJECTS


type OBJ
NAME as string
LOCATION as integer
INVENTORY as integer
endtype

REM CREATE AN ARRAY OF OBJECTS


dim OBJ_ARRAY(MAX_OBJ) as OBJ

139
REM POPULATE THE ARRAY WITH OBJECT DATA
for i = 1 to MAX_OBJ
read OBJ_ARRAY(i).NAME
read OBJ_ARRAY(i).LOCATION
read OBJ_ARRAY(i).INVENTORY
next i

REM NAME, LOCATION, INVENTORY


data "small mailbox", 1, 0
data "leaflet", 2, 0

REM PLAYER STARTING LOCATION


PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE

REM PLAYER CAN'T GO THAT WAY MESSAGES


MAX_NOGO = 3
DIM NOGO$(MAX_NOGO)
FOR I = 0 TO MAX_NOGO
READ NOGO$(I)
NEXT I

REM MESSAGES 0 - 3
DATA "You can't go that way."
DATA "The door is boarded and you can't remove the boards."
DATA "The windows are all boarded."
DATA "The door is nailed shut."

REM DISPLAY CURRENT LOCATION


GOSUB CRNT_LOC

REM DISPLAY CURRENT LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM LINE SPACING


PRINT

REM DISPLAY OBJECTS AT PLAYER LOCATION


GOSUB DISPLAY_OBJECTS

REM MAIN GAME LOOP


DO

REM LINE SPACING


PRINT

REM GET PLAYER COMMAND


INPUT "> ", CMD$

REM RESET TO TOP OF SCREEN AFTER FOUR COMMANDS


CLS_CNT = CLS_CNT + 1
IF CLS_CNT > 3
CLS_CNT = 0
CLS
ENDIF

REM PROCESS COMMAND


GOSUB PARSE

REM LINE SPACING


PRINT

140
SYNC
LOOP
REM END MAIN LOOP

REM DISPLAY CURRENT LOCATION


CRNT_LOC:
PRINT LOC_ARRAY(PLR_LOC).LOCATION
RETURN
REM END GOSUB

REM DISPLAY CURRENT LOCATION


CRNT_LOC_DESC:
SELECT PLR_LOC
CASE 1
PRINT "You are standing in an open field west of a white house,
with a boarded front door."
ENDCASE
CASE 2
PRINT "You are facing the north side of a white house. There is no
door here, and all"
PRINT "the windows are boarded up. To the north a narrow path
winds through the trees."
ENDCASE
CASE 3
PRINT "You are behind the white house. A path leads into the
forest to the east. In"
PRINT "one corner of the house there is a small window which is
slightly ajar."
ENDCASE
CASE 4
PRINT "You are facing the south side of a white house. There is no
door here, and all"
PRINT "the windows are boarded."
ENDCASE
CASE 5
PRINT "You are in the kitchen of the white house. A table seems to
have been used"
PRINT "recently for the preparation of food. A passage leads to
the west and a dark"
PRINT "staircase can be seen leading upward. A dark chimney leads
down and to the east"
PRINT "is a small window which is open."
ENDCASE
CASE 6
PRINT "You are in the living room. There is a doorway to the east,
a wooden door with"
PRINT "strange gothic lettering to the west, which appears to be
nailed shut, a trophy"
PRINT "case, and a large oriental rug in the center of the room."
ENDCASE
CASE 7
PRINT "This is the attic. The only exit is a stairway leading
down."
ENDCASE
ENDSELECT
RETURN
REM END GOSUB

REM DISPLAY OBJECTS


DISPLAY_OBJECTS:

141
REM LIST ANY OBJECTS AT PLAYER LOCATION
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).LOCATION = PLR_LOC
PRINT "There is a " + OBJ_ARRAY(I).NAME + " here."
ENDIF
NEXT I
RETURN
REM END SUB

REM PARSE PLAYER COMMAND


PARSE:
MOV_MSG = 0
PLR_MOVE = 0
SELECT CMD$
CASE "n"
IF LOC_ARRAY(PLR_LOC).NORTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).NORTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).NORTH)
ENDIF
ENDCASE
CASE "s"
IF LOC_ARRAY(PLR_LOC).SOUTH > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).SOUTH
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).SOUTH)
ENDIF
ENDCASE
CASE "e"
IF LOC_ARRAY(PLR_LOC).EAST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).EAST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).EAST)
ENDIF
ENDCASE
CASE "w"
IF LOC_ARRAY(PLR_LOC).WEST > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).WEST
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).WEST)
ENDIF
ENDCASE
CASE "u"
IF LOC_ARRAY(PLR_LOC).UP > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).UP
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).UP)
ENDIF
ENDCASE
CASE "d"
IF LOC_ARRAY(PLR_LOC).DOWN > 0
PLR_LOC = LOC_ARRAY(PLR_LOC).DOWN
PLR_MOVE = 1
ELSE
MOV_MSG = ABS(LOC_ARRAY(PLR_LOC).DOWN)
ENDIF

142
ENDCASE
REM LOOK
CASE "l"
PLR_MOVE = 1
ENDCASE
REM INVENTORY
CASE "i"
INV_CNT = 0
FOR I = 1 TO MAX_OBJ
IF OBJ_ARRAY(I).INVENTORY = 1
INC INV_CNT
IF INV_CNT = 1 THEN PRINT "You are carrying:"
PRINT OBJ_ARRAY(I).NAME
ENDIF
NEXT I
IF INV_CNT = 0
PRINT "You are empty-handed."
ENDIF
RETURN
ENDCASE

CASE "take leaflet"


IF OBJ_ARRAY(2).LOCATION = PLR_LOC
OBJ_ARRAY(2).INVENTORY = 1
OBJ_ARRAY(2).LOCATION = 0
PRINT "Taken."
ELSE
PRINT "You can't see any leaflet here!"
ENDIF
RETURN
ENDCASE

CASE "drop leaflet"


IF OBJ_ARRAY(2).INVENTORY = 1
OBJ_ARRAY(2).INVENTORY = 0
OBJ_ARRAY(2).LOCATION = PLR_LOC
PRINT "Dropped."
ELSE
PRINT "You don't have any leaflet!"
ENDIF
RETURN
ENDCASE

ENDSELECT
IF PLR_MOVE = 1

REM DISPLAY NEW LOCATION


GOSUB CRNT_LOC

REM DISPLAY NEW LOCATION DESCRIPTION


GOSUB CRNT_LOC_DESC

REM LINE SPACING


PRINT

REM DISPLAY OBJECTS AT PLAYER LOCATION


GOSUB DISPLAY_OBJECTS

ELSE

REM DISPLAY APPROPRIATE MESSAGE THAT PLAYER CANNOT GO THAT DIRECTION

143
PRINT NOGO$(MOV_MSG)

ENDIF
RETURN
REM END SUB

We are not finished with objects and managing objects but this is a good start. We have to learn the Zork
parser now, and that will be the subject of the next part of the tutorial. For now, play with the code, add a new
object or even a new location if you like.

144

You might also like