Flexible User Input Programming – An Idea
What is the first thing the customer does after he has installed a brand new game? He changes the controls to the way he likes, of course. You will be totally amazed with the keys some people use. I myself have seen quite strange faces, gazing at gamer’s fingers forming incredible configurations. The most terrible thing a game developer can do (as far as input goes) is to fix the keys (controls) and force the player to use them. So, what we need is a system that can report the controls’ state while keeping the opportunity for customization and, of course, remember this customization. Recently I had to program such a system to deal with the controls for a 3D game, and I tried to determine what tasks it should perform. Take a look at my list:
1. Internal work with input devicesWe can access the user input by tracking the application message queue but sometimes (almost every time) we need firsthand information for the devices we use. The DirectInput subdivision of DirectX can acquire exclusive control of common input devices such as the keyboard, mouse and joystick or wheel (if present). The programmer instantiates (creates) a DirectInput object and acquires pointers to the desired interfaces. Lately these interfaces are used to acquire exclusive input from the devices and retrieve the device status (mouse dragged, keys pressed, etc). The DirectX documentation completely covers these topics therefore I won’t go in to the basics. You can take a look at the samples in the DirectX SDK as well. However, a few words are necessary. DirectX is based on COM (Component Object Model). When you create DirectX objects you instantiate COM objects and obtain interfaces to them. Once acquired these interfaces are used to invoke methods that provide us with more direct access to the hardware devices, which is quite important for a game. Once used and no longer needed these interfaces must be released properly to prevent memory leaks and ensure proper execution. We can encapsulate DirectInput objects (pointers) into our class and when it falls out of scope automatically release the interfaces in the destructor. The constructor however is left empty, there are only some pointer assignments to NULL in order to keep pointers clean and guarantee accurate clean up. This has to be done (the empty constructor) because in order to construct the DirectInput objects and interfaces we need handles of the application instance and main window (it is possible that at the moment of the construction the main window of the application has not been created). Therefore I added an additional Create() method which has two handle parameters and takes care of the DirectInput object creation and obtains mouse and keyboard device interfaces as shown in Listing 1. Listing 1: DirectInput object creation HRESULT wj_controls::Create(HINSTANCE hInst, HWND hWnd) { hInstance=hInst; hWindow=hWnd; HRESULT hr; // Register with the DirectInput subsystem and get a pointer // to a IDirectInput interface we can use. hr = DirectInputCreate( hInst, DIRECTINPUT_VERSION, &m_pDI, NULL ); if ( FAILED(hr) ) return hr; hr=InitMouse(); if ( FAILED(hr) ) return hr; hr=InitKeyboard(); if ( FAILED(hr) ) return hr; AddSysKeys(); return S_OK; } The DirectInputCreate() function is provided by the DirectX library and creates a DirectInput object that supports the IDirectInput COM interface for us. Of course we can do it the harder way by directly calling CoCreateInstance and then obtaining and interface pointer to the object. But there is not much sense in doing so unless you want to do some special work such as aggregation or something else. InitMouse() and InitKeyboard() are almost identical – they only use different unique interface identifiers and set different data formats for the obtained device. All these interfaces must be stored in data members of our class. Having all initialization done there is one more thing you should do. Set some properties. These could be four:
It’s up to you to choose what method to use - relational or absolute mouse coordinates - but if you are building a 3D shooter there is much more sense in using relational ones and if it is a real time strategy game, absolute ones are perfect. Be sure to set properties at the initial setup because you cannot do that for a device that has been acquired. (Note: of course you can unacquire it and set the properties) We should keep an eye on the application message queue and when we detect application deactivation / activation, properly "unacquire" / "acquire" input devices - otherwise we may obtain undesirable results. State retrieval also must be synchronized with the "active" flag of the class because querying the status of an unacquired device can result in strange device hang ups (or at least I get them). So we add a MsgProg() method to our class and call it from the main message routine. It may look like this: Listing 2: Message proc code switch (uMsg) { case WM_ACTIVATE: //sent when window changes active state if ( WA_INACTIVE == wParam ) { m_bactive = FALSE; } else { m_bactive = TRUE; } SetAcquire(); // Set exclusive mode access according to m_bactive member break; } According to the DirectInput Keyboard Device data format we have a keyboard buffer (an array of 256 (bytes) keys), and relative (or absolute) coordinates for the mouse and mouse buttons buffer (4 bytes). We need to declare some data members in the class to store this device information. Of course we start with three long integers for the mouse axis ("there are only two of them" - someone will note - the third one, the z-axis, is the wheel rotation if any wheel is present). Next come the mouse buttons and the keyboard buffers. In fact I have implemented a little trick while I was dealing with the retrieval of data from device objects, namely to place the mouse buttons buffer at the end of the keyboard's array of bytes making it 260 bytes long (I use the default buffer size of 256 for the keyboard buffer – data format c_dfDIKeyboard - and a 4 byte buffer for mouse buttons – data format c_dfDIMouse). Thus I produced a common buffer for all game keys the player can use. No matter what the player does (pushes keyboard keys or clicks mouse buttons) I get the information from the same place. Next I have implemented an array of strings to name the keys – the player wants to see how the controls are configured and it is better to visualize "Space" than just the code (57). And once again for the code of 256 we have a name "Left Mouse Button". 2. Controls implementation, system controlsControls are much more than just keys. A single control (for example forward) can be represented by more than one keyboard key or even by a combination of keys. There has to be a small number of controls that cannot be changed (customized) which I call system controls that allow a game to maintain its behavior and of course change other controls. Here is my structure for the control: Listing 3: typedef struct { WORD id; BYTE flags; BYTE key[4]; BYTE state; BYTE oldstate; BYTE *external; } control; We need an ID to identify the control among other controls. Flags – to assign some properties such as system control, exclusive keys (some controls require to have unique keys and others don’t - imagine that you have the same key for forward and backward) and logical operators for the keys (if you have alternate keys you have an OR operator but if you have a key combination you have an AND operator). The key array represents the keys for the control; of course I limited their number to 4 but you can extend it to however many you wish - although I’ve never seen somebody playing with six keys for the same action. The state and the oldstate contain the state of the control according to the specified keys. The following code (Listing 4) shows how the state is calculated: Listing 4: Updating single control state if (control.flags&WJ_CTRLS_OPAND2) control.state = keys[control.key[0]] & keys[control.key[1]]; else control.state = keys[control.key[0]] | keys[control.key[1]] | keys[control.key[2]] | keys[control.key[3]]; We have an external pointer to store the state of the control somewhere else (although I don’t have it in my implementation it makes much more sense to store the state directly somewhere else than to iterate each time trough all control ids to retrieve the wanted control state). So, now that we have a control structure, let’s make some instances and organize them. You have to choose a data structure to store your control objects. The one I have chosen is the STL vector (yes I’m addicted to it – have some mercy - I’m trying some "lists" just now; since I’m an independent developer code reusability is an important thing – it saves time :) because it is faster for iterative access and we do not need any fast insert or delete operations on the collection (if you're not familiar with STL, just think of a vector as a dynamically sized array). Here we are: we have an empty collection of controls. The first thing we should do is to populate it with the system controls and limit the change/customize access to them. Next we have to put some real controls in there. For example, to create a "forward" control we need an AddControl() method implemented by the class. What parameters should we use? Not much, just the ID and the flags. So we have: AddControl(id,flags). "But where are the keys associated with this control?" you might ask. Ok, there is a AddKey(id,key) method although nobody said that you cannot customize control with no keys associated to it. The AddKey method must have some information about the keys sharing and properly handle the exclusive flag. This means that when a key is added it should check whether there is another control associated with this key and if that control has the exclusive keys flag set. So we end this section with a collection of controls structures and couple of methods to control them. For our convenience we will add another method called FormatControlString(szstr,id) to retrieve control key information into a string sequence, once again keeping an eye on the flags for the Boolean operator used. When we get to alternate keys the string will look like this "Right Alt , Space" but for the AND operator it will change to "Right Alt + Space". 3. Customize (change) a controlHow to customize controls? In fact you could do it with the AddKey method but that will assume you have obtained (in some way) the key the user wants to use for this particular control. This presumes that you have an alternative set of DirectInput objects or some other way of getting the user input. All this is unnecessary. All we need to do is implement the RecordControl(id) method. When we make the call, it waits for the user input and the first pressed key is added to the specified control using the AddKey method. So, in order not to bother the game with the keys the player wants to use, we just call the RecordControl method when user wants to change a control and continually update the information about the controls using FormatStringControl. The problem here is how to figure out what was the first pressed key after the record method was called. Here the common buffer comes in quite handy. We just scan it for differences each time it is updated and when we detect such a change we stop recording, passing the detected key to the other method. (Remember that the mouse stores its keys in the keyboard buffer so no additional checks are needed) That was easy, wasn’t it? Hey, wait a moment what about mouse movement? Oh, yes, don’t forget to implement some method for adjusting the mouse sensitivity. You can do it by implementing a simple value for scaling the mouse input for relational input, but what about absolute input? Remember the direct input properties and the granularity? That’s right, set proper granularity in order to adjust the mouse movement. 4. Retrieve a control stateLet’s return to the point. The task we want to perform is to get information from input devices, to filter it trough our collection of controls, and transfer it to a collection of control states that we can use. I named the method UpdateState() and it is invoked at least once per game iteration. Well, what does it do? DirectInput objects are responsible for retrieving the mouse and keyboard states into our buffer. Having that information, we iterate trough our collection of controls and perform the Listing 4 algorithm to calculate the control state. Listing 5: Updating the control states iterating the collection vector<control>::iterator it=controls.begin(); for(; it != controls.end(); it++) { it->oldstate = it->state; if (it->flags&WJ_CTRLS_OPAND2) it->state = keys[it->key[0]] & keys[it->key[1]]; else it->state = keys[it->key[0]] | keys[it->key[1]] | keys[it->key[2]] | keys[it->key[3]]; } At the end of UpdateState() we have our collection updated with the appropriate control states. That is fine but we need these states outside the class. There are two approaches to this problem: first. implement a GetControlStatus(id) method, and second, implement an external state pointer. The first one is much clearer but at the cost of some overhead (each time the program searches the needed control in the collection by id). The second one is fast enough (just an assignment) but it leaves an opening for making logical errors such as releasing memory assigned to the pointer or other memory problems. And finally the mouse: some GetDeltaX(),GetDeltaY() and GetDeltaZ() methods will be in use and remember to clear the mouse data members in order to accumulate new delta values in. 5. Controls persistenceControls persistence is an important thing. Somewhere, we have to store the controls configuration until the next game instance runs. Otherwise the player will soon get bored of configuring keys each time he starts the game. I won’t discuss file operations with collections here - that's up to you - but there are few interesting issues involved. When you save the collection to the file, do not save the system controls - they can be regenerated by the code. This saves space and prevents system controls from be violated accidentally or on purpose by the user. When loading controls just initialize the collection, add system controls and then do the file input. Speaking of persistence, users have an incredible talent of mixing up all the keys, so be sure to keep a clean configuration "defaults" file. 6. MiscA common need when working with user input is text entry (for example – player name entry, game name, message to other players, etc.). But our input devices are exclusively acquired and the ordinary way of dealing with text input will not work. We need an additional method to our class, which can handle the translation of the scan codes returned from the GetKeyboardState. The main points here are:
All these API functions are explained in the MSDN Library so take a look if you are interested. How does this algorithm fit into our class? It’s up to you. Do you need to cancel the user input during the text entry or do you need to run it concurrently? Two methods come in handy: StartText(mode) and EndText() where mode determines whether other controls should work during the text entry (of course the system controls work all the time). So when the StartText is invoked it starts recording and translating key codes to characters into a buffer and EndText finishes that process and returns the buffer. The conversion process is pretty simple, and I have seen it in Photon’s article at gamedev.net. Another common need is alternative mouse input during game play. Imagine that you use relational mouse coordinates to navigate your player in 3D but sometimes you need a mouse cursor and absolute mouse coordinates in order to handle the inventory. Two possibilities here:
The first approach is much more flexible because you don’t need to switch any DirectX modes or properties, it simply add relative values to the absolute internal ones (you need to keep data members for absolute mouse coordinates), and of course restricts them to the screen size (one more need – screen coordinates for clipping). The second one does not need any additional information but will demand setting the device’s properties, requiring that the device first be unacquired. Well, you may think we have finished, but there are still some more topics to cover. ProblemsImagine that you have added a system control "Alt + Enter" to handle the toggle-fullscreen operation. And another control "Enter" for, let’s say, a jump action. With the current algorithm running both controls will signal a positive state but you probably do not want to activate the jump action every time the player switches between full screen and windowed mode. Solution: just clear the keys you have already checked. Wait, we said that we would probably want some keys shared among the different controls but clearing checked keys cancels this possibility. To fix that we should introduce an additional "clear" flag and if it is present on the current control we clear all key states associated with it. Listing 6: Updating single control state and clearing keys if clear flag is set if (control.flags&WJ_CTRLS_OPAND2) ... if (control.flag&WJ_CTRLS_CLEAR) keys[control.key[0]] = keys[control.key[1]] = keys[control.key[2]] = keys[control.key[3]] = 0; DebuggingOnce you have implemented the user input you will come across some debugging problems. Imagine that there is a breakpoint, which returns the program flow to the debugger. Input devices are not unacquired properly and further execution is dubious. So what should we do? One way is to use the remote debugging options!!! And this is the best way to debug the user input system. However it is much easier to debug locally and if you are after some errors which you know are not in your input system, I would recommend bypassing DirectInput during the debugging procedure. Remember that we have a routine (method) that tracks the message queue. There's the input information during debugging process. OutcomeSo I think I have outlined my idea. There is a lot of work out there in user input. I don’t pretend this is the way to do it. It just seems right to me. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|