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