And I missed your response! Oops.
Thanks! And it's far, far from final.
UPDATE 64
Here's a really boring one for most of you, but might be interesting to those wanting to know more about my game engine Karhu and it's k00l systems.
Lots of refactoringI won't say too much about this part, but I've spent days going through the engine code of Karhu in order to finally fix some stuff (unnecessary code, lacking code and renaming things and modifying pipelines, for example) I've wanted to fix for a while.
It includes a access to the component system, cleaner script binding, and updating some stuff to make use of neat C++14 features that I had forgotten about or were never aware of after reading up on both C++11 and C++14 (seems my project was actually still set to C++11 too despite Xcode supporting C++14).
Versatile controller scheme and input systemsThis is the big one I've been coding for almost two days straight.
I've set up an input system that solves all of my problems from previous projects and ones specific to this one, while making input coding for games using Karhu a lot more elegant, with support for multiple controller schemes (optionally active at the same time) and a useful system of locking and listening to player input. It also supports multiple players with individual gamepads (or sides of a keyboard or whatever) even tho Vatnsmyrkr won't make use of that (it wasn't much extra work so I thought I might as well add it).
The schemesThe class
Scheme provides two means of input:
input callbacks returning
input weights (a structure holding a
value and a
weight) and the registration of
constants referring to buttons (gamepad, keyboard or mouse) which can be queried to check whether they were
pressed or
released during the current frame, or whether they are currently being
held.
Input callbacksA callback (a function returning a value in this case) can be registered with a name (called a key). For example, we could register a callback called "direction" and have it return the angle of the joystick as the
value and how hard the stick is being pushed as the
weight. We can then invoke this callback by its name/key to get this input weight.
Input constantsSimilarly, constants are registered with a name/key, but instead of a callback a value is registered, which is either a gamepad button, a keyboard key or a mouse button (which can be accessed in Karhu as scoped enum constants such as CodeButton::LEFT, CodeKey::SPACE or CodeMouse::MIDDLE).
Unlike callbacks, multiple constants can be registered with the same name/key. This way we can query whether "left" is currently being
held and get a positive response no matter if it is the left keyboard arrow key or the A key (for WASD controllers) that is being pressed, by having registered both by the same name/key, "left".
ShorthandsThere are three shorthand functions defined in the scheme class:
direction() is equivalent to
input("__direction") (invoking a callback by the name/key "__direction"), and similarly we can call
horizontal() and
vertical() for
input("__horizontal") and
input("__vertical"). I chose to register these as virtually every controller system out there will want to have easy access to these values, plus it ties in with the next point, which is…
AutomationThere are two more helpful functions defined:
automateForKeyboard() and
automateForGamepad(). These set the controller scheme up in a barebones way for keyboard or gamepad. A few options can be set before doing this, by calling the function
option().
For example, gamepad automation lets you choose whether to merge D-pad and primary stick or not, whether to merge primary and secondary sticks and whether to enable diagonal output from the D-pad by combining the input of opposite axes.
If no options have been set, by default the gamepad automation will be set up with nothing merged and diagonals disabled, making
direction() (or
input("__direction")) return the angle and push strength of the primary joystick,
input("__direction-secondary") return the same for the secondary joystick (which has no shorthand) and it will register the constants
__left,
__right,
__up and
__down for the D-pad buttons.
Vatnsmyrkr uses these default options and adds some functionality of its own by registering further callbacks and constants for its gamepad controller scheme.
Global accessLike many things in the engine Karhu, schemes are global and can be created and accessed by the global function
scheme(). It takes as its argument the name/key of a scheme and returns that scheme, creating it if it does not already exist.
Thus we can create a new scheme set up with the basics for gamepad input like so:
scheme("gamepad").automateForGamepad();
This will now be accessible anywhere we want to register the scheme for use, which is the next topic.
The playersPlayers are special components which are set as children to player objects (in Vatnsmyrkr, a player component would be added to the submarine component) and in which multiple controller schemes can be registered. Schemes can be activated or deäctivated, so that a player may either allow for several input schemes at once (which is probably how I'll be doing it in Vatnsmyrkr; if the gamepad fails, then keyboard and mouse should be available for the rescue) or only the active one(s), depending on the game's needs.
Global accessAgain, players are global. I've worked with enough games to know that the player object is something you will want to be able to access anywhere, and so the support is right there in the engine.
The global function
player() takes a component (which would be the submarine in Vatnsmyrkr), registers it among the players at the position it ends up in the list (if there are no players yet, it'll be the first player, otherwise the second or third and so on) and then returns the player component. However, when invoked without a component, to access a player, the parent component (i.e. the submarine) will be returned and not the player component (because usually other objects would want to access the submarine and not the object controlling the submarine's input schemes).
Multiple schemesWe are then free to register schemes to the player component. As schemes are global, it would be as simple as this to register our newly registered scheme "gamepad" in the first player:
player().get<Player>().scheme("gamepad", scheme("gamepad"));
Let's say we also want to register a keyboard scheme. We can do that. The whole code might look something like this:
Player &p(player(submarine));
p.scheme("gamepad", scheme("gamepad").automateForGamepad());
p.scheme("keyboard", scheme("keyboard").automateForKeyboard());
CustomisationNote that the schemes are copied into the player; they're not references. Thus, after adding a scheme to a player, we can modify that scheme for that player individually, without modifying the global scheme. That way we could for example assign the same gamepad scheme to player one and two but allow them to individually customise their controllers.
InputThe player has copies of the scheme functions
input(),
direction(),
horizontal() and
vertical() as well as the functions to check constants,
pressed(),
held(),
released().
The difference is that the player component will check all of its registered and currently active schemes, meaning that Vatnsmyrkr can check whether "action" (for the action button) was pressed and get a positive response no matter if it were the gamepad scheme (PS3 X button or Xbox 360 A button, for example) or the keyboard scheme (space key perhaps) that invoked it, if both schemes are active at the same time.
However, the most elegant way is not always to check for presses directly, but to use…
EventsKarhu's component system has already supported events for a long time, of course. However, they were a bit clunky and part of my refactoring spree dealt with this.
Initially I had thought myself clever by adding shorthands for registering events, adding a bunch of functions to the component class, such as
begin(),
update() and
draw(). Derived component types such as physics colliders (collision masks; hitboxes), adding
collision() to register a collision event callback and so on.
I did away with all of these and made it into a single function,
event(), taking the name of the event as a string, meaning that
begin(callback) became
event("begin", callback) and so on. A few more characters to type, but so much more versatile and more importantly polymorphism-friendly, as a collider component doesn't even have to know it is a collider component in order to register a collision event, removing the need for casting.
This also ties in with my use of events for player components, which are never called on the player components themselves, but on the parent object (in Vatnsmyrkr, the submarine) and just about any other component that has chosen to
listen to the player's input, which we'll be getting to later.
With the player component comes three new events: "press", "hold" and "release", corresponding to the accessors for the constants. Whenever a button registered by a constant is pressed, held or released, an event will be sent to the player component's parent as well as any registered
listeners. This way, the Submarine doesn't need to constantly check whether the light toggle is pressed by itself, but only respond to the light toggle constant's
pressed (or
released, as I tend to do it in my games) event and run the appropriate code to toggle the light.
But it extends further than this, and this is where it gets really neat…
ListenersSo whereas the player component's parent owns its input and gets automatically notified by any input event, other components can tell the player that they want to be listening in on certain constants, and so they too will get notified when the button(s) linked to that constant is/are pressed, held or released (for which they too need to implement callbacks for the events "press", "hold" and "release", showing again why the event system update was a good thing).
For example, my
door script listens to the "action" button in order to take the player through a door when that button is pressed. But it is not the only eavesdropper. The
monologue (the little speech bubble that pops up above the submarine when the person inside has something to say, which expands to a textbox when the action button is pressed) too is keen on knowing when this button is pressed.
Here we might run into a problem (and my old code before I designed this system did run into that problem): what happens when we are in front of a door and there is a monologue speech bubble as well? Will pressing the action button make the submarine go through the door or open up the speech bubble? Or both? Even tho I'd tried to write a little code in the old system (which wasn't a system at all; just loose, direct input queries), they were in fact both invoked before. That's a bug!
This is where the next little gem comes in…
PrioritiesWhen a listener is registered, it can choose to set a priority number (higher number means higher priority). If no number is set, this will default to 0. When multiple components are listening to the same constant and a press, hold or release event is invoked for it, the player component will go through them in order of priority, starting with those of the highest priority. Only the listener with the highest priority, and any listeners sharing the same priority, will be told about the event, and the rest will be ignored and these listeners won't know that any button was pressed or released or is currently being held down.
This way, as long as there is no listener with a higher priority, all listeners will still be told because they have the same default priority of 0. My doors have a priority of 0. However, I've given the priority of 100 to my monologue after making the design decision that monologues should always go first if there are multiple things trying to get invoked by the action button in the same place. Now the system will make sure that only the monologue will get told about the button press and the door will be ignored.
Now, this would be a problem too, if all listeners were constantly listening. If the monologue was always listening, you could never go through a door, even if there was no monologue in the way, because the monologue would still be listening and get prioritised and the door would be ignored. This is why listeners can, and must, somewhere in their code, make sure to stop listening (which does not unregister them as listeners, but simply puts them on hold) when they do not need to check for any button (for example, when you're not in front of the door) and start listening only when they do (when you're in front of the door).
Locking and autolockingInputs (both callbacks and constants) can be
locked either individually or all together (locking all input for all schemes of the player), which is useful in situations where the player and its listeners must really not be allowed to press a specific button or the like.
When a constant is locked, its pressed, held and released states will always be false and no events will be invoked. When a callback is locked, it will return value and weight both of 0.
There is also the feature of
autolocking. Enabling this will automatically lock everything when the game is in a sequence or paused, which is what one would want for most players.