Chapter 19 - Games and SOUND
Now we've covered user-defined graphics, the next logical step would be to
combine them and make a little game. After all, what's the point in aliens if
you can't blast them to bits? Actually, my effort is a little more friendly: you
are in charge of an ore collection vessel flying through an asteroid belt in
deepest space (i.e. a black background!). You have to pick up the meteors but
avoid the aliens. We'll develop this in steps and introduce some new techniques
as we go. First we need some characters to play with. Here are mine. We can
define them then wrap them in a string with the chosen colour.
REM Meteor run |
MODE 6 |
VDU 23,220,129,219,255,255,126,60,60,24 |
VDU 23,221,24,60,118,221,251,110,60,24 |
VDU 23,222,153,189,219,126,36,60,36,36 |
REM Build characters |
Ship$=CHR$(17)+CHR$(3)+CHR$(220) |
Rock$=CHR$(17)+CHR$(4)+CHR$(221) |
Alien$=CHR$(17)+CHR$(2)+CHR$(222) |
|
REM Print to see how they look |
PRINT Ship$ |
PRINT Rock$ |
PRINT Alien$ |
|
END |
Once we've printed the characters, we need a way to make them move up the screen.
Fortunately this is easy. By printing on the bottom line, we can cause the whole
screen to scroll up, which is a cheap and cheerful way of achieving animation.
On each loop we print a meteor and an alien in a random column, by printing a
semicolon after the alien we suppress the line feed. Press ESC to finish.
REM Meteor run |
MODE 6 |
VDU 23,220,129,219,255,255,126,60,60,24 |
VDU 23,221,24,60,118,221,251,110,60,24 |
VDU 23,222,153,189,219,126,36,60,36,36 |
REM Build characters |
Ship$=CHR$(17)+CHR$(3)+CHR$(220) |
Rock$=CHR$(17)+CHR$(4)+CHR$(221) |
Alien$=CHR$(17)+CHR$(2)+CHR$(222) |
|
|
REPEAT |
WAIT 15 |
REM Print at bottom of screen |
PRINT TAB(RND(40),24);Alien$; |
PRINT TAB(RND(40),24);Rock$ |
UNTIL FALSE |
|
END |
We need to add the ship. As the screen is scrolling up it must be reprinted each
time on the top line. We can use the cursor keys to move it left and right but
need a variable to record the position. Let's switch the cursor off too so it
doesn't get in the way.
REM Meteor run |
MODE 6 |
VDU 23,220,129,219,255,255,126,60,60,24 |
VDU 23,221,24,60,118,221,251,110,60,24 |
VDU 23,222,153,189,219,126,36,60,36,36 |
REM Build characters |
Ship$=CHR$(17)+CHR$(3)+CHR$(220) |
Rock$=CHR$(17)+CHR$(4)+CHR$(221) |
Alien$=CHR$(17)+CHR$(2)+CHR$(222) |
|
X%=20 |
OFF |
REPEAT |
WAIT 15 |
REM Print at bottom of screen |
PRINT TAB(RND(40),24);Alien$; |
PRINT TAB(RND(40),24);Rock$ |
REM Reprint ship |
PRINT TAB(X%,0);Ship$ |
REM Detect ship movement |
IF INKEY(-26) AND X%>0 X%-=1 |
IF INKEY(-122) AND X%<39 X%+=1 |
UNTIL FALSE |
ON |
END |
Getting there, but there's no indication of when we hit a rock or a baddie. We
need to be able to detect the character directly in front of our ship so we can
award points or subtract a life as the case may be. When a new object is
created, we could remember the co-ordinates and update them on each loop, but
that seems like a lot of work just to test that one little square in front of
the spaceship. Fortunately, BBC BASIC is capable of reading the character back
from a given screen location. Even better, you already know the keyword involved:
GET. When supplied with a pair of co-ordinates, GET will return the ASCII code
of the character at the given location. This enables us to make our game truly
interactive. First we read the screen then test the character returned to see
if it is an alien or a meteor. As well as making the game pause for a short
while so the player knows something has happened, we can award points and remove
lives as the occasion demands. Our program now looks like this:
REM Meteor run |
MODE 6 |
VDU 23,220,129,219,255,255,126,60,60,24 |
VDU 23,221,24,60,118,221,251,110,60,24 |
VDU 23,222,153,189,219,126,36,60,36,36 |
REM Build characters |
Ship$=CHR$(17)+CHR$(3)+CHR$(220) |
Rock$=CHR$(17)+CHR$(4)+CHR$(221) |
Alien$=CHR$(17)+CHR$(2)+CHR$(222) |
|
X%=20 |
Score%=0 |
Lives%=3 |
OFF |
REPEAT |
WAIT 15 |
REM Print at bottom of screen |
PRINT TAB(RND(40),24);Alien$; |
PRINT TAB(RND(40),24);Rock$ |
REM Reprint ship |
PRINT TAB(X%,0);Ship$ |
REM Read character in front of player |
Ch%=GET(X%,1) |
IF Ch%=222 THEN |
Lives%-=1 |
WAIT 20 |
ENDIF |
IF Ch%=221 THEN |
Score%+=10 |
WAIT 20 |
ENDIF |
REM Detect ship movement |
IF INKEY(-26) AND X%>0 X%-=1 |
IF INKEY(-122) AND X%<39 X%+=1 |
UNTIL Lives%=0 |
CLS |
COLOUR 7 |
PRINT TAB(13,12);"You scored ";Score% |
ON |
END |
There's not much more to say about GET when used like this. Keep the
co-ordinates in the range of the MODE you are using, remembering that the top
left corner is 0, 0 and all will be well.
REFRESHing the screen
Depending on the speed of your computer, you may notice that the scroll /
update cycle in our game causes an irritating flicking of the characters,
especially the player's ship. This flicker can be viewed as a minor distraction
or a major annoyance, depending on how generous you feel. BB4W has a rather
neat way of eliminating it. When we write to the screen normally, BASIC and
Windows take care of actually displaying the pixels we want. This is well and
good most of the time but there are occasions when we require a little more
control. BB4W has a set of commands that relate to the operating system in the
same way that the VDU commands affect the display on the screen. All these
commands start with an asterisk (*) and if you look under 'Operating System
Interface' in help, you'll see them listed there. They are generally
referred to as star commands for reasons that I'll let you guess. We'll
concentrate on *REFRESH. Using *REFRESH we can actually tell BASIC when we want
to update the screen. First we can draw all our characters and, when
finished, say 'Right here's the complete screen, display it now'. To do this we
call *REFRESH OFF which suppresses the automatic update and then call *REFRESH
when we want to show our finished screen. Here's a simple example:
REM Using *REFRESH |
*REFRESH OFF |
REM Write some text |
PRINT "Hello, world" |
REM Wait for key press |
Dummy$=GET$ |
REM Display screen |
*REFRESH |
|
*REFRESH ON |
END |
The star commands are not recognised as keywords so the syntax
colouring doesn't pick them up. Although the text is printed to the screen, it
is not actually displayed until the *REFRESH command is reached. The
*REFRESH ON just restores the default behaviour when we no longer require the
facility. You only need to call *REFRESH OFF once. After that call *REFRESH
each time you need to update the screen.
REM Using *REFRESH |
*REFRESH OFF |
REM Write some text |
PRINT "Hello, world" |
REM Wait for key press |
Dummy$=GET$ |
REM Display screen |
*REFRESH |
|
CLS |
Dummy$=GET$ |
*REFRESH |
|
PRINT "Back again" |
Dummy$=GET$ |
*REFRESH |
|
*REFRESH ON |
END |
Applying all this to our little game, I'm sure you can see that we need to do
the scroll and redraw of the ship before the showing the screen again. Add
three lines so the result looks like this:
REM ... |
OFF |
*REFRESH OFF |
REPEAT |
WAIT 15 |
REM Print at bottom of screen |
PRINT TAB(RND(40),24);Alien$; |
PRINT TAB(RND(40),24);Rock$ |
REM Reprint ship |
PRINT TAB(X%,0);Ship$ |
*REFRESH |
REM rest of code ... |
UNTIL Lives%=0 |
*REFRESH ON |
CLS |
COLOUR 7 |
REM ... |
Try these lines in the game and notice that the whole affair is much easier on
the eye. Don't forget the final *REFRESH ON after the main loop or you won't get
to see your score. When refresh is disabled, the display sometimes leaves
ghost cursors scattered around the screen. If this is important to your
application, disable the cursor using OFF.
Introducing SOUND
I wasn't aiming for Half-Life here and what we've got isn't bad for a few lines
of code but our little game still lacks something. One way we can instantly
upgrade our epic is by adding some sound. The obvious places would be a
small explosion when we hit an alien and perhaps a little tune when we pick up
a meteor. BBC BASIC is a pretty unique programming language in that it has a
command that will allow us to make simple noises without having to distribute
WAV files that are 50 times larger than our simple program. Usually
languages allow you to make the speaker go bleep. BB4W allows you to access the
soundcard straight from native commands. Entire books have been written on SOUND
and its best friend ENVELOPE, but we'll just give the aerial view here. If you
want more, the best way is to experiment and read the help files - well worth it.
Right, let's make a simple explosion - just a burst of white noise will do.
Go into immediate mode and type:
SOUND takes four parameters. In order they are:
Channel |
values 0 to 3. 0 is for noises, as above, 1 to 3 allow musical notes. |
Volume |
values -15 (loudest) to 0 (silent) |
Pitch |
on channel 0 this is the type of noise - values are 0 to 7 on channels 1 to 3 this is the pitch of the note - value 0 to 255 |
Duration |
values -1 to 254, the length of time to play the sound in twentieths of a second. -1 plays forever. |
When using channel 0, the third parameter alters the noise produced. 0 to 3 give
buzzing noises and 4 to 7 give white noise or hiss. Here are some examples to
start you off:
SOUND 0,-5,4,20 |
SOUND 0,-15,0,100 |
SOUND 0,-10,3,-1 |
There's no actual difference between the white noise generated with pitch
set from 4 to 7.
To play a tune, we use channels 1 to 3. The channels are the same in terms of
capacity to play notes, they all have the same range. There are three so you can
play chords and counter-melodies if you wish. As the second parameter (volume)
and the fourth (duration) retain the same function, we only need to concentrate
on the pitch. Values range from 0 to 255, but with 0 you don't get a sound, so
realistically the lowest pitch is 1 up to the highest of 255. Try:
SOUND 1,-15,1,20 |
SOUND 1,-15,255,20 |
Increasing the pitch by one raises the pitch by one quarter of a semitone,
if you understand such things. Put another way, the difference between
two consecutive frets on a guitar or two consecutive notes on a keyboard is
equal to four increments of pitch in the SOUND statement. There is a complete
list of the values for all the available notes in the help files under SOUND and
if you're writing tunes, you'll need to get a feel for these values. For our
part, we just want a little three-note sequence like this:
SOUND 1,-15,200,2 |
SOUND 1,-15,208,2 |
SOUND 1,-15,212,2 |
Put the noise in the alien collision, the tune in the meteor section and here,
at last, is our complete game.
REM Meteor run |
MODE 6 |
VDU 23,220,129,219,255,255,126,60,60,24 |
VDU 23,221,24,60,118,221,251,110,60,24 |
VDU 23,222,153,189,219,126,36,60,36,36 |
REM Build characters |
Ship$=CHR$(17)+CHR$(3)+CHR$(220) |
Rock$=CHR$(17)+CHR$(4)+CHR$(221) |
Alien$=CHR$(17)+CHR$(2)+CHR$(222) |
|
X%=20 |
Score%=0 |
Lives%=3 |
OFF |
*REFRESH OFF |
REPEAT |
WAIT 15 |
REM Print at bottom of screen |
PRINT TAB(RND(40),24);Alien$; |
PRINT TAB(RND(40),24);Rock$ |
REM Reprint ship |
PRINT TAB(X%,0);Ship$ |
*REFRESH |
REM Read character in front of player |
Ch%=GET(X%,1) |
IF Ch%=222 THEN |
SOUND 0,-15,4,10 |
Lives%-=1 |
WAIT 20 |
ENDIF |
IF Ch%=221 THEN |
SOUND 1,-15,200,2 |
SOUND 1,-15,208,2 |
SOUND 1,-15,212,2 |
Score%+=10 |
WAIT 20 |
ENDIF |
REM Detect ship movement |
IF INKEY(-26) AND X%>0 X%-=1 |
IF INKEY(-122) AND X%<39 X%+=1 |
UNTIL Lives%=0 |
*REFRESH ON |
CLS |
COLOUR 1 |
PRINT TAB(13,12);"You scored ";Score% |
ON |
END |
Notice that with the SOUND statements we still have to put a WAIT in there. BASIC
doesn't halt the execution of the code until the duration of the sound has
expired. Instead it places the instructions in a queue and continues on its way.
The sound queue is then processed at its own rate.
Most of what you can do with SOUND involves experimentation and imagination. If
you're interested investigate the entries in help. They tell you how to
synchronise the sounds on different channels so you can play chords and how to
alter the 'shape' of the sounds using ENVELOPE. Here are a couple of effects to
whet your appetite:
REM Siren |
|
Inc%=1 |
Pitch%=100 |
FOR I%=1 TO 20 |
IF I%=10 Inc%=-1 |
Pitch%+=Inc% |
SOUND 1,-15,Pitch%,2 |
NEXT I% |
|
END |
|
REM Modem |
|
FOR I%=1 TO 10 |
SOUND 1,-15,RND(4)*4+150,1 |
SOUND 1,-15,0,2 |
WAIT 3 |
NEXT I% |
|
SOUND 0,-15,2,15 |
SOUND 0,-15,0,15 |
SOUND 0,-15,4,25 |
|
END |
I would like to emphasize here that this is not the limits of BBC BASIC in terms of
graphics and if you are interested in games, there is even an entire sprite library.
Using these simple techniques, however, you can liven up your programs quite considerably.
Once upon a time, the computer world was awash with little programs like our
game, basically because the micros of the day didn't have enough memory to do
anything else! I believe there is still a place for fun programs like this. Just
because you have gigabytes of memory doesn't mean
your program is better for using as much as possible. As you progress as a
programmer, your programs will get bigger. How to plan and develop these is the
subject of the next chapter. In the meantime, you can have a lot of fun
practising BASIC by concocting little programs such as this.
Exercises
1) Create a sound effect that uses short bursts of hiss to generate a noise like
a machine gun.
2) Here is an incomplete game:
REM Chicane |
MODE 6 |
VDU 23,220,255,213,171,213,171,213,171,255 |
VDU 23,221,90,126,90,24,90,126,90,24 |
Road$=CHR$(17)+CHR$(3)+STRING$(4,CHR$(220)) |
Car$=CHR$(17)+CHR$(4)+CHR$(221) |
|
CarX%=20 |
RoadX%=18 |
Score%=0 |
Lives%=5 |
Count%=0 |
OFF |
REM Fill screen before we start |
FOR I%=1 TO 24 |
WAIT 20 |
PRINT TAB(RoadX%,24);Road$ |
NEXT I% |
*REFRESH OFF |
REM Main loop |
REPEAT |
WAIT 20 |
REM Score |
Score%+=1 |
|
REM Create new road |
RoadX%=RoadX%+RND(3)-2 |
REM Keep on screen |
IF RoadX%<5 THEN RoadX%=5 |
IF RoadX%>30 THEN RoadX%=30 |
PRINT TAB(RoadX%,24);Road$; |
|
PRINT TAB(0,24) |
PRINT TAB(CarX%,0);Car$ |
*REFRESH |
|
REM Add car control code here ... |
|
UNTIL Lives%=0 |
|
FOR I%=200 TO 150 STEP -1 |
SOUND 1,-15,I%,1 |
WAIT 1 |
NEXT I% |
*REFRESH ON |
CLS |
COLOUR 7 |
PRINT TAB(6,12);"You scored ";Score% |
ON |
END |
Try it. You will see a road snake its way up the screen. Your task is to add the
lines of code that control the car. This should be added where indicated.
Inspect the left and right keys and adjust the position, CarX%, accordingly. Keep
the car in the range of 5 to 34. Examine the character in front of the car. If
it is not 220 (the road character), make a noise and lose a life.
© Peter Nairn 2006