About the course

This is a short course in simple game design using QBasic. No prior experience with QBasic is necessary, although I dont go into detail on how to use the QBasic menu interface.

The principle aim of the course is to stimulate interest in game design. Its divided in modules to keep you busy for two weeks, although nothing stops you from doing more than one module per day. The program parts can be copied from the file and used directly. This way you dont have to type anything!

The two example games can be tailored and expanded upon as an additional exercise. I will use ASCII characters for graphics and only briefly discuss the implementation of bit-mapped graphics at the end of the course.

What to expect from the course

Its for "newbies" to game design. You might experience this as a course in QBasic as well as games design. Well, without a thorough understanding of the QBasic commands you wont be able to write a game, now will you?

The course is not meant to teach you how to write DOOM, only to teach some basic game design skills.


Use QBasic's built-in HELP function to get more information about a BASIC command.

Why use QBasic to teach game design? Here are the reasons:

  1. QBasic is bundled with MSDOS 5 and higher so you dont have to fork out hundreds of dollars to get started.
  2. Contrary to most beliefs it is possible to teach good programming principles with BASIC, especially in QBasic where line numbers and GOTO's aren't mandatory.
  3. BASIC is easy enough to start with and once you've learned the ropes you can move on to a language more suited to professional game design.
My teaching methods are unorthodox, but I aim to teach in a fun way.

Week 1, Day 1

Today you will learn how to make a PACMAN symbol move across the screen.

Start QBasic and type in the following program:

CLS PRINT "Below is PACMAN himself!" PRINT "C" Now RUN the program by pressing ALT-R and then S. Is the graphics a little unconvincing? Well, we have to start somewhere!

The command CLS clears the screen and we'll use it often.

Lets put PACMAN in the middle of the screen. The command you need is LOCATE. Type (or edit) and RUN the following:

CLS LOCATE 12,40 PRINT "C" Easy as pie!
The first value between the brackets indicates the row, ranging from 1 at the top to 23 at the bottom on the screen. The second value is the column and ranges from 1 at the left to 80 at the right on the screen.

Now to make PACMAN move! You could do it this way:

LOCATE 12,1 PRINT "C" LOCATE 12,1 PRINT " " :REM delete the old picture LOCATE 12,2 PRINT "C" :REM draw a new picture to the right (REM statements are ignored by the computer and you dont have to type )
(them in. )
(I will use REM statements to clarify and comment upon my programming.)

This method will take forever to program so lets start using variables.

A variable is described has a name and contains a value. This illustrates the difference between a numeric- and a string variable:

contents ---> ¦ 10 ¦ ¦ "twenty" ¦ ------ ------------ name ---> apples pears$ (numeric) (string) The "$" in "pears$" is pronounced "string" and identifies it as a string variable.
When you PRINT a variable its contents is shown, not its name. Type (or edit) and RUN: CLS apples = 10 pears$ = "twenty" PRINT "Amount of apples are "; apples PRINT "Amount of pears are "; pears$ Notice how the text between the double quotes is displayed literally while the variable name is replaced by its contents. The "=" operator assigns a value to a variable and does it by working from right to left, e.g. in "apples = 10" the "10" goes into "apples". The ";" causes the computer to remember where it last PRINTed and the following PRINT will continue at that position.
PRINT "ABC" will give the same output as PRINT "A"; "B"; "C"

While PACMAN is warming up for his jog, lets make things even easier on ourselves.
You might have figured that I want to use variables in the following way:

