Thursday, March 17, 2011

Puppeteer Part #1

Hot damn, seems mama Earth is angry again. Tectonic violence, God frustrated, Godzilla, who knows. Thank God the death toll didn't exceed a 5-zeroed number like in Haiti or the 2004 tsunami. When I saw that monstrous whirlpool (wtf, I thought only cartoons had these), and massive waves swallowing up everything with the greatest ease, I expected the casualties to be a lot higher to be honest.

Nevertheless, what a drama. And now that Fukushima reactor. Not only is it still threatening with a meltdown, the whole economy in Japan (the number 3 economy in the world) is offline. Yet, one cold comfort, I was amazed how calm the Japanese people remain. No chaos, plundering, raping or complete dismay. They behave like intelligent, civilized people. Hard working has never been a problem for the Japanese, so I'm sure they have the knowledge, strength and will to overcome all of this. You can do it, let the sun rise again Japan!
-------------------------------------------------------------------------------


Ok, programming then. Instead of graphics I'd like to focus on something else for a change. Controls. You mean keyboards, Wii Nunchucks and that kind of stuff? Yawn. C'mon, everyone knows how to detect a mouse-click or how to check whether the forward button was pressed. Basic stuff, already invented in the Commodore 64 era. Hell, Pong era.

True. But how would you implement everything without knowing what to expect? When dealing with I/O, you usually exactly know what should happen. Sensor X goes off. Then engage actuator Y if analog value Z is lower than Setpoint W. Got to love the logic when dealing with PLC's or Microcontrollers. Clear & simple. Games aren't different, although the diversity of "output" goes a lot wider than switching a few relays or proportional valves. What happens if you press Forward? Pfff, just a grasp:

- Change animation to "walk" (if possible. Maybe the game is paused, or playing a cuts-cene, or you are dead, or ...)
- Move the character forwards... how fast by the way?
- Play sounds (GAS!, in case you drive a vehicle)
- Maybe trigger some A.I. (others can see you better, motion-detector sees you & triggers alarm, ...)

But it gets worse. Games are often context-sensitive. C.J. can walk & run in GTA, but also drive cars, bicycles, apaches, or even jetpacks. And in other situations that "forward" button (or analog stick) has yet another meaning. Like doing dance-moves in that annoying mini-game. And who says Forward = forward? Maybe you prefer the WASD keys for movement. Or maybe you use a gyro-scope sensor in the joystick. Or telekinesis.

Hence, who says you are steering a vehicle or trigger-happy idiot anyway? Maybe you are navigating through a menu, or playing Tetris. Controls(input) on themselves do not know what happens if they are triggered. Unless you hard-code all actions right behind the "onPress(key)" event:

procedure onKeyDown( keyCode : asciiCode );
begin
case keyCode of
13{enter} : player.doHijackCarBiatch();
38:{up} : player.moveForward( doRun := isKeyDown(shift) );
27:{esc} : game.escapeToMainMenu();
end;
end;

I don't like this approach though. First of all, you should be able to change the key-bindings. You can replace the exact ASCII codes with variables of course. But... what if you don't use a keyboard at all? If you plan anything multi-platform, dealing with different input is certainly on the menu. And how about complex controls (pressure sensitive buttons, analog sticks, gyro-scopes, motion detectors) that become more and more popular? Or… howto check a good old Mortal Kombat comb? (FINISH HIM! up, up, left, hold A 2 secs, tap B 3 times within 200 millisecs --> Raiden stuffed in Johny Cages arse, Excellent! Johny Wins).

Then there are also some conditions. The forward button shouldn’t work if your player is dead of course. And walking away during a cut-scene is just plain rude. Asides from all these little problems, I just don't want to hardcode the outcome of a key-event inside the engine core. Who says you are controlling a player at all? And who says it should "walk" when pressing forward? Maybe I like to use the same engine for a wheelchair racing game. Separating engine core business from specific game-related events always makes the design of apparent simple things a lot more difficult.

Off topic. New scene in the make. No, this isn't an engine shot. It's a simple Lightwave model with a MS Paint jacket. Got to start somewhere. By showing each other relative simple models like these, there is still space to suggest, improve, fix or to throw it away.

So… we need some abstraction. Both on input & output sides. I saw a lot of engines have a separate "Input" module (like a DLL), so I started there. This module simply detects input, and puts it in a ControlState we can pass further. Now you can't make EVERYTHING abstract. You could make the Input module so universal that it can handle a NES-8bit joystick, as well as a Space shuttle Simulator. But it will cost you (too) much time. So what I did was defining a list of common inputs:

- forward, backward, left, right, leanleft, leanright, boost
- action1,2,3, reload
- use, examine, pickup, drop
- get down, get up / ascend, descend
- jump
- toggle items / jump to item 0..9
- quick item 1,2,3 (flashlight, nightvision toggle, gun fire modus, etc.)
- team command1,2,3
- focus / aim / zoom
- inventory, stats, briefing, exit
- ...

As you can see, these inputs can be translated to most games. Whether you do a racing, RTS, puzzling or sports game: some navigation, menu and action buttons are always welcome. These inputs have a record with its current state:

TControlInput = record
keybinding : integer; // Key/mouse/button to listen at <-- configurable
isDown : boolean;// pressed at the moment
isPressed : boolean;// from false to true
isReleased : boolean;// from true to false
pressure : single; // 0..100% for analog controls. A keyboard button is just 0 or 100%
releasedTime : single; // secs being released since last down
downTime : single; // secs being down currently
tapCount : integer; // Amount of presses within a short time
end;

// Polling the input (in another module) would be something like this:
if (controlState.input[ INPUT_FORWARD ].isDown) and
(controlState.input[ INPUT_BOOST ].isDown) then --> run

With that info, you can check if a (mouse)button is down, but you can also check for more complex controls, like combo's or button bashing. I also store a few combined controls to make motion vectors (for analog stick, motion sensors, or gyro-scopes). Depending on pressure from 2 (X and Y) inputs, you can make a 2D position and motion vector. In a normal PC game, thise would be used for the Mouselook & cursor position.


Each game cycle updates all (active) inputs by polling the physical key, mouse, stick or whatever it is. What to do with the input depends on other modules, we just pass the info. Don't shoot the messenger! Finally there is also feedback to the Input module. Most joysticks have "rumble" or "force-feedback" function these days. Or other physical output (LED's, sound, buzzers, ...).

To make a long story full of stupid jokes a little bit shorter:

And what we got for all this? Well, we can remove some code from the main-engine. And believe me, less is better when your project grows and grows. But more important, you can reuse and replace this module. If the physical inputs require a complete different approach, we can make adjustments in the DLL or ship the game with a specific platform DLL. The outcome of the DLL remains the same, no matter what key-bindings or inputs you use. That makes input handling in the engine or other modules easier, and reduces the chance on errors.

So far the Input module. It can handle the mouse and keyboard for now, but you can also relative easily add other controllers for specific platforms, without having to litter the core engine or game-code. Next time I’ll tell how you can handle all that input without having to litter your engine with game-specific code.

Talking about input… Still have to make my own Zapper one day, so I can play Halflife with a real gun in my hands. Would be a nice Microcontroller fun project. Ah, another time.

No comments:

Post a Comment