Lesson 21: Advanced Controller Input
Lesson 7 introduced you to the STICK and STRIG statements to interact with joystick and game pad devices. These statements were introduced with the original QuickBasic language when joysticks and game pads were much simpler devices. Although these statements have been improved by QB64 to take advantage of new controller features they still leave much of the discovery process about connected controllers up to the programmer. How many total buttons does a joystick or game pad have? This question can only be answered using STRIG and getting the user involved to press buttons and having your code detect which button was pressed. How many total axes are there? Again, you'll need to use STICK in the same manner to find that answer. How do I use wheel inputs? This can't be done with STRIG or STICK unfortunately as wheel inputs (such as found on a mouse) were not a thing yet.
Fortunately the QB64 developers have taken this into account and created a whole new suite of controller statements to better work with today's modern controllers. Furthermore, the new controller statements include the keyboard and mouse as valid controller devices and allow you to interact with them just as you would a joystick or game pad device. Let's get started.
The _DEVICES Statement
The _DEVICES statement is used to get the number of input devices connected to the computer. Input devices include the keyboard, the mouse, and all joystick and game pad controllers currently connected. Other types of controllers found could be gas/brake pedals, rudder pedals, and flight simulator throttle quadrants.
IMPORTANT NOTE: The _DEVICES statement must be called first and at least one time to activate the new QB64 controller statements. After _DEVICES has been used all the other new controller statements can be used throughout your code.
DeviceCount% = _DEVICES ' get total number of controllers connected to computer
DeviceCount% will now hold the total number of controllers found connected to the computer. Remember, besides the total number of joysticks and game pads found, the count will also include the keyboard and mouse if they are present.
The _DEVICE$ Statement
Once the total number of input devices have been determined by _DEVICES you can use _DEVICE$ to get descriptive information about each device found. The _DEVICE$ statement returns a string value holding information about the controller type, the descriptive name, and the input types (buttons, axes, wheels) available on the controller.
DeviceInfo$ = _DEVICE$(DeviceNumber%)
DeviceNumber% can be any number in the range from 1 to the total number of devices reported by _DEVICES. Type in the following example and save as "DevicesDemo.BAS" when finished. You can also find this code in your .\Tutorial\Lesson21\ subdirectory.
Figure 1: Input Controllers Found
The information produced by _DEVICE$ is separated by square brackets [ ] and can either be parsed out or searched using the INSTR statement. One method I like to use is to parse the information into a user defined type contained within an array. Type in the following example and save as "DeviceInfoDemo.BAS". You can also find this code in your .\Tutorial\Lesson21\ subdirectory.
Figure 2: Controller Information Saved to an Array
The controllers and related information are now stored in an array that can be called upon when needed. However, simply knowing if a controller has buttons, axes, or wheels is not very useful. It would be better if the number of each could be stored instead. Let's explore those options now and return to this code with highly useful modifications.
The _LASTBUTTON Statement
The _LASTBUTTON statement returns the number of buttons a controller contains.
Buttons% = _LASTBUTTON(DeviceNumber%)
DeviceNumber% can be any number in the range from 1 to the total number of devices reported by _DEVICES.
Buttons% will contain the number of buttons that the controller reports to the operating system. It's important to note however that the actual number of buttons on the controller may differ from the number that the controller reports.
The "USB Gamepad" seen listed in figures 1 and 2 above reports that it has 10 buttons but in actuality only 4 buttons exist on the controller. To make things even more confusing the buttons are listed as buttons 2, 3, 9, and 10. This game pad is a cheap Nintendo USB clone that can be purchased on Amazon for $8 a pair. All of my more expensive name brand controllers, such as the Saitek ST290 Pro joystick and others, report the exact amount of buttons they contain. Keep this in mind when writing software; your game players may not have expensive name brand controllers at their disposal. A little interaction from your players may be necessary to identify their controller's button arrangement.
The _LASTAXIS Statement
The _LASTAXIS statement returns the number of axes a controller contains.
Axes% = _LASTAXIS(DeviceNumber%)
DeviceNumber% can be any number in the range from 1 to the total number of devices reported by _DEVICES.
Axes% will contain the number of axes that the controller reports to the operating system. Just like with _LASTBUTTON, the actual number of axes on the controller may differ from the number that the controller reports.
None of the controllers I own, not even the cheap USB game pad, mis-report their number of axes. I've tried steering wheels, flight yokes, rudder pedals, gas/brake pedals, game pads, joysticks, and throttle quadrants. All have correctly reported their number of axes. However, this may not always be the case.
The _LASTWHEEL Statement
The _LASTWHEEL statement returns the number of wheels a controller contains.
Wheels% = _LASTWHEEL(DeviceNumber%)
DeviceNumber% can be any number in the range from 1 to the total number of devices reported by _DEVICES.
Wheels% will contain the number of wheels that a controller reports to the operating system. Again, the actual number of wheels on the controller may differ from the number that the controller reports.
The only controller I have with a wheel is a mouse. None of the other controllers I have report any of their input methods as wheels. As far as I know wheels are only included with mice, but that may not be the case. If you happen to have a game controller, other than a mouse, that reports one or more of its input methods as being a wheel please contact me so I can purchase that device for testing.
Ok, now that we know the methods for identifying the number of buttons, axes, and wheels, let's revisit the previous code and modify it to take advantage of these new statements. Type in the following code and save it as "DeviceInfoDemo2.BAS" when you are finished. You can also find this code in your .\Tutorial\Lesson21\ subdirectory.
Figure 3: Number of buttons, axes, and wheels now reported
You're probably looking at the chart in figure 3 with a bit of confusion. A 512 key keyboard? A 3 wheeled mouse? While they are rare you can purchase a 512 key keyboard and 3 wheeled mouse from Amazon at fairly reasonable prices. I highly recommend using them! It's enabled me to program so much faster having a macro key for every single QB64 statement.
The _LASTBUTTON statement will always report 512 buttons (also known as keys) and a mouse will always report 3 wheels (or 2 wheels if no actual wheel is present). The 512 reported buttons for a keyboard is due to scan codes that will be used (more on that later). The reason a mouse will report three wheels is because you're given the ability to use the mouse axes as wheels if you wish. Have you ever pressed your mouse's center button (or wheel) to switch the mouse into scroll mode? When in scroll mode the mouse's vertical axis is used like a scroll wheel instead (again, more on this later).
The _DEVICEINPUT Statement
The _DEVICEINPUT statement returns the controller device number when an event such as a button, axis, or wheel has been interacted with. _DEVICEINPUT can be used in two different ways:
Device% = _DEVICEINPUT ' return the controller number that has been interacted with
Active% = _DEVICEINPUT(DeviceNumber%) ' check a specific controller for interaction
The first method above will return a number that correlates to any input controller that has been interacted with. From there you can use the second method to interact with that specific controller and gather information from it. Type the following example in and save it as "DeviceInputDemo.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory. This code assumes that you have a mouse and keyboard connected to your computer.
Figure 4: Controller Interaction
As you interact with the various controllers attached to your computer the controller names will highlight. Line 20 of the code uses _DEVICEINPUT to scan all controller devices for interaction and will return a value of -1 if interaction is detected. Once interaction has been detected line 22 of the code clears all previous interactions of the specific controller that was interacted with leaving the latest and most up to date interaction.
In Lesson 7 you learned how the mouse has an input buffer that saves all movements and clicks. You can then either read in every mouse event one at time or clear the mouse buffer to get the latest mouse information. The same this is happening here with _DEVICEINPUT. When using the second method of _DEVICEINPUT one interaction at a time is read from the controller buffer. Line 22 clears that controller buffer leaving the last interaction to be read. If you REMark line 22 and run the code again you'll see this in action (you may also want to change the _LIMIT 10 in line 11 to _LIMIT 30 to speed things up a bit). Most of the time it's only desirable to get the latest information from a controller. If you don't clear the controller buffer you'll need to make sure your code handles and processes this extra input properly.
You may have noticed that one or more of your game controllers, or the mouse if you let it hover over the window, randomly gets activated. With game controllers this is typically caused by jitter created in the potentiometers that read joystick axes. I'm not sure why the mouse does it but my guess is that either QB64 or the operating system (or both) polls it for occasional information. Either way, this jitter and constant mouse polling can add up in the controller buffers quickly. Keep this in mind.
Now that we can detect interaction with devices let's investigate the statements that pinpoint which inputs are being interacted with.
The _BUTTON and _BUTTONCHANGE Statements
In Lesson 7 you learned that the STRIG statement worked in numbered pairs when detecting button presses. The first number in the pair is used to record a button event while the second number is used to get instant feedback on a button being either up or down. QB64's controller statements have broken this down into two separate statements; _BUTTONCHANGE to record a button event and _BUTTON to get instant feedback on a button's status.
Status% = _BUTTONCHANGE(ButtonNumber%) ' -1 for a press, 1 for a release, 0 for no interaction
Press% = _BUTTON(ButtonNumber%) ' -1 if pressed, 0 if not
ButtonNumber% can be any number in the range from 1 to _LASTBUTTON of the controller being checked. ButtonNumber% can't exceed the _LASTBUTTON value or an error will occur.
Status% will contain the value of -1 if a button was pressed since the last _BUTTONCHANGE, a value of 1 if a button was released since the last _BUTTONCHANGE, or a value of 0 if no interaction occurred.
Press% will contain a value of -1 if a button is currently pressed or a value of 0 if it is not.
Notice that both of these statements have no way of identifying which controller you want them to check. This is determined by the _DEVICINPUT statement previously discussed. The last controller referenced by _DEVICEINPUT is the current active controller used by _BUTTON and _BUTTONCHANGE.
Type the following code in and save it as "ButtonDemo.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectoy. This code assumes you have a keyboard, mouse, and at least one joystick or game pad controller connected.
Figure 5: Controller Button Test
This code produces the same results as the STRIG example in Lesson 7 with a few important differences. First, buttons are referenced by a logical order instead of the odd/even pairing that must be deciphered using STRIG. Second, the number of buttons for each controller is known because of the use of _LASTBUTTON. Third, unlike STRIG, _BUTTON will produce an error if a button number is used that does not exist on a controller.
Line 36 in the code ensures this does not happen. The loop will exit when the number of buttons on the controller is reached or 10 buttons have been polled, whichever is smaller.
As you can see in lines 27 through 30 of the code, keyboard "buttons" must be referenced by a scan code number. The scan code numbers for the four keyboard arrow keys are provided as CONSTants in lines 1 through 4 of the code. We'll talk more about the use of keyboards and their scan codes in a bit.
Change all the _BUTTON statements in the code to _BUTTONCHANGE statements and save the code as "ButtonChangeDemo.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory. Once you've made the changes or loaded the code execute it to see the difference between the _BUTTON and _BUTTONCHANGE statements.
The _AXIS Statement
The _AXIS statement returns the position of a specified axis contained in a controller. The use of _AXIS is much more straightforward that the STICK statement from Lesson 7. If a controller has, for instance, four axes then you simply identify them by an axis number rather than having to identify the controller and desired axis together as you do with the STICK statement.
Position! = _AXIS(AxisNumber%)
AxisNumber% can be any number in the range from 1 to the total number of axes reported by _LASTAXIS.
Position! will be a value expressed between -1 and 1 depending on the direction of axis deflection. For horizontal axes -1 is typically a full deflection the left and 1 a full deflection the the right. For vertical axes -1 is typically a full deflection upward and 1 a full deflection downward. A value of 0 for both axis orientations means the axis is centered. Controller axes come in many forms; joystick handles, sliders (throttles), top hats, gas/brake pedals, rudder pedals, game pad "plus" inputs, and joystick handle twists, etc..
Type the following code in and save it as "AxisDemo.BAS" when finished. You can also find this code in your .\Tutorial\Lesson21\ subdirectory.
Figure 6: Getting Axis Values
Analog axis inputs such as joystick handles and throttle sliders will smoothly change from values between -1 and 1 depending on their current position with a value of 0 being the center point. Push button axis inputs such as top hats and Dpads or the "plus" style inputs found on game pads will only give three values, -1 for up or left, 0 for center or no button pressed, and 1 for down or right.
Note in Figure 6 above the first two values seen for controller 4. Axis 1 (1X) and axis 2 (1Y) are the Dpad ("plus" buttons) inputs on the cheap USB game pad I have. They are both showing a value of -0.0078. This is considered 0 or centered for these two axis inputs. It may be necessary at times to adjust the resolution of values you are seeking. Later in the lesson you'll be shown how to remap axis values for better resolution instead of relying on the values of -1 to 1 for input.
The _WHEEL Statement
The _WHEEL statement is used to read the current state of wheel inputs on controllers. The most common wheel of course will be the one included with a mouse.
Direction% = _WHEEL(WheelNumber%)
WheelNumber% can be any number in the range from 1 to the total number of wheels reported by _LASTWHEEL.
Direction% will contain a value of -1 when scrolling up, a value of 0 when no movement is detected, and a value of -1 when scrolling down. It's best to read _WHEEL values within a loop that scans the entire input buffer for wheel changes. Type in the following example code and save as "WheelDemo.BAS" when finished. You can also find this code in your .\Tutorial\Lesson21\ subdirectory.
Figure 7: Pulling a Wheelie With The Mouse
The code is very straightforward but lines 15 through 22 need examining. You'll see that _WHEEL values were gathered within a loop that reads each value contained in the input buffer. Once all values have been read the loop ends. If you try to read _WHEEL values after emptying the buffer, i.e WHILE _DEVICEINPUT(2): WEND, you'll almost never get accurate _WHEEL values if any. This is because of that periodic mouse polling pointed out earlier and the fact that the mouse is moved around a lot by the user which in many cases causes the last mouse input in the buffer to be a movement value. Keep this in mind when working with the mouse wheel.
IMPORTANT NOTE: The mouse supports the use of _WHEEL(1) and _WHEEL(2) to utilize mouse movements as wheel inputs. However, there seems to be an issue right now with that function not working properly in QB64. When this gets sorted out an update on the main page will posted and this section will be updated to show _WHEEL(1) and _WHEEL(2) use.
Example: Using the Keyboard as a Controller
The keyboard can be used as a game controller that supports the _BUTTON and _BUTTONCHANGE statements. Each key on the keyboard is assigned a scan code to be used with both statements. It's important to note that these scan codes are not the same as used with the _KEYHIT and _KEYDOWN statements. Figure 8 below is a copy of the scan code values for use with _BUTTON and _BUTTONCHANGE taken from the QB64PE wiki here.
Figure 8: Keyboard Button Scan Codes From the Wiki
Included in your .\Tutorial\Lesson21\ subdirectory is an include file you can use called "KeyboardConstants.BI" that contains all of the scan codes listed in figure 8 above associated with the name of the keys. Simply add the statement:
depending on where your source code is located to the top of your code and you'll be able to reference scan codes by name.
Type in the following example code and save it as "KeyboardDemo.BAS" when finished. You can also find this code in your .\tutorial\Lesson21\ subdirectory.
As you can see using _BUTTON to reference keyboard keys acts similar to _KEYDOWN. You can hold the keys down for continuous movement and hold multiple keys for diagonal movement. Always remember to reference the keyboard controller first, as was done in line 16 of the code, so the _BUTTON and _BUTTONCHANGE statements will use the keyboard when scanning for button presses.
Example: Using a Joystick Controller
For this section you will need to have an analog joystick connected to your system. Analog inputs on a joystick range smoothly from -1 to 1 in both the vertical and horizontal axes. You can use these values in a number of different ways. By multiplying the _AXIS values by a set speed you can have an object increase or decrease in speed with direct relation to the amount of joystick handle deflection. Let's explore this method first. Type in the following example code and save it as "JoystickDemo1.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory.
NOTE: You may need to change the value in line 1 of the code to match your joystick's controller number.
Small joystick handle movements yield small movements of the circle. The further you deflect the joystick handle the faster the circle moves. Something to take notice of is how the Threshold variable is being used. As mentioned before analog joystick axes are prone to slight jitter when they are centered. To remove the jitter the value in Threshold is used to test each axis for at least that amount of deflection. If you set the Threshold variable in line 12 to 0 you may see this jitter first-hand. Cheap joysticks usually contain quite a bit of jitter while more expensive ones may not contain any at all. Keep in mind that all of your users may not have expensive joysticks at their disposal.
Another way to use the _AXIS values is to treat them as a switch. When a certain threshold of deflection is met the axis is seen as "down" like a button. Type in the following example code and save it as "JoystickDemo2.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory.
A third method sets the joystick's axes in a direct relationship with the screen coordinates by mapping the values of the axes to a different value range. Type in the following example code and save it as "JoystickDemo3.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory.
A function has been created in this code called Map_Axis that takes an axis value and converts it to a new value range. In line 17 the horizontal axis value is converted to 0 through 639 (the width of the screen) and in line 18 the vertical axis in converted to 0 through 479 (the height of the screen). The joystick handle now has a direct relationship to screen. Wherever the joystick handle is pointing the circle will be drawn on the screen in the same position accordingly.
Example: Using a Game Pad Controller
Game pads come in many different flavors, from Nintendo style pads with a simple Dpad ("plus") for axis control, to the more complex with analog top hats that act like joystick handles and top hats that act like Dpad axis inputs. For the analog top hats simply use the joystick examples above to get the effect you desire. Dpad axis inputs however don't offer a full range of motion but instead simply return the values of -1 (up or left), 0 (center), and 1 (down or right). Therefore Dpad axes can be treated as buttons similar to the joystick demo #2 above. Plug in a simple game pad now if you have one. Preferably one with nothing more than a Dpad for axis input (i.e. the original Nintendo style controller). Type in the following example code and save it as "GamepadDemo.BAS" when finished. This code can also be found in your .\Tutorial\Lesson21\ subdirectory.
The code in this example is almost identical to the joystick 2 demo above with a notable difference. A threshold value is still being used and set to a very high .9 setting. Even though Dpads and top hats are supposed to return -1 and 1 they sometimes may not. The top hat on my joystick every once in a while will return +/- .99, just shy of +/- 1. By using a high threshold value you're guaranteed to pick up every Dpad or top hat button press.
QB64 Controller Library
I wrote a controller library that puts all of the concepts and ideas in this lesson into an easy to use set of statements. The library is included in your .\Tutorial\Lesson21\ subdirectory as a ZIP file named "ControllerLibV1.10.ZIP". The individual files within the ZIP file have also been extracted into your subdirectory as well. As of this writing the library is on version 1.10. You can find the latest version and more information about the library here at the QB64 Phoenix Edition forum. You can also download the controller library directly here if you prefer.
The controller library will allow you to do the following:
Report existing controllers
__KEYBOARD_EXISTS, __MOUSE_EXISTS, __JOYPAD_EXISTS()
Report a new controller plugged in and an existing controller unplugged/plugged back in
Get controller Properties
__CONTROLLER_NAME$(), __BUTTON_TOTAL(), __AXIS_TOTAL()
Query a controller directly
__CONTROLLER_BUTTON(), __CONTROLLER_AXIS(), __CONNECTED()
Create user defined buttons
__MAKE_BUTTON, __ASSIGN_BUTTON, ASSIGN_AXIS
Set axis threshold sensitivity
Detect user defined button presses
Auto assign user defined buttons
Remove user defined button assignments
Enable or disable user defined button reassignment
Retrieve a button's name
Remap joystick and game pad axes to different values
Save and load user defined buttons
There is a mini-game included with the library (Configure_buttons.BAS) to illustrate how to use the library and how to allow the player to assign their own buttons and axes to movements and actions (see Figures 9 and 10 below). Instructions on how to use the library are included at the top of the CONTROLLER.BI basic include file. See Lesson 20 on how to use and incorporate .BI and .BM library files into your projects.
Figure 9: Mini-game in action
Figure 10: Player configuring controller inputs