column = 1 LOCATE 12,column :REM This translates to LOCATE 12,1 PRINT "C" column = column + 1 :REM Lets calculate the value: :REM column + 1 gives you 1 + 1, in other words 2 LOCATE 12,column :REM This translates to LOCATE 12,2 PRINT "C" Still lots of repetition! An easier way is to make the computer go in a "loop", increasing the value of "column" and printing PACMAN.
Type (or edit) and RUN: CLS FOR column = 1 TO 80 LOCATE 12,column PRINT "C" NEXT column Hope I haven't put you off programming altogether with that one! You've implemented as so-called "FOR..NEXT loop" and here's the low-down on it: The "FOR" line tells the computer that a variable called column will start off at 1 and then increase in value until it reaches 80. The "NEXT" line increases column's value with one and makes the program jump back to the "LOCATE" line. Everything between the "FOR" and the "NEXT" is thus the actual loop and gets repeated 80 times.

If you still dont understand the concept of the "FOR..NEXT loop" then type and RUN:

CLS FOR values = 1 TO 10 PRINT values NEXT values Hope you've found the experience enlightening!

Clearly also is that everything is happening too fast. PACMAN completes his trip in the blink of an eye or, depending on how fast your computer is, even less! The solution is to use the SLEEP command which waits a number of seconds. Type (or edit) and RUN:

CLS FOR column = 1 TO 80 LOCATE 12,column PRINT "C" SLEEP 1 NEXT column Too slow now! Another way is to use an empty FOR..NEXT loop that does nothing but kill time. That way you can change the value of the loop untill it provides an acceptable delay for your computer.

Another problem was that the PACMAN isn't deleted in its old position and you are left with a whole row of "CCCCCC"s. We'll solve that now. Here's the final code for the PACMAN 100m sprint: Type (or edit) and RUN!

CLS FOR column = 1 TO 79 LOCATE 12,column PRINT " C" FOR nothing = 1 TO 100 :REM This just delays the computer NEXT nothing NEXT column If you didnt see a thing the program is probably still too fast for your computer, so try changing the limit of the "nothing" loop to 300 or more.

The space before PACMAN cleverly deletes its old position. How? Two characters are displayed (a " " and a "C") but the position only shift one to the right. When the two characters are displayed at their new position, the " " overlaps with the "C" previously displayed and effectively deletes it. That's all for today!

Week 1, Day 2

Watching PACMAN move is fun, but being in control opens up endless possibilities.. Enough said! Remember the FOR..NEXT loop? Lets have a look at a different kind of loop. Type (or edit) and RUN: CLS value = 1 DO WHILE value < 11 PRINT value :REM This is the loop value = value + 1 LOOP This displayed numbers 1 through 10. The program loops while "value" is less than 11. We will now use the DO..WHILE loop to read the keyboard and display the characters that you press. However, there's one snag. The program will loop "forever" unless you stop it! Pressing CTL and BREAK at the same time will do this. Type (or edit) and RUN: CLS DO :REM there's no WHILE, so DO forever.. keyed$ = INKEY$ :REM make keyed$ = the key pressed IF keyed$ <> "" THEN PRINT keyed$ :REM if keyed$ isnt empty display it LOOP I bet INKEY$ has you confused!
INKEY$ is a special string variable. The computer always sets the value of INKEY$ to the key that you press. While you're not pressing any keys it will contain nothing (nothing is ""). The IF command is very straight forward. IF something is true THEN do something.. got it? So IF keyed$ is not equal to nothing THEN its value gets PRINTed. If I didnt include the IF clause then keyed$ would always be PRINTed, whether it contained a value or not. Try leaving out the IF..THEN part and you'll see what I mean! Lots and lots of "nothings" fill the screen!

Armed with our new commands we can finally control PACMAN with the keyboard. Type (or edit) and RUN:

CLS row = 12 column = 40 DO DO :REM <this loop waits for a key to be pressed keyed$ = INKEY$ LOOP UNTIL keyed$ <> "" LOCATE row, column PRINT " " :REM this erases the "C" IF keyed$ = "q" THEN row = row - 1 IF keyed$ = "a" THEN row = row + 1 IF keyed$ = "o" THEN column = column - 1 IF keyed$ = "p" THEN column = column + 1 LOCATE row, column PRINT "C" :REM shows the "C" at the new position LOOP Note that you can use your own choice of keys by replacing "q","a","o" and "p". Using special keys like the cursor keys is another cup of tea.

