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 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. When finished typing the code in save it as InkeyDemo.BAS.
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$
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 INKEY$ = "w" THEN y% = y% -1
IF INKEY$ = "s" THEN y% = y% + 1
IF INKEY$ = "a" THEN x% = x% - 1
IF INKEY$ = "d" THEN x% = x% + 1
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$
IF KeyPress$ = "w" THEN y% = y% - 1
IF KeyPress$ = "s" THEN y% = y% + 1
IF KeyPress$ = "a" THEN x% = x% - 1
IF KeyPress$ = "d" THEN x% = x% + 1
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.
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. Save the code as InkeyMenu.BAS when finished typing it in.
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 ' test for up arrow key
ELSEIF KeyPress$ = CHR$(0) + "P" THEN ' test for down arrow key
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$. Save this code as KeyhitDemo.BAS when finished.
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
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. Save the code as BetterKeyhitDemo.BAS when finished.
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. Save the code as KeydownDemo.BAS when finished.
_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.
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. Save this code as MouseXYDemo.BAS when finished typing in the example code below.
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
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. When finished typing in the code save it as MouseBuffer.BAS.
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. Save this code as MousebuttonDemo.BAS when finished typing it in.
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)
RightClick% = _MOUSEBUTTON(2)
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. Save the code as WheelDemo.BAS when finished.
_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. Type the following code in and save it as StickTest.BAS when finished.
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. Type the following code in and save it as StrigTest.BAS. This example will test up to 10 buttons on up to 4 joystick/game pad devices.
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
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.
Type in the following example program and save it as StrigEvent.BAS. This code will demonstrate how to use the button event functions.
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.
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.