Anyway, one obvious next step, if I want to write an interactive program, is to start handling input in an interactive way. It turns out that what "handle interactive input" means, for a game like Tungsten at least, depends on what part of the chain you happen to be looking at:
- Take events from the toolkit ("SDL_keyboard_event") and translate them into genericized events ("Up arrow pressed") that Tungsten client code can use;
- Take generic Tungsten events ("Up arrow pressed") from the above component and translate them into game-specific actions ("Player 1 is hitting Thrust Forward"); and
- Take game-specific actions ("Player 1 is hitting Thrust Forward") and translate them into actual game behaviour ("player1->ship()->thrustForward();" or however I decide to implement it that doesn't violate the Law of Demeter).
Start with an Input class, which we'll specialize for specific toolkits (Input.h -- BitBucket). First of all we provide a big enum of key names:
enum InputKey { KEY_NOT_IMPLEMENTED = 0, // any key code we don't recognize KEY_F1 = 1, KEY_F2 = 2, KEY_F3 = 3, KEY_F4 = 4, // ... etc ... KEY_MAX_ELEMS };
The actual enum values don't matter too much; I tried to assign keys to their ASCII values as much as possible, but I don't know how helpful that's going to be down the road. Then we have a generic interface:
class Input { public: virtual ~Input() { } // This is framework-specific and gets defined in child classes virtual void pollEvents() = 0;
and some functions we already know how to implement, whose behaviour really shouldn't change in child classes:
bool keyDown(InputKey key) const { return key_down_[key]; } bool keyPressed(InputKey key) const { return key_pressed_[key]; } const char* keyName(InputKey key) const { return key_names_[key]; }
(keyDown is true as long as a key is pressed; keyPressed is only true for the frame in which a keypress event was recorded. This part of the interface may be more trouble than it's worth; for now I'm only including it because That's How I've Always Done It. It may become obsolete once I finally plug in callbacks.)
Here's where things start to get awkward. Class Input doesn't know how to set its own key status flags -- that's something only derived classes, which know about a toolkit, can do. On the other hand, we insist that all derived classes exhibit this default behaviour. For now I'm declaring the status flag arrays as protected, along with a ctor to set them to a sensible default:
Input() { // Not a very idiomatic way to do it in C++, but hey.... for(int k = 0; k < KEY_MAX_ELEMS; k++) { key_down_[k] = key_pressed_[k] = false; } } bool key_down_[KEY_MAX_ELEMS]; bool key_pressed_[KEY_MAX_ELEMS];
Except for the bool type, this all looks very C-like, and that's another cause for concern. Shouldn't I be using a std::map<InputKey, bool> or something? In defence of my array implementation, this data is all very static in character: Keyboard scancodes are derived from the USB keyboard spec, and it's rather unlikely that those will be changing at runtime. Integer indexes into static arrays should work just fine (and at O(1) rather than O(log n) speed).
The other half of the problem, of course, is that by making the interface between Input and its derived classes a pair of arrays in the protected space I've coupled them together pretty tightly. If I decide to get rid of keyPressed(), as I've said I might, I'll have to go through all the derived classes and fix them to match. (Granted that I'm unlikely ever to have more than one derived class from Input, but the principle holds.) Much better to make the data structures for non-overrideable behaviour completely invisible to derived classes and throw in a protected interface between the two. Something for me to do this afternoon.
All that's left is the derived class, SdlInput (SdlInput.h and SdlInput.cc -- BitBucket). The boring part is a table translating between SDL's rather impressive list of recognized scancodes and Tungsten's InputKey enum:
InputKey SdlInput::scancode_crosstab_[] = { KEY_NOT_IMPLEMENTED, // SDL_SCANCODE_UNKNOWN = 0, KEY_NOT_IMPLEMENTED, // 1 KEY_NOT_IMPLEMENTED, // 2 KEY_NOT_IMPLEMENTED, // 3 KEY_A, // SDL_SCANCODE_A = 4, KEY_B, // SDL_SCANCODE_B = 5, KEY_C, // SDL_SCANCODE_C = 6,
// ... etc ...
which gets used in the implementation of PollEvents:
void SdlInput::pollEvents() { SDL_Event event; for(int k = 0; k < KEY_MAX_ELEMS; k++) { key_pressed_[k] = false; } while(SDL_PollEvent(&event)) { switch(event.type) { case SDL_KEYDOWN: { const InputKey key = scancode_crosstab_[event.key.keysym.scancode]; key_down_[key] = true; key_pressed_[key] = true; break; } case SDL_KEYUP: { const InputKey key = scancode_crosstab_[event.key.keysym.scancode]; key_down_[key] = false; break; } case SDL_QUIT: // FIXME: Probably want a callback here exit(0); break; default: break; } } }
More C-like control flow; probably not a good sign there. At least I'm using consts properly whilst handling KEYDOWN and KEYUP events.
Note that the first thing pollEvents has to do is make sure base-class behaviour is consistent -- this is an implementation of a pure virtual function, remember, so it should be concerned with derived class behaviour. That's a pretty clear indication that something's gone horribly wrong in my design. (That comment in the SDL_QUIT case is also telling.)
Let's take a closer look at key_pressed_ and its associated behaviour. Really what's going on is that Input is trying to consider two different things: the current state of the keyboard, and keyboard-related events. (Or, if you prefer, the keyboard state function and its first derivative with respect to time.) "Someone pressed the up key" is an event. "The up key is down" is a state.
This makes me think that Input should in fact be split up into one event-handling class that only actually has to care about, you know, handling events and a separate keyboard class that only ever cares about tracking the state of the keyboard. The latter seems like it'd be analogous to a gamepad, which suggests a Controller base class.
Stay tuned.
No comments:
Post a Comment