If you move PACMAN outside the screen boundaries you'll get an error message. To prevent this you must add checks to the program. Just replace the IFs with the following lines and RUN:

IF (keyed$ = "q") AND (row > 1) THEN row = row - 1 IF (keyed$ = "a") AND (row < 23) THEN row = row + 1 IF (keyed$ = "o") AND (column > 1) THEN column = column - 1 IF (keyed$ = "p") AND (column < 80) THEN column = column + 1 This should do for today. Hope you enjoyed this as much as I did!

Week 1, Day 3

A playing area is essential to any game so lets design a maze for our PACMAN.

First I'll have to tell you about arrays. Take a look at this example:

DIM values(3) values(1) = 5 values(2) = 92 values(3) = 45 Arrays are variables containing multiple elements. Each element is named after the array followed by a number to specify the position of the element.
Each element has its own value. Type (or edit) and RUN the following to see how arrays can save you from a lot of repetitive programming: CLS DIM values(10) FOR count = 1 TO 10 values(count) = count NEXT count FOR count = 1 to 10 PRINT values(count) NEXT count Remember that "count" ranges from 1 to 10 so the line "values(count) = count" translates to "values(1) = 1, values(2) = 2, values(3) = 3" etc.

Now lets define our maze. Type (or edit) and RUN:

CLS DIM maze$(6) maze$(1) = "#########" maze$(2) = "# # #" maze$(3) = "# # # # #" maze$(4) = "# # # #" maze$(5) = "# # #" maze$(6) = "#########" FOR count = 1 to 6 PRINT maze$(count) NEXT count Now lets put PACMAN in the maze. Wait - there's nothing that will keep him from moving over the walls. To solve that problem I'll show you how to inspect the maze (the contents of maze$).

The command MID$ allows you to look at parts of a string variable. It has the following format: MID$(name of string, starting position, number of letters). Lets look at an example:

alphabet$ = "ABCDE" PRINT MID$(alphabet$, 1, 2) :REM displays "AB" PRINT MID$(alphabet$, 3, 1) :REM displays "C" Every time PACMAN moves we'll use his position (row and column) to see if he overlaps a wall in the maze. Type (or edit) and RUN: (you'll have to press CTRL-BREAK to stop the program) CLS DIM maze$(6) maze$(1) = "#########" maze$(2) = "# # #" maze$(3) = "# # # # #" maze$(4) = "# # # #" maze$(5) = "# # #" maze$(6) = "#########" row = 5 column = 3 DO LOCATE 1, 1 FOR count = 1 to 6 PRINT maze$(count) :REM PRINT the maze NEXT count LOCATE row, column PRINT "C" :REM PRINT PACMAN DO keyed$ = INKEY$ LOOP UNTIL keyed$ <> "" oldRow = row :REM remember old position of PACMAN oldColumn = column IF keyed$ = "q" THEN row = row - 1 IF keyed$ = "a" THEN row = row + 1 IF keyed$ = "o" THEN column = column - 1 IF keyed$ = "p" THEN column = column + 1 IF MID$(maze$(row), column, 1) = "#" THEN row = oldRow :REM move PACMAN back to his column = oldColumn :REM old position END IF LOOP All that needs mentioning is that I used the "IF..END IF" structure to allow for more than one action.

If you type in the examples you can save yourself some time by saving the PACMAN game to disk (press ALT-F then S), because we will re-use the game during the rest of the week.

Tomorrow we'll add dots and a ghost to the maze!

Week 1, Day 4

Add the dots to the maze by changing the following lines: maze$(1) = "#########" maze$(2) = "#...#...#" maze$(3) = "#.#.#.#.#" maze$(4) = "#.#...#.#" maze$(5) = "#...#...#" maze$(6) = "#########" As you might have guessed we'll inspect the maze each time PACMAN moves and keep count of the number of dots he eat. A dot has to be removed when eaten. When our count reaches 21 (count them!) dots, the game ends.

