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". |