Lesson 7: Gathering Input
QB64 offers a wide variety of gathering input from users that will eventually play your games. Input can be gathered from keyboard, joysticks, the mouse, and game pads. This lesson will cover keyboard and mouse input methods since those are the two primary means every computer user has at their direct disposal.
Keyboard Input: The INPUT Statement
Up to this point the only way we've gathered input from the user is through the use of the INPUT statement. The problem with the INPUT statement is that you must wait for the user to press the ENTER key before the program will continue. This does not make for very exciting, action packed games.
As a recap from lesson 2 the INPUT statement is a simple way of getting information from the user through the keyboard. The INPUT statement allows the programmer to gather both string and numeric information from the user depending on the type of variable assigned to the statement. The INPUT statement by itself will always force a question mark to the screen, as seen in these examples:
INPUT Test$ ' question mark then flashing cursor on the screen
PRINT "Enter your name ";
INPUT UserName$ ' question mark at end of previous text
However, by using INPUT's optional text print feature you can get rid of the forced question mark.
INPUT "Enter your name ", UserName$ ' no question mark just a flashing cursor
INPUT "Enter your age ", Age%
If the INPUT statement expects a numeric value, such as in the second line above, QB64 helps out by only allowing the user to type in numeric values. If the numeric value is expected to be an integer QB64 will also block the decimal point from being entered by the user. These features were added by QB64 and were not available in earlier versions of BASIC such as Microsoft's QuickBasic and GWBasic. The INPUT statement will also truncate, or trim off, any leading or trailing spaces the user may have typed in. The INPUT statement also supports multiple variable input on one line like so:
INPUT "Enter your name, followed by a comma, then your age ", UserName$, Age%
This method of using INPUT can be tricky for end users to fill out correctly and is rarely, if ever, used.
Keyboard Input: The LINE INPUT Statement
The LINE INPUT statement is used to enter literal strings of information from the keyboard regardless of punctuation. The only type of variable that can be used with the LINE INPUT statement is a string. If the user enters a numeric value the text will be seen as a literal string and not numeric information. Your program will need to convert the string to a numeric value. There are commands available that do the conversion and will be discussed in a later lesson. The LINE INPUT statement will never force a question mark on the screen either.
LINE INPUT Test$ ' a flashing cursor on the screen with no question mark
LINE INPUT "Enter your name :", UserName$ ' flashing cursor after the text string
The text saved into the variable will retain all punctuation marks and any leading or trailing spaces that the user may have entered as well.
Keyboard Input: The INKEY$ Statement
Most games need real-time user input from the keyboard to be effective. For example, if the user presses the "W" key the player moves forward, the "S" key and the player moves backward, and so on. The INPUT statement and its variations are no good for this kind of interaction. This is where the INKEY$ statement comes into play. Let's start off with a simple example program that we'll investigate in detail.
( This code can be found at .\tutorial\Lesson7\InkeyDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' set colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM KeyPress$ ' the key user is pressing
DIM x%, y% ' x,y coordinate of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
DO ' begin main loop
CLS ' clear the screen
_LIMIT 30 ' keep loop at 30 frames per second
LOCATE 2, 25 ' position text cursor
PRINT "Use WASD to move circle around" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw bright green circle
PAINT (x%, y%), GREEN, BRIGHTGREEN ' fill in with green
KeyPress$ = INKEY$ ' grab any key being pressed
IF KeyPress$ = "w" THEN y% = y% - 1 ' if w then decrement y
IF KeyPress$ = "s" THEN y% = y% + 1 ' if s then increment y
IF KeyPress$ = "a" THEN x% = x% - 1 ' if a then decrement x
IF KeyPress$ = "d" THEN x% = x% + 1 ' if d then increment x
IF y% < 49 THEN y% = 49 ' stop circle at top edge
IF y% > 429 THEN y% = 429 ' stop circle at bottom edge
IF x% < 49 THEN x% = 49 ' stop circle at left edge
IF x% > 589 THEN x% = 589 ' stop circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL KeyPress$ = CHR$(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
Figure 1: INKEY$ Demonstration
After playing around with the program I'm sure you've found the limitations with INKEY$. If you hold one of the direction keys down there is a slight pause before the key press will repeat. Also, holding two keys down to go diagonal does not work. This behavior may be acceptable in some slow-paced games but for the majority of them this will cause issues. INKEY$ is the perfect type of input for things like scrolling through a menu where single keystrokes are expected or looking for the Y and N keys when answering a Yes or No question.
The reason for this is that your computer maintains a speed limit on keyboard input. The delay you are experiencing is to keep accidental key repeats from happening in programs such as word processors. The computer also limits the key repeat speed after the mandatory delay to no more than 30 characters per second. These settings can typically be found in your computer's BIOS setup program that can be accessed when you first turn your computer on. These limitations were put in place with the advent of the IBM-AT back in 1983 and BASIC's INKEY$ command used this BIOS area to read keystrokes from. Back in the early days of BASIC you would need to read special areas of the ROM to get around this which required cryptic code to do so.
Your computer's BIOS also maintains a buffer where keystrokes are placed until read by the program that needs them. Back in the days of the IBM PC/XT/AT it was quite possible to type faster than a program could keep up with. This BIOS buffer helped to alleviate this issue. Once again the INKEY$ statement is limited by this BIOS action.
Line 27 of the example program:
KeyPress$ = INKEY$ ' grab any key being pressed
is grabbing a character from the buffer area that BIOS maintains. When using the INKEY$ statement you must grab a character and assign it to a string variable. If you were to do this to lines 28 through 31 of the code:
IF KeyPress$ = "w" THEN y% = y% - 1 ' if w then decrement y
IF KeyPress$ = "s" THEN y% = y% + 1 ' if s then increment y
IF KeyPress$ = "a" THEN x% = x% - 1 ' if a then decrement x
IF KeyPress$ = "d" THEN x% = x% + 1 ' if d then increment x
the code would not work. The INKEY$ statement in the first line would grab the first character in the buffer and from that point on INKEY$ would always equal that character. You must assign the value that INKEY$ obtains from the keyboard buffer to a string variable right away.
The example program highlights how INKEY$ is usually used within a loop construct. Each time through the loop the keyboard buffer is read by INKEY$ and then that string value placed into a string variable. If there are no keys left in the keyboard buffer INKEY$ will return a null string ( "" ) indicating no key presses to be read.
A rule of thumb when using INKEY$ is that if a key being pressed results in a character that can be printed to the screen the character itself can be used for the test. For example, in the example code, the keys w, a, s, and d all result in letters that can be printed to the screen so this works:
KeyPress$ = INKEY$ ' grab any key being pressed
IF KeyPress$ = "w" THEN y% = y% - 1 ' if w then decrement y
IF KeyPress$ = "s" THEN y% = y% + 1 ' if s then increment y
IF KeyPress$ = "a" THEN x% = x% - 1 ' if a then decrement x
IF KeyPress$ = "d" THEN x% = x% + 1 ' if d then increment x
You can simply test for the literal strings "w", "s", "a", and "d". However, how would you test for the TAB, Back Space, ENTER, or even the Escape keys? These keys can be tested with INKEY$ but you'll need to test for the ASCII value of the key instead of the string character using the CHR$() statement.
DO
KeyPress$ = INKEY$
IF KeyPress$ = CHR$(13) THEN PRINT "ENTER key pressed"
IF KeyPress$ = CHR$(9) THEN PRINT "TAB key pressed"
IF KeyPress$ = CHR$(8) THEN PRINT "BACKSPACE key pressed"
IF KeyPress$ = CHR$(27) THEN PRINT "ESCAPE key pressed"
LOOP UNTIL KeyPress$ = CHR$(27) ' leave loop when escape key pressed
ASCII stands for American Standard Code for Information Interchange and was developed in the 1950's as a way for different computers by different manufacturers to have a common set of characters that all computers would recognize. When information is sent from one computer to another through a network, such as phone lines or Ethernet, the data received is compared to a built in ASCII chart in the computer's ROM. This ensures that if the sending computer sends the letter "A", or ASCII code 65, the receiving computer sees this number 65 as an "A" as well. Every key on a keyboard can be represented by an ASCII code number so all programmers have an ASCII chart handy when needed. (some keys use a combination of two ASCII numbers which will be discussed later). You can view an ASCII chart here in the QB64 Wiki.
INKEY$ is perfect for handling situations where real-time input is needed but its shortcomings are irrelevant. This example menu program shows how INKEY$ can be used effectively.
( This code can be found at .\tutorial\Lesson7\InkeyMenu.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM KeyPress$ ' hold the value of a user key press
DIM Highlight% ' the current highlighted menu entry
DIM Selected% ' the selected menu entry
DIM Count% ' generic counter
'----------------------------
'- Main Program Begins Here -
'----------------------------
Highlight% = 1 ' set initial highlighted entry
Selected% = 0 ' no entry selected yet
LOCATE 2, 30 ' position text cursor on screen
PRINT "Example Menu System" ' print menu title
LOCATE 13, 22 ' position text cursor on screen
PRINT "Use UP/DOWN arrow keys to highlight" ' print instructions to user
LOCATE 14, 24 ' position text cursor
PRINT "Press ENTER to make a selection" ' print more instructions to user
DO ' begin main loop
FOR Count% = 1 TO 5 ' create 5 dummy menu entries
LOCATE Count% + 5, 32 ' position text cursor on screen
IF Highlight% = Count% THEN ' is this the highlighted entry?
COLOR 30, 1 ' yes, text flashing yellow on blue
ELSE ' no
COLOR 14, 0 ' text yellow on black
END IF
PRINT " Menu Option"; Count% ' display the menu entry
NEXT Count%
DO ' begin key press loop
KeyPress$ = INKEY$ ' get key from buffer
_LIMIT 30 ' 30 loops per second (don't hog CPU)
LOOP UNTIL KeyPress$ <> "" ' loop back if no key pressed (null)
IF KeyPress$ = CHR$(13) THEN ' did user press ENTER?
Selected% = Highlight% ' yes, remember which entry selected
ELSEIF KeyPress$ = CHR$(0) + "H" THEN ' no, did user press UP ARROW?
IF Highlight% <> 1 THEN ' yes, already on first entry?
Highlight% = Highlight% - 1 ' no, move up one entry
END IF
ELSEIF KeyPress$ = CHR$(0) + "P" THEN ' no, did user press DOWN ARROW?
IF Highlight% <> 5 THEN ' yes, already on last entry?
Highlight% = Highlight% + 1 ' no, move down one entry
END IF
END IF
LOOP UNTIL Selected% <> 0 ' loop back if nothing selected
LOCATE 18, 2 ' position text cursor
COLOR 15, 0 ' text bright white on black
PRINT "You chose menu option"; Selected% ' report which entry chosen
Figure 2: An INKEY$ driven menu
Some keys are read by INKEY$ as two value combinations that start with CHR$(0). Keys like the keyboard arrow keys were not around when the ASCII code was created so they do not have their own place in the ASCII chart to draw a value from. The work-around for this in BASIC was to create a two value combination set that started with the null character CHR$(0) and then a number afterwards. In the example code above the up and down arrow keys are detected by these two value sets.
ELSEIF KeyPress$ = CHR$(0) + "H" THEN ' no, did user press UP ARROW?
and
ELSEIF KeyPress$ = CHR$(0) + "P" THEN ' no, did user press DOWN ARROW?
A listing of these special two value combinations can be found in the QB64 Wiki under the INKEY$ entry.
Keyboard Input: The _KEYHIT Statement
_KEYHIT is an improved form of INKEY$ introduced by QB64. _KEYHIT has the same keyboard buffer, repeat delay, and speed limit imposed by BIOS but with a trick under its sleeve. First let's modify the example code above to use _KEYHIT instead of INKEY$.
( This code can be found at .\tutorial\Lesson7\KeyhitDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' set colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM KeyPress& ' the key user is pressing
DIM x%, y% ' x,y coordinate of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
DO ' begin main loop
CLS ' clear the screen
LOCATE 2, 25 ' position text cursor
PRINT "Use WASD to move circle around" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw bright green circle
PAINT (x%, y%), GREEN, BRIGHTGREEN ' fill in with green
KeyPress& = _KEYHIT ' get key user is pressing
IF KeyPress& = 119 THEN y% = y% - 1 ' if w then decrement y
IF KeyPress& = 115 THEN y% = y% + 1 ' if s then increment y
IF KeyPress& = 97 THEN x% = x% - 1 ' if a then decrement x
IF KeyPress& = 100 THEN x% = x% + 1 ' if d then increment x
IF y% < 49 THEN y% = 49 ' stop circle at top edge
IF y% > 429 THEN y% = 429 ' stop circle at bottom edge
IF x% < 49 THEN x% = 49 ' stop circle at left edge
IF x% > 589 THEN x% = 589 ' stop circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
At this point you're probably thinking, "Improved? It's exactly the same as the INKEY$ demo!" We'll get to that in a second. First, notice that _KEYHIT returns a long integer numeric value instead of string characters like INKEY$. In the code above we are placing the _KEYHIT value into a declared long integer variable called KeyPress&.
KeyPress& = _KEYHIT ' get key user is pressing
The values returned by _KEYHIT can be found in the QB64 Wiki under the _KEYHIT entry. Notice that instead of needing two value combinations that were required by INKEY$ for the arrow keys there is one single value returned instead. The up arrow key returns 18432 and the down arrow key returns 20480.
Ok, now for _KEYHIT's trick. It can also tell you when a key has been released. Let's modify our code once again to take advantage of this new feature.
( This code can be found at .\tutorial\Lesson7\BetterKeyhitDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' set colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM KeyPress& ' the value of the key user is pressing
DIM x%, y% ' x,y coordinate of circle
DIM GoUp% ' status of w key
DIM GoDown% ' status of s key
DIM GoLeft% ' status of a key
DIM GoRight% ' status of d key
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
DO ' begin main loop
CLS ' clear the screen
_LIMIT 240 ' limit loop to 240 frames per second
LOCATE 2, 25 ' position text cursor
PRINT "Use WASD to move circle around" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw bright green circle
PAINT (x%, y%), GREEN, BRIGHTGREEN ' fill in with green
KeyPress& = _KEYHIT ' get key the user is pressing
IF KeyPress& = 119 THEN GoUp% = 1 ' remember when w key pressed
IF KeyPress& = -119 THEN GoUp% = 0 ' remember when w key released
IF KeyPress& = 115 THEN GoDown% = 1 ' remember when s key pressed
IF KeyPress& = -115 THEN GoDown% = 0 ' remember when s key released
IF KeyPress& = 97 THEN GoLeft% = 1 ' remember then a key pressed
IF KeyPress& = -97 THEN GoLeft% = 0 ' remember when a key released
IF KeyPress& = 100 THEN GoRight% = 1 ' remember when d key pressed
IF KeyPress& = -100 THEN GoRight% = 0 ' remember when d key released
IF GoUp% = 1 THEN y% = y% - 1 ' if w key then decrement y
IF GoDown% = 1 THEN y% = y% + 1 ' if s key then increment y
IF GoLeft% = 1 THEN x% = x% - 1 ' if a key then decrement x
IF GoRight% = 1 THEN x% = x% + 1 ' if d key then increment x
IF y% < 49 THEN y% = 49 ' stop circle at top edge
IF y% > 429 THEN y% = 429 ' stop circle at bottom edge
IF x% < 49 THEN x% = 49 ' stop circle at left edge
IF x% > 589 THEN x% = 589 ' stop circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
Woohoo! Look at that circle fly around the screen now. In fact the code had to have a _LIMIT 240 statement placed in it to slow the circle down! Better yet, you can press multiple keys to go diagonally.
_KEYHIT will return a negative value when the key has been released. Four new integer variables have been introduced into the code: GoUp%, GoDown%, GoLeft%, and GoRight%. These variables are being used as latches. When one of the four keys has been pressed the corresponding latch variable gets set to 1. When the key has been released the latch variable gets set to 0. As long as the variable is set to 1 the circle will continue to move in that direction. Only when the key is released will the latch variable get set to 0 stopping the circle's motion. The reason the circle can go in a diagonal direction is that _KEYHIT can see when keys have been pressed even while others are still pressed down. Nice trick!
Keyboard Input: The _KEYDOWN Statement
The _KEYDOWN statement removes the BIOS limitations from the keyboard equation all together. _KEYDOWN scans the keyboard hardware directly but unlike _KEYHIT which returns the key being pressed you have to specifically check each key for interaction. Let's once again modify the example code to use the _KEYDOWN statement.
( This code can be found at .\tutorial\Lesson7\KeydownDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' set colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM x%, y% ' x,y coordinate of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
DO ' begin main loop
CLS ' clear the screen
_LIMIT 240 ' limit loop to 240 frames per second
LOCATE 2, 25 ' position text cursor
PRINT "Use WASD to move circle around" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw bright green circle
PAINT (x%, y%), GREEN, BRIGHTGREEN ' fill in with green
IF _KEYDOWN(119) THEN y% = y% - 1 ' if w then decrement y
IF _KEYDOWN(115) THEN y% = y% + 1 ' if s then increment y
IF _KEYDOWN(97) THEN x% = x% - 1 ' if a then decrement x
IF _KEYDOWN(100) THEN x% = x% + 1 ' if d then increment x
IF y% < 49 THEN y% = 49 ' stop circle at top edge
IF y% > 429 THEN y% = 429 ' stop circle at bottom edge
IF x% < 49 THEN x% = 49 ' stop circle at left edge
IF x% > 589 THEN x% = 589 ' stop circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
_KEYDOWN will only detect when a key is currently being held down. Furthermore, you always have to identify the key you wish to check for using either the ASCII value of the key or the special number assigned to it. The values you can check for are listed in the QB64 Wiki under the _KEYDOWN entry. Just like with _KEYHIT you can press multiple keys and _KEYDOWN will recognize them even if other keys are currently down. The _KEYDOWN statement is usually the preferred keyboard input method for fast-paced action games.
Keyboard Input: The SLEEP Statement
The SLEEP statement is used to pause a program's execution for a given time. However, it has the curious side effect of being affected by keyboard input. The syntax for the SLEEP statement is:
SLEEP Seconds%
The optional Seconds% parameter pauses program execution for the number of seconds provided.
PRINT "Program execution will continue in 3 seconds..."
SLEEP 3 ' pause program execution for 3 seconds
If the Seconds% parameter is omitted program execution will pause indefinitely or until a key is pressed on the keyboard.
PRINT "Press any key to continue..."
SLEEP ' pause program execution until a key is pressed
The SLEEP statement can also be interrupted by a key press even if a Seconds% parameter is specified.
PRINT "Program execution will continue in 10 seconds."
PRINT "Or you can press any key to continue within the 10 second wait."
SLEEP 10 ' pause program execution for 10 seconds or a key is pressed
The SLEEP statement uses very little CPU resources while executing allowing other programs to share processor time.
Something to take note of when using the SLEEP statement is that key presses are not cleared from the keyboard buffer. For instance, if a program uses the escape key to exit and the user presses the escape key to interrupt a SLEEP statement, that escape key press is passed on:
PRINT "Press the ESC key to exit the program at any time."
PRINT
PRINT "Counting from 1 to 10"
Count% = 1 ' set counter
CONTINUE: ' GOTO returns here
PRINT Count% ' print current value
PRINT "Press any key for the next value." ' instruct user
SLEEP ' wait for a key press
A$ = INKEY$ ' get a key from the keyboard buffer
IF A$ = CHR$(27) THEN END ' end program if the ESC key was pressed
Count% = Count% + 1 ' increment counter
IF Count% < 11 THEN GOTO CONTINUE ' go back if count is not finished
The reason the escape key can be pressed at any time to exit is because the SLEEP statement leaves each key press in the keyboard buffer for the INKEY$ statement to grab. If the SLEEP statement were to clear the buffer then the INKEY$ statement would never have a key in the keyboard buffer to grab.
Mouse Input: _MOUSEX and _MOUSEY
Original versions of BASIC did not have mouse support built in so programmers had to get creative with cryptic code to use a mouse in their software. Fortunately QB64 had added a full range of mouse commands that are very easy to use. The _MOUSEX and _MOUSEY statements return the current x and y coordinates of the mouse pointer within the program screen. Here again is the green circle example modified to use the mouse instead of the keyboard to move the circle around.
( This code can be found at .\tutorial\Lesson7\MouseXYDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' set colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM x%, y% ' x,y coordinate of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
_MOUSEHIDE ' Make mouse pointer invisible
DO ' begin main loop
CLS ' clear the screen
LOCATE 2, 25 ' position text cursor
PRINT "Use MOUSE to move circle around" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw bright green circle
PAINT (x%, y%), GREEN, BRIGHTGREEN ' fill in with green
DO WHILE _MOUSEINPUT ' get latest mouse information
LOOP
x% = _MOUSEX ' retrieve mouse X position
y% = _MOUSEY ' retrieve mouse Y position
IF y% < 49 THEN y% = 49 ' keep circle at top edge
IF y% > 429 THEN y% = 429 ' keep circle at bottom edge
IF x% < 49 THEN x% = 49 ' keep circle at left edge
IF x% > 589 THEN x% = 589 ' keep circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
In the example above the _MOUSEHIDE statement is used to hide the operating system's pointer from view. Without this command in place you would see the mouse icon your operating system provides hovering over the circle the entire time.
The mouse routines in QB64 use a buffer to store all mouse activity between mouse command calls. The user may be clicking around on the screen faster than your program can keep up. By storing the mouse movements and click events into a buffer your program can retrieve the series of mouse events as your user intended them. The _MOUSEINPUT statement is used to get the next mouse event in the buffer ready for retrieving. As long as _MOUSEINPUT returns a non-zero value there is data in the buffer to be retrieved. Lines 26 and 27 in the example above are used to clear the mouse buffer.
DO WHILE _MOUSEINPUT ' get latest mouse information
LOOP
As long as _MOUSEINPUT is returning a non-zero value the loop will continue to cycle. As soon as _MOUSEINPUT equals zero the loop will end effectively clearing the mouse buffer. The reason for doing this is to ensure only the latest mouse information is retrieved with the next mouse related command. If however you absolutely must know every mouse interaction at all times you can read the buffer in the manner outlined in the example below.
( This code can be found at .\tutorial\Lesson7\MouseBuffer.bas )
'-------------------------------------
'- Reading the mouse buffer contents -
'-------------------------------------
CONST WHITE = _RGB32(255, 255, 255) ' define white
DIM KeyPress$ ' user input when asked to press ENTER
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
CLS
PRINT
INPUT " Click around on the screen a few times then press the ENTER key", KeyPress$
WHILE _MOUSEINPUT ' is there mouse data in the buffer?
IF _MOUSEBUTTON(1) THEN ' yes, is this data related to the left mouse button?
LINE -(_MOUSEX, _MOUSEY), WHITE ' yes, draw a line to mouse position where click happened
END IF
WEND ' leave loop only when no mouse data remains in buffer
PRINT " Your pattern of clicks is now displayed"
Figure 3: Reading mouse buffer contents
Note: It's important to remember that _MOUSEINPUT must be called first before a mouse related command can be used to retrieve any mouse related information.
Mouse Input: The _MOUSEBUTTON Statement
_MOUSEBUTTON is used to retrieve the status of mouse buttons. Here's the circle example again but this time modified to use both the left and right mouse buttons.
( This code can be found at .\tutorial\Lesson7\MousebuttonDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST GREEN = _RGB32(0, 127, 0) ' define colors
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
CONST RED = _RGB32(127, 0, 0)
CONST BRIGHTRED = _RGB32(255, 0, 0)
DIM x%, y% ' x,y coordinate of circle
DIM LeftClick% ' -1 when left button clicked, 0 when not
DIM RightClick% ' -1 when right button clicked, 0 when not
DIM CircleColor~& ' color of circle
DIM PaintColor~& ' inside paint color of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
CircleColor~& = BRIGHTGREEN ' set initial circle color
PaintColor~& = GREEN ' set initial paint color
_MOUSEHIDE ' Make mouse pointer invisible
DO ' begin main loop
CLS ' clear the screen
LOCATE 2, 25 ' position text cursor
PRINT "Use MOUSE to move circle around" ' print user instructions
LOCATE 3, 22 ' position text cursor
PRINT "Mouse buttons to change circle color" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, CircleColor~& ' draw circle
PAINT (x%, y%), PaintColor~&, CircleColor~& ' fill circle
DO WHILE _MOUSEINPUT ' get latest mouse information
LOOP
x% = _MOUSEX ' retrieve mouse X position
y% = _MOUSEY ' retrieve mouse Y position
LeftClick% = _MOUSEBUTTON(1) ' retrieve left button status
RightClick% = _MOUSEBUTTON(2) ' retrieve right button status
IF LeftClick% THEN ' was left button clicked?
CircleColor~& = BRIGHTGREEN ' yes, set circle color (bright green)
PaintColor~& = GREEN ' set circle fill color (green)
ELSEIF RightClick% THEN ' no, was right button clicked?
CircleColor~& = BRIGHTRED ' yes, set circle color (bright red)
PaintColor~& = RED ' set circle fill color (red)
END IF
IF y% < 49 THEN y% = 49 ' keep circle at top edge
IF y% > 429 THEN y% = 429 ' keep circle at bottom edge
IF x% < 49 THEN x% = 49 ' keep circle at left edge
IF x% > 589 THEN x% = 589 ' keep circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
The left mouse button is designated as button 1, the right mouse button as button 2, and the middle mouse button as button number 3. If a mouse has more than three buttons then mouse button numbers 4 and above will be assigned to them. In lines 40 and 41 of the example code the current status of the left and right mouse buttons are being saved.
LeftClick% = _MOUSEBUTTON(1) ' retrieve left button status
RightClick% = _MOUSEBUTTON(2) ' retrieve right button status
If either button is clicked the value of -1 will be saved into the associated integer variable. The variables will contain a value of zero if the mouse button is not pressed.
Mouse Input: The _MOUSEWHEEL Statement
If a mouse is equipped with a scroll wheel then QB64 can read its value as well. Type in the following example code that uses the scroll wheel to change the circle's color intensity.
( This code can be found at .\tutorial\Lesson7\WheelDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
CONST BRIGHTGREEN = _RGB32(0, 255, 0)
DIM Green% ' green intensity of circle
DIM x%, y% ' x,y coordinate of circle
'----------------------------
'- Main Program Begins Here -
'----------------------------
SCREEN _NEWIMAGE(640, 480, 32) ' initiate graphics screen
x% = 319 ' set x,y circle coordinates
y% = 239
_MOUSEHIDE ' Make mouse pointer invisible
Green% = 127 ' set green intensity
DO ' begin main loop
CLS ' clear the screen
LOCATE 2, 25 ' position text cursor
PRINT "Use MOUSE to move circle around" ' print user instructions
LOCATE 3, 22 ' position text cursor
PRINT "Use scroll wheel to change intensity" ' print user instructions
LOCATE 29, 27 ' position text cursor
PRINT "Press ESC to leave program"; ' print user instructions
CIRCLE (x%, y%), 50, BRIGHTGREEN ' draw circle
PAINT (x%, y%), _RGB32(0, Green%, 0), BRIGHTGREEN ' fill circle
DO WHILE _MOUSEINPUT ' get latest mouse information
Green% = Green% - _MOUSEWHEEL * 16 ' set color according to wheel movement
IF Green% > 254 THEN Green% = 254 ' keep green color within limits
IF Green% < 0 THEN Green% = 0
LOOP
x% = _MOUSEX ' retrieve mouse X position
y% = _MOUSEY ' retrieve mouse Y position
IF y% < 49 THEN y% = 49 ' keep circle at top edge
IF y% > 429 THEN y% = 429 ' keep circle at bottom edge
IF x% < 49 THEN x% = 49 ' keep circle at left edge
IF x% > 589 THEN x% = 589 ' keep circle at right edge
_DISPLAY ' update changes to the screen
LOOP UNTIL _KEYDOWN(27) ' exit program if ESC pressed
SYSTEM ' return to Windows
_MOUSEWHEEL will return a value of -1 when being scrolled up, 0 if there is no movement, and 1 when being scrolled down. It's best to check for _MOUSEWHEEL events within a tight mouse buffer loop like the example above due to the nature of rapid changes when a mouse's wheel is spun.
Joystick/Game Pad Input: The STICK Statement
The STICK statement is used to return the directional axis coordinate values of joysticks and game pads. Many of today's joysticks and game pads have multiple direction axis inputs (joystick, D-pad, top hats, throttle input, Z rotational axis on the joystick, etc...) and the STICK statement has the capability to read each one of them.
Back in the QuickBasic days creating a game that utilized a joystick was fairly simple. Joysticks were primitive and most of the time simply contained 2 directional axis inputs (the joystick itself) and maybe one or two buttons. Today's joysticks and game pads however often contain a myriad of axis inputs and buttons to choose from varying wildly between different makes and models. The big game studios will often program separate input variables for the most popular game controllers (a driver). This ensures that the game controller you have will work "out of the box" with their game with minimal to no configuration needed. As a QB64 programmer you'll often need to write software that performs a bit of investigative work to discover the capabilities of the game controllers connected to the system. This often requires getting the player involved in the process: "Press UP on the joystick now", "Press the FIRE button now" to help your game configure itself for the player's controller.
If you have not done so yet you'll need to connect a game pad or joystick to your system. If you have more than one game controller I encourage you to connect a few of them to get the most out of the demo programs to follow. The STICK statement can read joysticks plugged into USB or the 15 pin "game port" that was found on most sound cards back in the day.
( This code can be found at .\tutorial\Lesson7\StickTest.bas )
DO
_LIMIT 30 ' don't hog the CPU
LOCATE 2, 1
PRINT " Joystick Axes Test"
PRINT
PRINT " --------------------------------------------------------------------------"
PRINT " X,Y Axes 1,2 - the main Joystick or game pad 'plus' style input"
PRINT " --------------------------------------------------------------------------"
PRINT " X Axis 1:"; STICK(0, 1); " "
PRINT " Y Axis 2:"; STICK(1, 1); " "
PRINT " --------------------------------------------------------------------------"
PRINT " X,Y Axes 3,4 - a twisting joystick handle, joystick throttle, or 'top hat'"
PRINT " --------------------------------------------------------------------------"
PRINT " X Axis 3:"; STICK(0, 2); " "
PRINT " Y Axis 4:"; STICK(1, 2); " "
PRINT " --------------------------------------------------------------------------"
PRINT " X,Y Axes 5,6 - a 'top hat' or similar input"
PRINT " --------------------------------------------------------------------------"
PRINT " X Axis 5:"; STICK(0, 3); " "
PRINT " Y Axis 6:"; STICK(1, 3); " "
PRINT " --------------------------------------------------------------------------"
PRINT " X,Y Axes 7,8 are rare but usually another 'top hat' style input"
PRINT " --------------------------------------------------------------------------"
PRINT " X Axis 7:"; STICK(0, 4); " "
PRINT " Y Axis 8:"; STICK(1, 4); " "
LOOP UNTIL _KEYDOWN(27) ' Leave when ESC pressed
SYSTEM ' return to OS
The STICK statement is used in the following manner:
Axis% = STICK(Direction%, Axis_Number%)
Direction% can be either 0 or 1. For joystick handles, 'plus' style buttons, and top hat style inputs, 0 typically indicates the horizontal axis and 1 indicates the vertical axis.
Axis_Number% is the axis controller you wish to access on the joystick or game pad. This can be a joystick handle, a top hat control, a slider, a twist control, a turn control (a spinner), or any other type of input that game controller manufacturers can come up with.
For example, I have a Saitek ST290 Pro USB joystick. It has the following axis inputs and STICK sees them as:
STICK(0, 1) - Joystick handle (x) horizontal (0), axis number (1), values of 1 through 254
STICK(1, 1) - Joystick handle (y) vertical (1), axis number (1), values of 1 through 254
STICK(0, 2) - Throttle slider (0), axis number (2), values of 1 through 254
STICK(1, 2) - Joystick handle twist (1), axis number (2)' values of 1, 127, or 254
STICK(0, 3) - Top hat (x) horizontal (0), axis number (3), values of 1, 127, or 254
STICK(1, 3) - Top hat (y) vertical (1), axis number (3) ,values of 1, 127, or 254
Notice that the joystick handle x,y axes make sense being paired together as axis number 1. Likewise, the top hat x,y axes are paired together as axis number 3. The two remaining axes, the joystick twist and throttle slider, are paired together as axis number 2. Even though they are not related like the other two axis pairs they must be paired to complete another axis pair so STICK can read their values.
STICK will return a value between 1 and 254 for any given direction, with 127 being the center point for axis pairs like a joystick handle or top hat. For some controls, like top hats and joystick handle twists, only three values will be returned, 0, 127, and 254 since they don't have a full range of motion available to them.
IMPORTANT NOTE: The STICK(0, 1) command MUST ALWAYS be issued first before any other STICK statement inputs can be done. STICK(0, 1) not only reads the horizontal direction of axis 1, it also initiates the STICK statement to read other axis directions as well. In the example above STICK(0, 1) was the first STICK statement used so the other STICK statements work properly.
Joystick/Game Pad Input: The STRIG Statement
The STRIG statement is used to return the status of buttons on joysticks and game pads. This example will test up to 10 buttons on up to 4 joystick/game pad devices.
( This code can be found at .\tutorial\Lesson7\StrigTest.bas )
format$ = " | # | ## | ## | ## | ## | ## | ## | ## | ## | ## | ## |"
DO
_LIMIT 30 ' don't hog the CPU
LOCATE 2, 1
PRINT
PRINT " Joystick Button Test"
PRINT " Test only for down (-1) or up (0)"
PRINT
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT " | JOYSTICK | B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 |"
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 1; STRIG(1, 1); STRIG(5, 1); STRIG(9, 1); STRIG(13, 1); STRIG(17, 1); STRIG(21, 1);_
STRIG(25, 1); STRIG(29, 1); STRIG(33, 1); STRIG(37, 1)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 2; STRIG(3, 2); STRIG(7, 2); STRIG(11, 2); STRIG(15, 2); STRIG(19, 2); STRIG(23, 2);_
STRIG(27, 2); STRIG(31, 2); STRIG(35, 2); STRIG(39, 2)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 3; STRIG(1, 3); STRIG(5, 3); STRIG(9, 3); STRIG(13, 3); STRIG(17, 3); STRIG(21, 3);_
STRIG(25, 3); STRIG(29, 3); STRIG(33, 3); STRIG(37, 3)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 4; STRIG(3, 4); STRIG(7, 4); STRIG(11, 4); STRIG(15, 4); STRIG(19, 4); STRIG(23, 4);_
STRIG(27, 4); STRIG(31, 4); STRIG(35, 4); STRIG(39, 4)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to OS
The STRIG statement is used in the following manner:
Status% = STRIG(Button%, DeviceNumber%)
Status% will contain a value of -1 if an event happened or button is currently down and a value of 0 otherwise.
The STRIG statement can be a little tricky to decipher because of the odd way it uses button numbers. In the example above the button numbers used only check if a button is currently pressed or not. There are other button numbers which can be used to test if a button was pressed since the last STRIG statement was used (a button "event", more on that in a bit). Here is a breakdown of how the STRIG button numbers are laid out:
STRIG(0, 1) - Button 1 on device 1 was pressed since the last STRIG(0, 1) was read (odd device)
STRIG(1, 1) - Button 1 on device 1 is currently pressed
STRIG(2, 2) - Button 1 on device 2 was pressed since the last STRIG(2, 2) was read (even device)
STRIG(3, 2) - Button 1 on device 2 is currently pressed
STRIG(4, 1) - Button 2 on device 1 was pressed since the last STRIG(4,1) was read (odd device)
STRIG(5, 1) - Button 2 on device 1 is currently pressed
STRIG(6, 2) - Button 2 on device 2 was pressed since the last STRIG(6, 2) was read (even device)
STRIG(7, 2) - Button 2 on device 2 is currently pressed
STRIG(8, 1) - Button 3 on device 1 was pressed since the last STRIG(8,1) was read (odd device)
STRIG(9, 1) - Button 3 on device 1 is currently pressed
STRIG(10, 2) - Button 3 on device 2 was pressed since the last STRIG(10,2) was read (even device)
STRIG(11, 2) - Button 3 on device 2 is currently pressed
STRIG(12, 1) - Button 4 on device 1 was pressed since the last STRIG(12, 1) was read (odd device)
STRIG(13, 1) - Button 4 on device 1 is currently pressed
STRIG(14, 2) - Button 4 on device 2 was pressed since the last STRIG(14, 2) was read (even device)
STRIG(15, 2) - Button 4 on device 2 is currently pressed
Etc..
Replacing the second parameter of 1 in STRIG with the value of 3 will reference device number 3. Replacing the second parameter of 2 in STRIG with the value of 4 will reference device number 4. For example changing STRIG(1, 1) to STRIG(1, 3) now references button 1 on device 3. Changing STRIG(7, 2) to STRIG(7, 4) now references button 2 on device 4.
Hopefully you can see the pattern that has emerged. Odd numbered devices (Joystick 1, 3, 5, etc..) always use the button number pairs:
(0, 1), (4, 5), (8, 9), (12, 13), (16, 17), (20, 21), (24, 25), (28, 29), (32, 33), (36, 37), etc.. (just keep adding 4 for more buttons)
Even numbered devices (Joystick 2, 4, 6, etc..) always use the button number pairs:
(2, 3), (6, 7), (10, 11), (14, 15), (18, 19), (22, 23), (26, 27), (30, 31), (34, 35), (38,39), etc.. (again, just keep adding 4)
Of those pairs, the first number is used to record a button event, the second number is used to get instant feedback on whether the button is pressed or not.
This code will demonstrate how to use the button event functions.
( This code can be found at .\tutorial\Lesson7\StrigEvent.bas )
format$ = " | # | ## | ## | ## | ## | ## | ## | ## | ## | ## | ## |"
DO
LOCATE 2, 1
PRINT
PRINT " Joystick Button Test"
PRINT " Test for button event since last read"
PRINT
PRINT " Press a few buttons on multiple joysticks then press ENTER to see the results."
PRINT
SLEEP
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT " | JOYSTICK | B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 |"
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 1; STRIG(0, 1); STRIG(4, 1); STRIG(8, 1); STRIG(12, 1); STRIG(16, 1); STRIG(20, 1);_
STRIG(24, 1); STRIG(28, 1); STRIG(32, 1); STRIG(36, 1)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 2; STRIG(2, 2); STRIG(6, 2); STRIG(10, 2); STRIG(14, 2); STRIG(18, 2); STRIG(22, 2);_
STRIG(26, 2); STRIG(30, 2); STRIG(34, 2); STRIG(38, 2)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 3; STRIG(0, 3); STRIG(4, 3); STRIG(8, 3); STRIG(12, 3); STRIG(16, 3); STRIG(20, 3);_
STRIG(24, 3); STRIG(28, 3); STRIG(32, 3); STRIG(36, 3)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT USING format$; 4; STRIG(2, 4); STRIG(6, 4); STRIG(10, 4); STRIG(14, 4); STRIG(18, 4); STRIG(22, 4);_
STRIG(26, 4); STRIG(30, 4); STRIG(34, 4); STRIG(38, 4)
PRINT " +----------+----+----+----+----+----+----+----+----+----+-----+"
PRINT
PRINT " Buttons showing -1 were pressed while waiting for the ENTER key to be pressed."
PRINT " Go ahead and press different buttons then press ENTER to see results change."
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to OS
Using the alternate pair number you can now test for button events instead of just checking for a button currently being pressed or not. Your game player may be frantically pressing buttons in hopes of killing the invaders you have thrown at them. By checking for button events, rather than real-time button presses, you're less likely to miss your player's button presses.
QB64 also offers an alternate method of checking for controller axes and buttons with _DEVICES and its related commands. That topic will be covered in Lesson 21: Advanced Controller Input.
Your Turn
Write a program that displays the output as shown in Figure 4 below.
Figure 4: A moveable and sizeable box
- The graphics screen is 800 pixels wide by 600 pixels high.
- The box width can be changed by holding down the left mouse button and turning the scroll wheel.
- The box height can be changed by holding down the right mouse button and turning the scroll wheel.
- The box, no matter its size, will stop at the edges of the screen.
- The box width and height can be no less than 10 pixels and no greater than 100 pixels.
- The box increases or decreases in width and height by 5 pixels with each scroll of the wheel.
- The box, as seen in Figure 4 above, starts out at 100 pixels wide by 100 pixels high.
- Save your code as ScrollBox.BAS when finished.
Commands and Concepts Learned
New commands introduced in this lesson:
LINE INPUT
INKEY$
CHR$()
_KEYDOWN
_KEYHIT
_MOUSEX
_MOUSEY
_MOUSEINPUT
_MOUSEHIDE
_MOUSESHOW
_MOUSEMOVE
_MOUSEBUTTON
SLEEP
STICK
STRIG
New concepts introduced in this lesson: