[Alephmodular-devel] Input architecture (ActionFlags)
Status: Pre-Alpha
Brought to you by:
brefin
From: Woody Z. I. <woo...@sb...> - 2003-01-24 18:08:17
|
class ActionFlags encapsulates a single tick's inputs for a single player. The interface (IActionFlags, apparently) provides routines to get and set various information about the input and its representational status and abilities (SetPrimaryTriggerEngaged(bool), bool DoesProvideDeltaYaw(), GetDeltaYaw(), etc.). Subclasses know how to pack/unpack themselves, how to copy themselves (either in new dynamically-allocated storage or into existing storage), and how to predict what the next tick should look like given some tick's data. (This last bit would be used for network "smearing" as well as eventual prediction.) And I suppose it would be helpful if the ActionFlags could provide an ActionFlags object representing the current input state. :) (It could use one or more other objects to help it do this of course, so that the code for testing whether a key is pressed on all the various platforms does not need to be duplicated in every ActionFlags class, etc.) This encapsulation provides the following benefits: + The packed format (or formats, if external Packers are used) of ActionFlags is free to change independent of other usage. + The internal (unpacked) representation of ActionFlags is free to change, perhaps in support of the following... + The information carried by ActionFlags can be changed or extended. (e.g. one could add DoesProvideDeltaForward() in support of using proportional joysticks to walk around, or GetJumpKeyEngaged(), or SetTypedTerminalKey(), providing default implementations in a base class (e.g. returning false for 'DoesProvide...' or 'GetJumpKeyEngaged()', doing nothing for SetTypedTerminalKey(), etc.) so games that use an older ActionFlags class are shielded from the changes). A specific ActionFlags subclass should also be able to provide a "no action" Flags and possibly a (consistent) "invalid" Flags. When packing or unpacking, an ActionFlags object should have a "previous ActionFlags" object available to it so that it can apply some sort of differential compression etc. (Maybe there should be a separate but related ActionFlagsPacker that's responsible for tracking this sort of state.) It would be nice if the ActionFlags class could help the configure-controllers UI figure out what controls should be configured and what type of input they expect (boolean or numeric, etc.). If the frequent use of virtual functions instead of inline code makes anyone queasy, refer to my previous post about optionally selecting a specific subclass at build-time rather than at runtime. A factory somewhere would determine the actual C++ class type used for a given game's ActionFlags. In my current thinking, a given game uses the same such class throughout - i.e. a queue of ActionFlags has all CMarathonActionFlags elements, never a CMyCustomActionFlags element - or vice-versa. This could be enforced by having an ActionFlags queue type that knows what type should be used (probably via instantiating a template class); any needed queues would be provided by the same factory that provides flags. I suppose the actual class used would be determined by: the film file if playing back a film, the current game personality if playing a single-player game, and the negotiated least-common-denominator of game personalities in use in a netgame. Or, maybe the gatherer of a netgame specifies the class (directly or indirectly) and only joiners that support it will be able to play. Queues of ActionFlags support peek(index) to retrieve the head element or some element beyond the head (leaving the queue state unchanged), dequeue() which is strictly for removal (thus working around potential problems with storage invalidation), enqueue(flags) which copies the flags into the queue, count() which gets the number of elements currently in the queue, maxcount() which gets the largest number of elements that could ever be in the queue, and available() which gets the number of elements that could be enqueued right now without problems. Queues also have a 'hook' or 'trigger' so that some routine can be called whenever a new ActionFlags is enqueued (and some other routine when dequeued). Maybe these triggers would have a 'threshhold' and so would only call the routine when the queue gets sufficiently empty or full (which may be especially useful for film-related queues - though maybe it's not such a good idea anyway if different threads will be the enqueuers or dequeuers in different circumstances). Queues would likely be implemented as they are now - as circular queues - to control memory allocation and to simplify communication between a pair of threads. (Note that one thread must be consistently the 'enqueuer' and one (potentially different) thread consistently the 'peeker'/'dequeuer' - or else that multiple threads trying to work the same half mediate their access on a mutex or something - for this to work safely.) Resetting a queue is semantically equivalent to dequeueing all its elements, and thus is subject to the "thread-safety" rules for dequeuers. (An enqueuer may not reset a queue, unless it behaves as a dequeuer also, taking a mutex on dequeueing as necessary if it's not the only dequeuer, etc.) The basic structure of the input system would stand as it is: Input Capturer (in Input Capture thread) periodically enqueues onto LocalPlayerQueue Network code (either in its own Network thread or in the Input Capture thread as part of a 'hook' - but only one or the other on a given machine) uses info from the LocalPlayerQueue to pack data into packets it sends to other machines. Network code (in some Network thread or in the Game Update Thread, but only one or the other on a given machine) unpacks received network data onto various PlayerQueues. It may also enqueue ActionFlags onto the RecordingQueues. Game update code (in the Game Update thread) uses data from the PlayerQueues for updating the players' states. (Maybe it eventually builds PredictiveQueues, or something, internally...) Recording code (in some thread or another) takes data from the RecordingQueues and packs it out to a file (maybe using some of that differential compression hinted at above, e.g. to do RLE as the current code does...) In a single-player game, flags go from the Input Capturer directly into the PlayerQueue for the local player. In film playback, flags are unpacked (in some thread or another) from the film file into the PlayerQueues. So these elements should all be configurable with "what queue should I read from" and "what queue should I write to"; when starting a netgame or a single-player game or a film replay etc. they're all wired up the way they ought to be. There's a master clock somewhere that helps the world-updater figure out when it should actually use flags (it may have more in its queue than it should consume at the moment). In the current M2/A1 implementation this is driven by the Input Capturer and is called either heartbeat_count or "net time". As with the queues, the world-updater should be configurable with what master clock to use. All this decoupling and wiring-up stuff helps make the systems more independent and thus more modular and flexible. In the future some other code may want to route things another way; it does this just by wiring things up differently at initialization-time. There's no need for it to go through and change the Input Capturer to check whether the new configuration should be used and, if so, send flags elsewhere; Input Capturer stays just the way it is but is wired up differently when the new game starts up. There could be additional Queue types that don't really queue things... e.g. one "fan-out" that takes flags enqueued to it and enqueues a copy of them on each of several queues it's wired to, another "overlay" type that pulls from the highest-priority queue if there's something there, discarding the corresponding flags from all lower-priority queues, or else pulling from a lower-priority queue if the highest-priority queue is empty, etc. Random note that didn't fit anywhere else: currently the Input Capturer code (parse_keymap() at the moment, basically, I think) must be safe to run in both the input-capture thread (for normal use) and in the network thread (in case the network code needs a "smear"). Using mostRecentActionFlags->GetPredictedNextFlags() for smearing, instead of tripping back to to the Input Capturer directly, makes the scheduling behavior a little simpler and thus probably a little safer. Woody |