MID$ can be used to change a string variable as well as inspecting it. Look at the following example:

alphabet$="ABCDE" MID$(alphabet$, 2, 3) = "XYZ" PRINT alphabet$ :REM displays "AXYZE" We'll use MID$ to replace the dots with blanks when PACMAN eats them. Type (or edit) and RUN: (changes in the program are marked)
(QBasic does not allow me to place a ":REM" next to a IF..THEN line. ) (So if you suspect a REM is missing, see if its next tot an IF..THEN.)

The reason why we dont do the "IF dots = 0" test directly after inspecting the maze is because we want to draw PACMAN in his new position before ending the game.

Here is a better (or clearer) way of inspecting keyed$ and maze$:

REM examine the keys SELECT CASE keyed$ CASE IS = "q" row = row - 1 CASE IS = "a" row = row + 1 CASE IS = "o" column = column - 1 CASE IS = "p" column = column + 1 END SELECT REM examine the maze SELECT CASE MID$(maze$(row), column, 1) CASE IS = "#" row = oldRow column = oldColumn CASE IS = "." MID$(maze$(row), column, 1) = " " dots = dots - 1 END SELECT SELECT makes it a lot easier to read the program. Also notice the way I put spaces before (indent) some words. Another good idea is to use blank lines to seperate the various parts of your program.

Lets place a ghost in the maze. We'll just display the ghost for now and detect whether PACMAN collides with it. Tomorrow we can make it move.

Detecting whether there's a collision is a simple matter of comparing PACMAN's and the ghost's positions (row and column). Type (or edit) and RUN:

Phew! I need a rest after that! Remember to save the program for tomorrow.

Week 1, Day 5

Lets give the ghost some "artificial intelligence".

As you might have suspected we will compare his positions with PACMAN's and move him one position nearer. Allowing the ghost to move diagonally will give it an unfair advantage so ghostRow and ghostColumn will not change at the same time. Of course the ghost will also have to be blocked by the walls of the maze. We dont have to check for dots because the ghost dont eat them.

So what are we waiting for? Type (or edit) and RUN!

Notice that I test whether ghostRow has stayed the same ( = oldRow) before testing for ghostColumn. This prevents diagonal movement for the ghost. Another problem is that the ghost waits for you to make a move before moving himself.
The loop that waits for a key to be pressed is the culprit. Now we will have to put a delay somewhere in the program because the ghost will move at blinding speed! Replace the following lines in the program and RUN it: DO keyed$ = INKEY$ LOOP UNTIL keyed$ <> "" with: keyed$ = INKEY$ REM: kill time FOR nothing = 1 TO 500 NEXT nothing If the game is still too fast then change the limit of "nothing" to 1000 or more.

The game is still too difficult because the ghost is too intelligent - it follows you at every possible opportunity. To add some randomness to its pattern we will use the command RND. The value of RND is never the same (for all practical purposes) but is always a value between 0 and 1.
The condition "IF RND < 0.1 " is true roughly 10% of the time. Here is the entire listing with the changes added: (Type (or edit) and RUN)

Another way to slow down the game is to use a variable "wait" and add 1 to it every time the program loops. Only when "wait" = 20 the ghost is allowed to move.
Remember to reset "wait" to 0 after moving, or its value will be 21, 22, etc. and never again 20!
If you use this method you must remove the FOR..NEXT loop that kills time.

Next week we'll do an RPG!

Week 2, Day 1

Our RPG will be simple, requiring you to navigate a maze and find the exit. To make things more interesting the maze will be populated with creatures that attack you.
Defeat them and you will be rewarded in gold coins. Food, weapons and other useful items will be present in some rooms. Only one room has the exit but you must pay a toll of 100 gold pieces to use it.

