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.
Hints?
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:
- QBasic is bundled with MSDOS 5 and higher so you dont have to fork out
hundreds of dollars to get started.
- 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.
- 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 ""
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
Parow
7500
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,
Andre