DBPro Guide Beginners Guide to DarkBasic Pro Nickydude
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
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.
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
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â€Ś
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!
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. 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! 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:
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.
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:
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.
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:
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...
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 (>=)
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:
1. 2. 3.
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." When the user guesses correctly the program tells them how many guesses they took to do it. 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.
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$;"!!"
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."
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
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.
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.
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.
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
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).
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!
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.
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):
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
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.
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.
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?
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
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:
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.
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!
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 coordinates 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.
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 Text 100,100,"Text produced with the Set Text Transparent Print "2. This is text produced with Text 100,120,"Text produced with the
the Print command" Text command (Opaque)" the Print command" 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...
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!
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":
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":
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
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.
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:
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:
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.
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.
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
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 If I$="2" Then Print "You selected menu If I$="3" Then Print "You selected menu If I$="4" Then Print "You selected menu If I$="5" Then Print "You selected menu If I$="6" Then Print "You selected menu
option option option option option option
1" 2" 3" 4" 5" 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
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.
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"
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.
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 Write Write Write Write Write
String String String String String String
1,"This is the header" 1,Str$(MatrixWidth) 1,Str$(MatrixHeight) 1,Str$(TilesX) 1,Str$(TilesZ) 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:
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
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.
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.
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
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!
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
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:
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.
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.
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.
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.
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!
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")
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 recode 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!
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$():
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:
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 noone 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 Print Print Print Print
Mailbox1$ Mailbox2$ Mailbox3$ Mailbox4$ 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.
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.
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()
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.
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,
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.
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.
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.
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
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.
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
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().
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.
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:
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:
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.
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.
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
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 onscreen 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
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!
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
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
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
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.
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.
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...
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.
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
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.
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. 2. 3. 4. 5. 6. 7.
Place the bullet in front of the gun barrel Calculate the next position of the bullet Replace the background for the old bullet position Place the bullet in the new position Check to see if hit the enemy or left the screen If 'no' to both questions at number 5, repeat from number 2 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
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?
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.
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
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)
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)!
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. 2.
Using Sprites 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) 2) 3) 4)
Load the font file into DBP Cut the fonts into single character images Trim the wasted space to the left and right of each character. 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.
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
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.
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 chitchat, 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.
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 fullyfledged 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.
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:
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.
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 wellstructured 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.
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: ď‚ˇ
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.
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,
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 selfcontained 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.
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 DATA DATA DATA DATA DATA DATA
"West of House" "North of House" "Behind House" "South of House" "Kitchen" "Living Room" "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:
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 DATA DATA DATA DATA DATA DATA
"West of House" "North of House" "Behind House" "South of House" "Kitchen" "Living Room" "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.
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
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.
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.
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 DATA DATA DATA DATA DATA DATA
"West of House" "North of House" "Behind House" "South of House" "Kitchen" "Living Room" "Attic"
REM PLAYER STARTING LOCATION PLR_LOC = 1; REM LOCATION NUMBER 1 = WEST OF HOUSE REM REM DIM FOR
DIRECTIONS NORTH PATHS FOR EACH LOCATION NORTH(7) 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)
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
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
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:
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 data data data data data data
"West of House", 2,3,0,0,0,0 "North of House", 0,2,0,7,0,0 "East of House", 0,0,3,0,0,0 "South of House", 2,2,0,0,0,0 "Kitchen", 0,0,0,4,0,0 "Trophy Room", 0,1,0,0,0,0 "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:
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 data data data data data data
"West of House", 2,3,0,0,0,0 "North of House", 0,2,0,7,0,0 "East of House", 0,0,3,0,0,0 "South of House", 2,2,0,0,0,0 "Kitchen", 0,0,0,4,0,0 "Trophy Room", 0,1,0,0,0,0 "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 = read read read read read read read next i data data data data data data data
1 to 7 loc_array(i).loc_name loc_array(i).north loc_array(i).south loc_array(i).east loc_array(i).west loc_array(i).up loc_array(i).down
"West of House", 2,2,0,0,0,0 "North of House", 0,2,0,7,0,0 "East of House", 0,0,3,0,0,0 "South of House", 2,2,0,0,0,0 "Kitchen", 0,0,0,4,0,0 "Trophy Room", 0,1,0,0,0,0 "Attic", 0,0,0,0,0,5
print loc_array(1).loc_name print loc_array(1).east do sync loop
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."
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) NOGO$(1) NOGO$(2) NOGO$(3)
"You "The "The "The
can't go that way." door is boarded and you can't remove the boards." windows are all boarded." 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 )
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
data data data data data data
"North of House", 0,-2,3,1,0,0 "East of House", 2,4,0,5,0,0 "South of House", -2,0,3,1,0,0 "Kitchen", 0,0,3,6,7,0 "Living Room", 0,0,5,-3,0,0 "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
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
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
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'
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
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
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 data data data data data data data
LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN "West of House", 2,4,-1,0,0,0 "North of House", 0,-2,3,1,0,0 "East of House", 2,4,0,5,0,0 "South of House", -2,0,3,1,0,0 "Kitchen", 0,0,3,6,7,0 "Living Room", 0,0,5,-3,0,0 "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."
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"
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
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
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
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:
'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
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.
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 data data data data data data data
LOCATION N S E W U D - NORTH SOUTH EAST WEST UP DOWN "West of House", 2,4,-1,0,0,0 "North of House", 0,-2,3,1,0,0 "East of House", 2,4,0,5,0,0 "South of House", -2,0,3,1,0,0 "Kitchen", 0,0,3,6,7,0 "Living Room", 0,0,5,-3,0,0 "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
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
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:
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
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
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.