Lets begin by defining the maze and letting the player walk through it. (Entering "q" during play will stop the program.) Type (or edit) and RUN:

You'll notice that the maze is 10 by 10 rooms in size. I you try to move outside its boundaries you are stopped and an error message appears (I stored it in variable "moveErr$" so there's less typing).

The command INPUT prompts the user for a value and stores it in a variable (reply$). As a bonus you can display some text at the same time ("What now").

SPACE$ is a function that provides spaces; SPACE$(2) gives you " ". What the program does at that point is to overwrite any error messages that resulted from the previous user input.

"INT(RND * 10) + 1" gives a whole number ranging from 1 to 10. How does it work?
INT gives the integer value of a variable, e.g. INT(1.9) gives 1. Because RND gives a value between 0 and 1, RND * 10 will give a value between 0 and 10. And because INT(0.99) = 0 and INT(9.99) = 9, it is necessary to add 1 to the result.

Now to add an exit to the maze:

Thought you could escape? Maybe tomorrow when we add monsters and gold!

Week 2, Day 2

To make things interesting we'll use RND to scatter a variety of monsters throughout the maze. The amount of gold present in a room will depend on how difficult the monster is to beat. Type (or edit) and RUN:
I decided to allocate a number to each room to identify the type of monster in the room.
To do this I needed 10 x 10 numbers. If we keep in mind that our player's location is stored as a row and a column value, its easy to see why I decided to to use a similar method of storing the monster values.
Just by using row and column ( monster(row, column) ) you get the monster value at the player's location.

As you can see at the top of the program, I gave each location a monster value ranging from 0 to 10 (remember that INT(RND * 11) never gives 11). Why not 1 to 10?
The 0 will serve as an indicator that there is no monster at that location.

What's RESTORE and READ?
The READ command looks for DATA statements in your program and reads their values (e.g. the names of the monsters). It starts at the first DATA statement unless you use RESTORE to point it to a specific part of the program. RESTORE points to a LABEL (e.g. monsterData). You can put LABELS anywhere in your program, just remember to follow it with a colon.

Why READ the monster names when you can use: monster$(1) = "blind bat ", monster$(2) = "rat ", etc? Its just easier to find the data if its grouped together. You can then use a FOR..NEXT loop to set the values rather than typing monster(x)="abcd" every time.

One last thing about READ: it remembers the position where it last read a value , so you have to use RESTORE if you want to read from the top again.

Notice how I use the monster number (monsterType) to display its name from a monster$. So if monsterType = 1 then monster$(monsterType) = "blind bat " will be displayed.

This should be enough reading for one day!

Week 2, Day 3

What good are monsters if they dont put up a fight? I'll rectify that if you type and RUN:
Starting from the top:
The variable "health" starts of at 20 but 1 gets subtracted from it each time a monster hits you.
If it reaches 0 you've had it.
The variable "weapon" gets added to your attack score: weapon + INT(RND * 9). So the attack score will range from 1 to 9. (INT(RND * 9) gives 0 to 8)

MonsterAttack: A monster's attack score is stored as the second last character in a monster$.
VAL takes a string and converts it to a decimal value, so VAL("10") gives 10. Trying something like VAL("ABC") will cause a runtime error.

MonsterHealth: The last character READ into a monster$ is the monster's health. Each time you wound it, it loses a point of health and dies when it loses all its points.

MonsterGold: I chose to add a random factor to the amount of gold found, but by multiplying with monsterAttack you should still find more gold after defeating tougher monsters.

Notice that fighting takes place and continues until one side runs out of health.
So when this loop terminates we know that if your health > 0 then the monster's health must have run out. If the monster loses then monsterType is set to 0 so that you cannot fight it again.
Monsters(row, column) = 0 changes the location's monster to "nothing", so when you enter this location again there will be no monsters.

