PROGRAMMING IN QuickBasic 4.5 for Complete Beginners. ***************************************************** By the Raze 5th March 2000 Updated 3rd April 2000 LESSON III Skill Level - Beginner/Intermediate IN THIS LESSON: =============== Operators: Keywords: DECLARE EXIT... FUNCTION INKEY$ ON PLAY SHARED SOUND STEP TYPE ...UNTIL... VARPTR$ WHILE...WEND CONTENTS: ========= PART 1: The Journey So Far... PART 2: Practical Stuff - Sub-Procedure Modules - Global Variables/Arrays - Declaring Modules - Module Parameters - User Input in Loops - Data elements PART 3: Programming Theory - Planning Out - Algorithms - Foundational Program Layout - TRUE/FALSE Checking Theory PART 4: Practical Stuff Part 2 - PLAY Statement - VARPTR$ - Streaming Music with the PLAY Statement - Function Modules - More on FOR/NEXT - More on DO/LOOP - Exiting Procedures PART 5: Putting It All To Good Use PART 1 - THE JOURNEY SO FAR... ============================== So, did you try to modify the text-adventure with your newly aquired skills? How did you go with that, did you figure out a map system, and a data system that holds text for each area? Don't worry if you didn't succeed, it would not have been that easy really, actually you'd have been better off starting a new program from scratch. There is a universal law that is obvious: The larger your programs become, the more complex they are. So, now we've covered the very basics of QB programming, we can begin PLANNING. A lot of this lesson is going to be about just that, as well as introducing proper SUB-PROCEDURES, realtime LOOPs, and using these in creating a good, clean, efficient program structure. PART 2 - PRACTICAL STUFF ======================== SUB-PROCEDURE MODULES ===================== You've already learnt GOTO and GOSUB which work fine in QB, but are really some more of those old relics of BASIC ancestors to which there are now better alternatives. Enter the world of QB sub-procedures, or modules as they can be called. If you understand how subs using GOSUB/RETURN work, then you're not at all far off comprehending modules. These modules do the same thing as a standard GOSUB sub - A batch of QB lines, in affect a mini-program that is given a label and can be called by the main program or even by other modules at any time. Eventually, you will probably stop using GOTO/GOSUB altogether, put them in an old folk's home. Let's take an example we used in lesson 2 for putting a value into an array for a map's data: DIM map%(100, 100) tileput% = 1 mapx% = 10 mapy% = 35 GOSUB maparray END maparray: map%(mapx%, mapy%) = tileput% RETURN See in this case the sub 'maparray'? Well for starters, we want to cut the equation out of it and delete the whole sub, from the label to the line 'RETURN'. Go to the line 'GOSUB maparray' and delete just the GOSUB bit, leave the name there by itself. Now, on a blank line, anywhere, type 'sub maparray', and hit 'ENTER'. You have just created a sub-procedure module called 'maparray' that is currently empty, and should look like this: SUB maparray END SUB Obviously SUB maparray indicates the beginning of the array and END SUB ends it and returns control to the program from where you left, just as RETURN used to. Now, paste that line from the original sub into here so it should look something like this: SUB maparray map%(mapx%, mapy%) = tileput% END SUB That's it. You've modified the sub completely to a proper module. But where's the rest of the code? I hear you ask. If you look at the top of the window, you'll see the filename('Untitled' if you haven't yet saved, a semicolon, then the sub's name, 'maparray'. You see, modules are all part of the same file, but QB gives them all their own windows, separate from the rest of the code. To switch between modules, select 'View' from the menu then 'Subs...' A window will appear with the filename, then the sub's name maparray following it. The filename is the main part of your program where the rest of your code is, and from where the modules are run. To go back to the rest of your program, select the file-name. Notice we changed 'GOSUB maparray' to just 'maparray'? That's because all you need when calling modules is their name, no extra syntax. If you run it now, the program will run just as it did before. (Not that you'll be able to tell, it outputs nothing to the screen!) GLOBAL VARIABLES/ARRAYS ======================= Actually, I was wrong. The program WON'T run just as it did, for one important reason: The values of the arrays will not be carried into the module! You know how tileput%=1, mapx%=10 and mapy%=35? Well, when you enter the sub their values are not known, and assumed to be ZERO. This is because variables and arrays by default can only use their data in the one current module, whether it's the main module or any of the sub modules. This is the one mere disadvantage modules have. But I wouldn't exactly call it a disadvantage, because there is a method that easily avoids this, and it's to do with how you define your variables/arrays. Instead of 'DIM map%(100, 100)', try 'DIM SHARED map%(100,100)'. What SHARED does is simply say 'This can be used globally throughout any module in the program, and keep its current values'. Don't just stop there though, you'll have to do the same for your variables now!: DIM SHARED map%(100,100), tileput%, mapx%, mapy% This is by no means declaring them all as arrays, (only where you gave the names brackets with a range at the end), but means the values of these variables too can be used globally throughout any module. As you can see you can define them all on one line using commas to separate them, similar to the DATA syntax. DECLARING MODULES ================= If you have a module that is called at least once in the program, when you either run or save the program QB will automatically insert a line on the top of your program to declare that module, just like we have to declare our data types. In this case, the line will be: DECLARE SUB maparray () The line means 'Declare a Subprocedure-type module called 'maparray', with no parameters'. In between the brackets is where parameters for the module go (if there are any). We'll look at module parameters next. *NOTE*: Always define your variables/arrays before the actual code begins, but after the DECLARE lines. Never DIM a data type more than once in a program. MODULE PARAMETERS ================= In the PRINT statement, any string, variable, equation etc that you include afterwards are the parameters. For the COLOR statement, the foreground and background values are the parameters. Basically, a parameter is some value that changes the way a statement or module performs if added at the end. Unlike GOSUB subs, we can apply parameters to our module subs to make them useable like QB's default statements, so in effect we can create our own custom statements! Here is how we can modify our little map array program so the sub uses parameters: DECLARE SUB putOnMap (x%, y%) 'Declares sub and parameters DIM SHARED map%(100,100), tileput%, mapx%, mapy% 'Declare data types globally tileput% = 1 mapx% = 10 mapy% = 35 putOnMap mapx%, mapy%, tileput% 'Use module and parameters END SUB putOnMap (x%, y%, z%) map%(x%, y%) = z% 'Makes use of parameters RETURN Firstly, though not important I changed 'maparray' to 'putOnMap' to better describe what the module's task is, to put a value to a given location on the map. The parameters used are an x location and a y location for the map. Because of this, we can use putOnMap to place a value to any spot on the map depending on the given parameters. In this case mapx%/mapy%/tileput% are the parameters and the module puts these into the values x%/y%/z% and uses them to state the location on the array. To further clarify the use of parameters in a module, say we have a program where the text and background colour must be changed constantly. Instead of having to type out the two lines every time (COLOR and PRINT) we can make a sub module called 'colprint' for example that uses parameters to do it for us: DECLARE SUB colprint(foreground%, background%, clstf%) 'We do not need to DIM SHARED rainbow% as it is only used in main module colprint 0, 7, 1 'text 0, background 7, use CLS to reset whole screen. FOR rainbow% = 0 to 15 colprint 15, rainbow%, 0 'text 15, background is rainbow, donot CLS. PRINT "Woe, the colour man..." NEXT rainbow% END SUB colprint(foreground%, background%, clstf%) SELECT CASE clstf% COLOR foreground%, background% CASE 1 CLS CASE ELSE '(Donot Clear screen) END SELECT END SUB See how it works? It combines COLOR, PRINT and even CLS into the one statement, using 3 parameters! We have used CLS only once to reset the screen in the new background colour, hence the grey background and the rest of the backgrounds not taking the whole screen also. Remember, the code will not appear like above (all at once) when you paste it into the IDE, QB will automatically place the modules away in their own screens. USER INPUT IN LOOPS =================== We already learnt back in lesson 1 how to get user input at a prompt. But of course if you want a game like a platform, you don't want a prompt showing up saying "Press 1 to jump, 2 to go left..." This is just ridiculous. You want instant response as soon as you hit the key you don't want to have to press Enter to process it each time! The function we use is INKEY$. When using this in an expression like 'PRINT INKEY$' or 'a$ = INKEY$' It returns the ASCII character of the key being pressed. Obviously if you press lower-case 'f' on the keyboard, the value of INKEY$ will be ASCII character lower-case 'f'. To illustrate how INKEY$ works, let's design a looping program that displays to the screen whatever character is currently being pressed if any: COLOR , 7 'Background to colour 7 (light grey) CLS 'Reset screen in set colour COLOR , 0 'Change new text background to 0 (black) DO user$ = INKEY$ 'Put value of INKEY$ into user$ variable PRINT user$; 'Display user$ LOOP UNTIL user$ = CHR$(27) 'If escape is pressed, stop the loop. END The program loops continuously, repeating the lines between DO and LOOP. This loop basically checks to see if any key is currently pressed, and whatever key is pressed, its ASCII character is put into the user$ variable. When we print out the value of user$, we use a semi-colon to keep it on the same line otherwise the characters scroll away very fast. This program introduces to you a new way to use loops, and that's LOOP UNTIL. Basically it acts similar to the IF/THEN statements in that it checks for the value of something and performs an action until the result is true. In this case the loop checks to see if the ESCAPE key has been pressed (which is ASCII character 27), and if it has been pressed the loop ends and the program carries on, in this case it ENDs. Have you tried all the keys, other than the numbers and letters? Have you tried the F-keys, the cursor keys? See how they seem to print similar characters like capital letters, punctuation, math symbols, except there are 'spaces' in-between? This is because these keys are input differently. There is no single ASCII character that represents these keys, therefore they are represented by a combination of two ASCII characters, which is that 'space' you see, and then the visible character. But those 'spaces' aren't actual spaces at all, this is the 'null' character which is ASCII character number 0. You know how when you press the 'up' cursor key, there is a null-space followed by capital H? This is how it can be represented: CHR$(0) + CHR$(72) or CHR$(0) + "H" What about when you see a null and semicolon when you press 'F1'?: CHR$(0) + CHR$(59) or CHR$(0) + ";" So there you go, you now know how to input keys in a loop, even the cursor keys! There are other, considerably harder ways of inputting keys like Cntrl and Alt (If you like these for shooting or jumping) but this is currently above our level of skill and we will approach this in a future lesson. So, how can you usefully put checking for multiple keys in a loop?: CLS DO user$ = UCASE$(INKEY$) PRINT user$; SELECT CASE user$ CASE CHR$(27) PRINT " Escape has been pressed. Program will exit." EXIT DO CASE "A" TO "Z" PRINT " A letter has been pressed." CASE "0" TO "9" PRINT " A number has been pressed." CASE CHR$(0) + ";" TO CHR$(0) + "D" PRINT " A function key (F1 to F10) has been pressed." CASE CHR$(0) + CHR$(133) TO CHR$(0) + CHR$(134) PRINT " A function key (F11 to F12) has been pressed." CASE CHR$(0) + "H" PRINT " Up" CASE CHR$(0) + "P" PRINT " Down" CASE CHR$(0) + "K" PRINT " Left" CASE CHR$(0) + "M" PRINT " Right" CASE "" CASE ELSE PRINT " An unspecified key has been pressed." END SELECT LOOP PRINT "Ending program..." END If you cannot fully understand why you need certain ASCII characters for keys, remember it's all in that table in QB help! And if you ever want to find the ASCII character for a key, just run that other program that outputs it for you. One of the parts that may be a bit confusing in this is that there is a separate checker for the F-keys 11 and 12. This is because for some reason only the ASCII codes for F1 to F10 exist together. The F11-12 ASCII characters exist in the extended ASCII. One more thing to note: See how the end of this program is OUTSIDE the loop, but there is no LOOP UNTIL? Well, if you look at the case for the escape key (27) you will see a statement called "EXIT SUB". This line leaves the loop and continues the flow of the program unconditionally. Of course you could just put END in the case instead, but that may not always be what you want to do in other situations... DATA ELEMENTS ============= To finish off this practical section, let's learn one more major form of data storage. You know we can allocate pockets of memory with a fixed name and the ability to hold changeable data, and these are VARIABLES. And you also learnt from lesson 2 that you can create ARRAYS, which are multiple pockets of memory under one fixed name, each one allocated by its OFFSET, that is it's position from the start of the array. To make coding easier to read and to class data into groups better, we can break variables and arrays up into what's called ELEMENTS. An element is basically a variable or collection of variables that can sit under onevariable name, or array offset. Example: Say we have our game's hero, and we want all his various info as well-organised as possible. We could do it this way: DIM SHARED heroName$ DIM SHARED heroHealth% DIM SHARED heroScore% DIM SHARED heroBeerLeft% heroName$ = "Jim" heroHealth% = 100 heroScore% = 0 heroBeerLeft% = 10 Kind of messy, huh? We COULD make an array like this: DIM SHARED herostats(4) AS INTEGER But we have string AND numerical data, an array can only be one or the other. What about this example, where we have a game that requires the x and y location of the enemies as well as their health. Let's say there are at the most, 10 enemies at once: DIM SHARED enemyx%(10) DIM SHARED enemyy%(10) DIM SHARED enemyhealth%(10) See? We need a separate array for each of an enemie's 3 attributes. How clumsy! Well, how we get around that is by using the TYPE statement. With this, we can create all these elements under the one variable/array. Let's change the first example: TYPE heroStatsType 'Define Type name and elements heroName AS STRING heroHealth AS INTEGER heroScore AS INTEGER heroBeerLeft AS INTEGER END TYPE DIM SHARED heroStats AS heroStatsType 'DIM type under a global name Can you follow that? First you create a TYPE name. It can be anything, but to avoid confusion you should name it just like the actual element data type you end up with except add 'Type' to the end so you know what it is. Then we define all the elements one/by/one within it. See we can define strings AND numbers under the one name? The problem next is that we can't actually DIM SHARED our type, so we have to create a new data type and put the elements into it. In this case it's 'heroStats'. If we just use our type raw, we can't use it globally in other modules as it ain't SHARED. *NOTE*: The actual type and final data type cannot be defined as a specific integer or string, save that for the elements ONLY. Now, here's how we'd use them for example: INPUT "Enter the hero's name: ", heroStats.heroName PRINT heroStats.heroName; " has "; heroStats.heroBeerLeft ;" cans of beer left." See how you refer to an element? First you have the main data type name, then the period, then the specific element you wish to refer to. Now let's fix up the data types for the enemy stats: TYPE enemyType enemyx AS INTEGER enemyy AS INTEGER enemyhealth AS INTEGER END TYPE DIM SHARED enemy(10) AS enemyType 'Each of the 10 offsets has 3 elements 'This algorithm makes all enemies move down and across 1 like Space Invaders FOR moveall% = 1 to 10 enemy(moveall%).enemyx = enemy(moveall%).enemyx% + 1 enemy(moveall%).enemyy = enemy(moveall%).enemyy% + 1 NEXT x% When it comes to arrays using elements like this example, remember the offset value in the brackets always sticks WITH the main data type name on the LEFT of the period, the element name stays isolated. Well, it's still quite possible to use separate arrays and variables instead of elemented data types, but of course this proves a much easier to follow method of handling your data. PART 3 - PROGRAMMING THEORY =========================== We haven't yet even touched the subject of planning, which is essential to writing large programs properly! Sorta like you can't start writing a story when you haven't yet even planned how it's going to end... Then again, maybe you can if you've got a good imagination, but again I'm straying off the path. PLANNING OUT ============ Obviously we don't need to plan any of the programs we've done so far since they've just been about learning the foundations of programming. But now we have the basic know-how, we can plan out in plain-English what we want our program to do, then use the appropriate QB statements and functions to acheive it. We'll use a program we've already created as an example: The times table program. Let's assume we haven't yet created it, and we are planning out what we need: Plan for Program ________________ Aim: - To Create a program that outputs the times tables of any inputted number. Requirements: - User input of a number - Routine that calculates and outputs product of numbers one-by-one. e.g 0 x number, 1 x number etc... Pseudo Code: This is the method of laying out what the program will do, except you use plain English descriptions at this stage ... Clear Screen Prompt the user to enter a number into a variable e.g'number' Clear Screen again Display heading descibing the times table of the 'number' A For Loop That begins with a value of 0. Amount to Multiply starts with 0 find product of multiplier(= loop value) and 'number' Output the result visually including multiplier and 'number' Increment loop value by 1 and do again unless we have already done 12. End Program. Here's the Above planning later converted into QB code when programming into computer: ' Multiplication Table CLS INPUT "Enter number to find tables of: ", number CLS PRINT number; " Times Table" PRINT "" FOR x = 0 to 12 result = x * number PRINT x; " x "; number ; " = "; result NEXT x END ALGORITHMS ========== As I've mentioned once before, an algorithm is a set of instructions that can be repeatedly used to process different values of data. Basically, we use algorithms to reduce redundancy, that is the amount of repetetive code used many times that can be put into one re-usable sub with parameters. Here is an example of a very redundant method of doing a times table: CLS INPUT "Enter number to find tables of: ", number CLS PRINT number; " Times Table" PRINT "" PRINT "0 x "; number ; " = "; 0 * number PRINT "1 x "; number ; " = "; 1 * number PRINT "2 x "; number ; " = "; 2 * number ... And so forth You can see how ridiculous this could be, especially if the table included hundreds of sums not just twelve: You would need hundreds of lines just to acheive a simple task! What we need is an algorithm: A set of instructions that only require to be coded once in the program and can be used repeatedly in the program to get a variety of outputs. The times table program's FOR/NEXT loop is the algorithm here. It uses two lines of code to output (almost) any number of sums, by repeating the code the specified amount of times, in this case 12. The text-adventure example in Lesson 1 now appears as extremely redundant code. Every different spot in the game has its own input section - But Wouldn't it be a lot more efficient to just redesign the program so it only needs the one input system and use an algorithm to determine where you are? This is the beauty of variables: We can in a sense 'recycle' code to use many different data values. If we had no variables, algorithms would be impossible to create as we could only use constant values, therefore we'd have to create code for every possibility which could amount to thousands instead of dozens of lines of code. So there's the big tip: Use variables in place of straight numbers as much as possible, use only numbers to define initial variable settings when you first run a program. The more variables expressions you have, the more use you can get out of that single piece of code. FOUNDATIONAL PROGRAM LAYOUT =========================== Fundamentally, this is the order you want your program's source code to be layed out: 1 - Commented info like program author, contact e-mail, date of completion, description, credits (where applicable) etc 2 - Declare all SUB and function modules 3 - Define all variables and arrays 4 - Set initial values for variables/arrays 5 - Initialization procedures to prepare program 6 - Main program loop and algorithm/s 7 - Subs and function modules Here's a code example of a hypothetical game (NOTE: Donot attempt to run this as it is only to illustrate layout, it is *not* functional) : 'Hypothetical Strategy game by Raze March 2000 'Declare all SUB and function Modules DECLARE SUB endit () DECLARE SUB fireit (whichunit%) DECLARE SUB movunit (whichunit%, direction%, unitspeed%) DECLARE SUB loadgfx () DECLARE SUB preparemap () DECLARE SUB loadpal (file$) DECLARE SUB mapengine (x%, y%, direction%) 'Define all variables and arrays DIM SHARED name$, map%(64, 64), pal&(255), curx%(100), cury%(100), dir%(100) DIM SHARED speed%(100) 'Set initial values for variables/arrays curx%(0) = 42 cury%(0) = 16 dir%(0) = 1 speed%(0) = 4 curunit% = 0 'Initialization procedures to prepare program loadpal loadgfx preparemap 'Main program loop and algorithm/s DO movunit curunit%, dir%(curunit%), speed%(curunit%) user$ = UCASE$(INKEY$) SELECT CASE user$ CASE CHR$(27) EXIT DO CASE CHR$(32) fireit curunit% CASE CHR$(0) + "H" dir%(curunit%) = 1 CASE CHR$(0) + "P" dir%(curunit%) = 5 CASE CHR$(0) + "K" dir%(curunit%) = 7 CASE CHR$(0) + "M" dir%(curunit%) = 3 END SELECT LOOP endit '(Actual Module code not shown) Basically, the program shown typifies a war-game strategy like the Warcraft series, that require a selected units movement around the map by the cursor keys, and its ability to perform an action (e.g fire arrow) using space-bar. Donot worry so much about the actual workings of the 'game' itself, the code in-between is merely to help illustrate how you should lay a program out: Declare your modules, define your data types, intitialize the data then run the main realtime game loop, where the user input is taken and processed by the different subs. e.g pressing Up changes your direction to 1, and next time the loop is run it goes to a sub that moves the current unit in the right direction and at the right speed. Well, we've already covered defining data types, we've done declaring modules and now game loops, so putting it together in a good, neat flowing layout should not be too much of a hassle for you. TRUE/FALSE CHECKING THEORY ========================== To use loops, IF and SELECT CASE to their full potential you need to fully understand how programs can check for the values of say two values at once. You may only want a certain procedure to be carried out if say, two variables are both 0, or both another value, or if either are 0? Here is a small table that illustrates TRUE/FALSE checking: __________________________________________________________ | AND | OR | XOR | (Both must be | (Either | (Only one | true) | are true) | is true) |_______________________________________________ | | | | | | Value 1: | TRUE | FALSE | TRUE | FALSE | TRUE | FALSE | | | | | | Value 2: | TRUE | TRUE | TRUE | TRUE | TRUE | TRUE _________|_______|________|______|_______|_______|________ RESULT: | TRUE | FALSE | TRUE | TRUE | FALSE | TRUE Take this line for example: IF x% = 1 AND y% = 10 THEN END The program will only end if they both simultaneously equal 1 and 10. Good for giving a location on an x/y map an event. For example, 'If the character is 1 across AND 10 down, encounter the monster.' IF x% = 1 OR y% = 10 THEN END The program will end if either x% = 1 or y% = 10, regardless of the other one. IF x% = 1 XOR y% = 10 THEN END This means they cannot both match at ONCE, but if x% or y% is true while the other isn't, the program will only then end. PART 4 - PRACTICAL STUFF 2 ========================== Let's look at a few other things to expand your capability with QB... cheap-quality music and all the tones in-between. PLAY statement ============== You're probably aware that inside your computer's case is what's called a PC speaker, albeit these days they put less quality into them simply because all machines brand new these days come with at LEAST a 16-bit sound-blaster compatible card. As you're also probably aware, the synthesis quality of the internal speaker is pretty dismal by today's standards, that's if you can even call it synthesis: mono-channel electronic blips. But hey, when it comes to sound and music for your games it's better than nothing, right? For now, using actual MIDI and WAV through your card is WAY down the track, so we will stick with QB's in-built ability to output sounds of any note and (almost) any frequency audible to the human ear. The syntax for PLAY, to play notes C through to B is: PLAY "CDEFGAB" We start at C not A because a new octave begins at C. Here are some various ways to use play: n = numerical value _____________________________________________________________________________ A to G = Play a note (- or # turns the preceding note into a flat or sharp) On = Set current octave (0-6) Ln = Set current note length(1-64). 2 is half-note, 4 is a quarter-note, etc Pn = Pauses between notes. 2 is half-note pause, 4 a quarter-note pause, etc Nn = Play a note (ranges 0-84, 0 = a rest) < > = Lower / Raise octave Tn = Set Tempo (32-255, 120 default) MF = Play music in foreground MB = Play music in background X + VARPTR$(string-expression) = Play an extra expression _____________________________________________________________________________ There are some more commands on specifying note lengths but the above are really enough. The example I've presented is adequate enough for anyone who doesn't use training wheels to figure out how to use the rest of the expressions. PLAY is probably the easiest statement of all in QB, but if you really are stuck there is an extensive help section about it in QB help. Aah, don't those blippy little tunes take you back to the days of the old Sierra adventure games... VARPTR$ ======= VARPTR$ returns the address of a play expression as a string. This is simply a way of combining two strings of PLAY expressions, as it is used in the QB example. Here is one way of combining strings: music$ = "FGAB" PLAY "CDE" + "X" + VARPTR$(music$) 'X a separate entity or PLAY "CDE X" + VARPTR$(music$) 'X part of first string or PLAY "CDE" + music$ 'just omit the X and VARPTR$ altogther Personally, I don't see any relevance in using VARPTR$ at all when you can get the same output combining the two strings directly without the X sandwiched in-between. The last example works fine, and is quicker to type so I'd suggest don't use VARPTR$ for the PLAY statement. STREAMING MUSIC WITH THE PLAY STATEMENT ======================================= Let's say down the track you've got a realtime game (one with a main loop) and you want some tunes to crank away in the background continuously. Well, this is not a problem at all. We use a process called 'Event Trapping' to check the status of a certain situation being carried out by QB. Here is an example of how we'd continuosly play a melody in a realtime game: DIM SHARED tune$, counter% tune$ = "mb o1 l4 bag p4 bag p4 l8 bbbbaaaa l4 bag p4" 'The 'Hot Cross Buns' Tune in the background PLAY ON 'Enables Play event trapping ON PLAY(1) GOSUB playMusic 'If there is less than 1 note in the music ' buffer goto the PlayMusic sub to re-start it GOSUB playMusic DO counter% = counter% + 1 PRINT counter% 'This merely illustrates the loop still runs smoothly even with the background music. user$ = UCASE$(INKEY$) SELECT CASE user$ CASE CHR$(27) 'Escape key exits as usual EXIT DO END SELECT LOOP END playMusic: PLAY tune$ RETURN The first step to ensuring the program runs smoothly without being delayed by PLAY is to make sure the music is set to play in the 'background' with MB. ON PLAY(1) checks to see if the music left to play is running out, and if it is it simply re-runs it with by the playMusic sub. The game's loop will be completely un-affected no-matter how large your game gets. *NOTE*: You will notice a normal GOSUB routine instead of a SUB module for playing music. This is one weakness of QB, event-trapping cannot call modules, only GOSUB labels. There are other kinds of event trapping we can use including for the keyboard and file handling, we'll check them out in future lessons. SOUND STATEMENT =============== While PLAY gives you an easy way of expressing notes simply by their name, sound is much more versatile as you can specify the EXACT frequency and duration of a tone. If you want to attempt PC speaker SFX, the SOUND statement is the way to go out of the two. Try this example: duration% = 1 FOR x% = 37 TO 1000 SOUND x%, duration% NEXT x% END This program smoothly increases increases the tone of the sound right from 37Hz to 1000Hz (Cycles/second). 37 is the lowest allowed by the way, and 32,768 is the highest frequency. But I warn you, once you get to even the low 1000's you'll be bound to drive all the dogs in the neighbourhood mad. The ending parameter is simply the duration of the tone, equivalent to the computer clock cycles, in which there are 18.2 in every second, so a duration of 18.1 would mean the tone plays for 1 full computer second. FUNCTION MODULES ================ Well, if you understand how SUB modules work, and you understand variables, this should be a breeze. Basically, a FUNCTION is a module of code that can be assigned parameters, except instead of being a normal runnable piece of the program it returns a value, just as standard QB functions return values, for example ASC returns the ASCII number of a single ASCII character. Here's a simple example of the application of a FUNCTION module: DECLARE FUNCTION product (a%, b%) 'Declare module as FUNCTION CLS answer% = product(3, 5) 'Return a value from product to answer% PRINT answer% 'Print value of answer% END 'Program ends here FUNCTION product (a%, b%) 'Start of 'product' function product = a% * b% 'Gives product the value of a% * b% END FUNCTION Firstly, notice the module is declared as a FUNCTION, not a SUB. This applies to when you actually define your own function as well. Remember how we created SUB maparray by typing just that in on a blank line? Well, in this case, 'product' would have been created by typing FUNCTION workitout% (a%, b%). In this scenario we immediately specify the parameters because we know what data it will require for input already. For maparray we entered the parameters later as we didn't expect we needed them at first. But just to remember, you can add the parameters immediately or later for both depending on whether you have pre-designed or not. But notice how the name 'product' itself is treated like a variable, e.g product = a% * b% ? This is where functions differ from normal subs. You will also notice that when a function is used, you always put brackets around the parameters entered, unlike a normal sub where brackets are only needed in declaring the sub and the start of the sub itself. You will find FUNCTIONS very handy particularly where a long and complex equation that is used multiple times from multiple data sources is required. You can make the function return the result of the equation, giving it the parameters needed. MORE ON FOR/NEXT ================ We'd better expand slightly on some of our procedure syntax. Remember FOR/NEXT? Currently, you only know how to increment a value by one. However, you can also decrement, and not just by values of one... FOR x% = 0 TO 20 STEP 2 PRINT x% NEXT x% Notice the new addition to the syntax, 'STEP'? This basically states what amount the loop value should be incremented/decremented by. In this case, Each time the value of x will go up by two, so the output would be something like: 0 2 4 6 ...etc And if you want to go down? FOR x% = 20 TO 0 PRINT x% NEXT x% You will probably notice upon running no numbers are displayed. Why? Because The syntax is saying, "Increment from 20 by 1 until 0 has been reached". Of course, this way 0 will NEVER be reached, only distanced further. The QB interpreter for FOR/NEXT knows this, so it instantly decides not to process the procedure at all. To decrement by one for example, do this: FOR x% = 20 TO 0 STEP -1 PRINT x% NEXT x% So the 'STEP' part is saying 'Decrement by one each time.' Obviously since without using STEP the value increments by 1, because by default STEP will be positive 1 even if it's not included. MORE ON DO/LOOP =============== There are 4 ways to do indefinite loops: 'Infinite loop DO ... LOOP 'loop until a condition is met DO ... LOOP UNTIL... We have covered the two above, but here are two more condition-based indefinite loops: DO WHILE INKEY$ = "" ... LOOP This syntax of DO is similar to the use of UNTIL except the loop continues WHILE a condition is true. Notice here INKEY$ is equal to null, because this means 'WHILE no keys are pressed keep looping', as opposed to 'loop UNTIL a key is pressed'. There is one more conditional loop: WHILE INKEY$ = "" WEND This is simply almost exactly the same use as 'DO WHILE', except it is especially designed for conditional looping, where DO/LOOP can have no conditions. WHILE/WEND must have a condition, of course. Out of all conditional loops, I use 'DO/LOOP UNTIL' the most. EXITING PROCEDURES ================== Again, we have already covered using 'EXIT DO' to exit the current loop and continue the program below it. 'EXIT' is not just exclusive to DO/LOOP, you can use it like 'EXIT FOR', even 'EXIT SUB'. 'This program exits a FOR/NEXT procedure even before the end value is met. FOR x% = 1 to 1000 IF x% >= 455 THEN EXIT FOR NEXT x% PRINT "Last value of x%: "; x% END Without using EXIT you'll find yourself having to resort to messy old GOTO's when you have to leave a procedure halfway through, so make good use of it! PART 5 - PUTTING IT ALL TO GOOD USE =================================== We've certainly made progress since the text adventure of Lesson 1, so this example, though still damn primitive by even moderately experienced coders, may suprise you in that its the first program we've done that has sounds, and some degree of graphics, albeit tryhard graphics using combined ASCII characters. In it, you are the smiley face on the island and you have to find your way to the ship... How hard!! This program makes use of most but not all things covered in this lesson, as well as stuff from lesson 2 that we didn't have an ending program for. Well, here it is, so I'll see you next lesson. ' ASCII Adventure Demo for QBasic 1.1+ DECLARE SUB move (mx%, my%) DECLARE SUB playit (playline%) DECLARE SUB readmusic () DECLARE SUB endit () DECLARE SUB puttomap (putx%, puty%) DECLARE SUB Loadmap () DIM SHARED map%(1 TO 20, 1 TO 20) TYPE facetype xpos AS INTEGER ypos AS INTEGER END TYPE TYPE mapmaxtype x AS INTEGER y AS INTEGER END TYPE DIM SHARED face AS facetype DIM SHARED mapmax AS mapmaxtype DIM SHARED musicflag% DIM SHARED musicarray(5) AS STRING face.xpos = 14 face.ypos = 4 mapmax.x = 20 mapmax.y = 20 musicflag% = 1 Loadmap readmusic playit musicflag% PLAY ON LOCATE face.ypos, face.xpos: PRINT CHR$(1) LOCATE 1, 41 PRINT "ASCII Adventure Demo for QBasic 1.1+" DO ON PLAY(1) GOSUB playmusic user$ = UCASE$(INKEY$) SELECT CASE user$ CASE CHR$(27) endit CASE CHR$(0) + "H" movx% = 0 movy% = -1 move movx%, movy% CASE CHR$(0) + "P" movx% = 0 movy% = 1 move movx%, movy% CASE CHR$(0) + "K" movx% = -1 movy% = 0 move movx%, movy% CASE CHR$(0) + "M" movx% = 1 movy% = 0 move movx%, movy% END SELECT LOOP playmusic: musicflag% = musicflag% + 1 IF musicflag% > 4 THEN musicflag% = 1 playit musicflag% RETURN 'Remember, DATA statements must remain in the main module!! musicdat: DATA "l4o0cf#gp4cf#gp4cf#o1co0c#l2cp2" DATA "l4o0a#o1d#f#p4 l4o0a#o1d#f#p4 l4o1f# l8ff#l4g#fl2f#p2" DATA "l4o0cf#gp4cf#gp4cf#o1co0c#l2cp2" DATA "l4o0a#o1d#f#p4 l4o0a#o1d#f#p4 l4o1a# l8f#a#l4g#fl2d#p2" DATA "l4o3cg l12fec l4o4co3g l12fec l4o4co3g l12fefl2d" mapdata: DATA 177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177 DATA 177,177,177,177,177,176,177,177,177,177,176,176,176,176,176,176,177,177,177,177 DATA 177,177,177,177,176,176,176,176,176,176,176,176,176,176,176,176,176,177,177,177 DATA 177,177,176,177,176,176,176,176,176,176,176,176,176,176,176,176,176,177,177,177 DATA 177,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,177,177 DATA 177,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,177,177,177,176,176,176,176,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,177,177,177,177,177,177,176,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,177,176,176,176,176,177,176,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,177,176,232,176,176,177,177,176,176,176,176,176,176,176,177 DATA 177,176,176,176,176,176,176,176,176,177,177,176,177,177,176,176,176,176,176,177 DATA 177,176,176,176,176,176,176,177,176,177,176,176,176,177,176,177,176,176,176,177 DATA 177,176,176,176,176,176,176,177,177,177,176,176,176,177,177,176,176,176,176,177 DATA 177,176,176,176,176,176,176,176,177,176,176,176,176,176,176,176,176,176,176,177 DATA 177,177,176,176,176,176,176,177,177,176,176,176,176,176,176,176,176,177,177,177 DATA 177,177,177,176,176,176,176,177,176,176,176,176,176,176,176,176,176,177,177,177 DATA 177,177,177,177,176,176,176,177,176,176,176,176,176,176,176,176,176,177,177,177 DATA 177,177,177,177,177,177,177,177,177,177,176,176,176,176,176,177,177,177,177,177 DATA 177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177,177 SUB endit CLS PRINT "GAME EXITED." END END SUB SUB Loadmap 'Load from Data RESTORE mapdata FOR y% = 1 TO 20 FOR x% = 1 TO 20 READ map%(x%, y%) NEXT x% NEXT y% 'Clear Screen and Draw Map CLS FOR y% = 1 TO 20 FOR x% = 1 TO 20 LOCATE y%, x% puttomap x%, y% NEXT x% NEXT y% END SUB SUB move (mx%, my%) 'Check what face is moving to SELECT CASE map%(face.xpos + mx%, face.ypos + my%) CASE 177 xable% = 0 CASE ELSE xable% = 1 END SELECT IF xable% = 1 THEN puttomap face.xpos, face.ypos face.xpos = face.xpos + mx% face.ypos = face.ypos + my% LOCATE face.ypos, face.xpos: PRINT CHR$(1) END IF 'Check what face is on SELECT CASE map%(face.xpos, face.ypos) CASE 232 LOCATE 5, 22 PRINT "You have found the abandoned vessel and launch to safety." playit 5 SLEEP 3 LOCATE 10, 40 PRINT "THE END" END END SELECT END SUB SUB playit (playline%) PLAY musicarray(playline%) END SUB SUB puttomap (putx%, puty%) 'Puts map tile to current location LOCATE puty%, putx% SELECT CASE map%(putx%, puty%) CASE 0 COLOR 7 CASE 176 COLOR 2 CASE 177 COLOR 1 END SELECT PRINT CHR$(map%(putx%, puty%)) COLOR 7, 0 END SUB SUB readmusic 'Reads the 4 data lines of music into array RESTORE musicdat FOR x% = 1 TO 5 READ musicarray(x%) NEXT x% PLAY "MB" 'Set music as background END SUB