Can you remember that the SLEEP command is used to pause the program for a number of seconds?
SLEEP on its own waits indefinitely or until a key is pressed. I've used it to wait for a keypress. You can use DO: LOOP UNTIL INKEY$ <> "" instead of SLEEP.

Thats all for now!

Week 2, Day 4

Lets scatter some useful items like food and weapons throughout the maze. To balance things we'll add a few traps..
Starting from the top:
You have probably noticed that, although RND gives random effects, you keep getting the same set of random values! This is because the computer uses a base value (called a "seed") to start calculating RND.

RANDOMIZE sets the "seed" to a new value so the RNDs will be different. The value of TIMER is the amount of seconds since midnight. Because this value keeps changing we use it as a "seed" in the expression RANDOMIZE TIMER.

Items are addressed by using the array item(10, 10), allowing for one item per room in the maze. There's only a 20% chance of a room having an item (RND < .2) and then the item is identified by a number ranging from 1 to 9. More data on the 9 different item types is READ into item$.

CHR$ is used to convert an ASCII value into a character. So instead of doing PRINT "A", we can use PRINT CHR$(65), 65 being its ASCII value.
By using CHR$ we can access some nice characters for graphical purposes. To view these characters in QBasic: Press SHIFT-F1. Choose the "Contents" box, then choose the "ASCII character codes" box. The character with ASCII value 2 would make a nice PACMAN.

Each "item" has a name, class and value. The class shows whether its food ("F"), a weapon ("W"), or a trap("T"). The value is used differently for each class:

class: food Value: number of health points gained by player weapon new "weapon" value for player trap number of health points player will lose LEFT$(a$, 10) is the same as MID$(a$, 1, 10), in other words it just starts at the first position in the string. RIGHT$("ABCDE", 3) gives "CDE", in other words it takes the rightmost 3 characters.
You can use MID$ instead of LEFT$ and RIGHT$, but its possible to save time by using them.

The "trap" item immediately damages the player upon entering the room. The trap is then disabled by removing it from the array (item(row, column) = 0)

RTRIM$("ABC ") gives "ABC", in other words it removes the spaces on the right. LTRIM$ work in the same way, but removes spaces from the left.

I added some interesting checks for when a player takes an item. If he takes food and he has no wounds (health = 20), the food goes to waste. If he tries to take a weapon thats rated worse or the same as his present weapon, he is stopped.

This concludes our RPG.
Tomorrow I'll illustrate a few commands for generating graphics.

Week 2, Day 5

I'll demonstrate manipulation of a simple graphic by putting the screen in graphics mode (its usually in text mode), drawing the graphic, grabbing it from the screen and storing it in an array, displaying it again from the array while shifting its position from the left to the right of the screen. (I have a long breath!)
Here goes:
The REMs explain nearly everything.
Experiment with colors by choosing one of the two sets (four colors in each set) by modifying the COLOR statement. Then choose between the four available colors by modifying the LINE statement.

Why did I draw a 14x14 image but stored it as a 16x16 image? That way the stored image is surrounded by a "frame" of black dots. When moving the image around, the black "frame" deletes the previous (old) image.
So I'm displaying the image in its new position and deleting the old image at the same time!

You could make more interesting images by using the PSET command which places dots on the screen. Type (or edit) and RUN:

Storing the pixels as DATA beats doing every one with the PSET command! Try changing the picture DATA using the values 0 to 3.

Just by changing yDim and xDim and the DATA you can now design and display a bigger or smaller picture.

Thats all folks!

Hope you enjoyed the course - I can honestly say that I enjoyed writing it.

If you have any comments or questions you can E-mail me at my Internet address: avw@mickey.iaccess.za

or you can reach me by snail-mail:

A. van Wyk
105 Sidvale Court
Western Cape
South Africa

If you have found the course useful and would like to see further modules, just let me know!
Time allowing and if there is sufficient interest, I will expand on the course.

Cheers for now,