Input Manager

Introduction

The Input System has seen some evolution, from managing only keyboard input to virtually any possible input you want - so long as you create them by extending the InputController class. We've introduced some ideas to simplify input management such as actions (see the InputAction class). CitrusEngine also has its own Keyboard Class, from which you can get keycodes, so that you don't have to import the flash keyboard along with it.

Here's the link to a little example showing a game created in seconds, where you can control two players, both created from the Hero example object. The first is using channel 1 of the input system, the second channel 2. Both are created from the same class so are using the exact same actions (left,right,jump,down), yet since we're using channels, we can add keys to control the first one with WASD, and the second with the arrow keys without having to touch any code inside the object itself. With this example you can even test your keyboard to see if it can manage many key presses at the same time - because very cheap ones often can't !

Actions, explained.

Actions are events, identifiable by a string to be easily managed by users, and are quantized to a frame, Meaning one action triggered on frame 0 will exist for at least 1 frame. It also means since input can't be predicted, all actions are delayed by one frame.

Here's an example :

On my keyboard, I just set my A Key to trigger a “Hello” action in the state's initialize function.

// get the keyboard
var ce:CitrusEngine = CitrusEngine.getInstance();
var kb:Keyboard = ce.input.keyboard;
 
//add action to the keyboard
kb.addKeyAction("Hello", Keyboard.A);

Frame 0:

I'm pressing A. inside this frame, I'm asking input if the A Key was just pressed - or rather, if we just did the “Hello” action.

if (ce.input.justDid("Hello"))
	trace("You're doing the Hello action !");

Now the problem is, I can't know if my keyboard event was done before or after the citrus engine update, and since we're not listening to events here but simplifying events to last one full frame, justDid will not return my action.

if the keyboard event happens to have been received right before one of my object updates, I can still, in that object, see if the action exists by using input.getAction(“Hello”) . This will return an InputAction object if the action exists, with a phase of 0 and a time of 0.

Frame 1 :

Input caught the action on the frame before this one, enqueued it (to quantize it) and “anywhere” in frame 1, the justDid() method will return the correct InputAction object:

if (ce.input.justDid("Hello"))
	trace("You're doing the Hello action !");

Frame 2 : We keep pressing the key and we haven't stopped. and I'm introducing a new method from input you can use to know if we are still doing things.

if (ce.input.justDid("Hello"))
	trace("You're doing the Hello action !";
if (ce.input.isDoing("Hello"))
	trace("You are still doing the Hello action!!";

The output of Frame 2 will be : You are still doing the Hello action!!

as justDid() will only be “set off” once . useful for making your characters jump or do anything that needs to be called once!

Frame 3 : I'm letting go of the A Key now and I'm introducing a new method again.

if (ce.input.justDid("Hello"))
	trace("You're doing the Hello action !";
if (ce.input.isDoing("Hello"))
	trace("You are still doing the Hello action!!";
if (ce.input.hasDone("Hello"))
	trace("You have stopped doing Hello.";

Unfortunately, now you are still doing the Hello action, because actions are quantized - only Frame 4 will output “You have stopped doing Hello.” .

This system lets us unify all types of input, and treating them all the same Anywhere in a Citrus Engine update at the cost of less precision of course.

justDid, isDoing, hasDone returns an InputAction if the action was found, and null if it wasn't.

to access the properties of the action “received”, you can do the following :

var action:InputAction;
if( (action = input.justDid("Hello")) != null )
{
    trace("play just did the Hello action with a value of",action.value);
}

but because flash can dynamically convert an Object to a boolean (true) or null to false, if we don't care about the actual action we don't have to catch it and we can simplify our code as we've done for this example scenario.

if( input.justDid("Hello"))

see the next section for more details…

Actions with values : virtual joystick, gamepad controls...

Action “signals” (the InputAction class) carries a value, a number, which is a great way to grab the position of the Joystick on whatever axis the action is defined on ! This is mostly helpful for intensity of action and a less “digital” feel on gameplay if you are using analogous input actually.

The same can be applied for gamepads as sticks or buttons can send in analog values for a more responsive and clever gameplay not only depending on digital on/off input.

If you are going to create an art installation using Citrus Engine as your “display engine” and some electronic micro controllers for input - from which you might need analog input - this is also great ! You only have to create a new InputController, and have it communicate this way InputController↔sockets↔local server↔usb↔microcontroller. In fact InputAction can also carry strings (InputAction.message for advanced communication)

as of 3.1.8 getActionValue was removed for performance reasons. also, InputAction has new time and message properties which can be exploited to “dispatch” more than a single number and have a sense of time (timed combos anyone?) so it made sense to change the workflow on how you should actually get data from the actions.

you can use the following to get the full action and grab the data it holds :

var action:InputAction;
 
if ((action = ce.input.isDoing("Hello")) != null)
{
	var intensity:Number = action.value;
	trace("You have been doing the Hello action with an intensity of",intensity,"for",action.time,"frames!");
}

if you are using a gamepad controller and you'd just want to know how far the left stick is from its center, you can with this principle easily get access to the controller the action originates from and access its specific properties. Here a StickController is what the left or right stick of a gamepad would be, we check if the action originates from it, and use it to get its “length” property.

var action:InputAction;
var stick:StickController;
 
if ((action = ce.input.isDoing("Hello")) != null)
{
	if(action.controller is StickController)
        {
           stick = action.controller as StickController; 
           trace("you are doing the Hello action with a stick on a gamepad. the stick distance from its center is",stick.length);
        }
}
InputAction also has a message property to carry a String. This is not used by any default controller CE provides, but you could use think of an InputController that might use it… though the input system should not replace an Event or Signal dispatcher to make your objects communicate.

Channels

Channels are only virtual, they are units that help filtering actions. You don't have to have “jump1” and “jump2” for your two heroes. Just have both listen to separate channels.

By default, the keyboard uses channel 0, and so does all the platformer kit objects.

A common misconception about the Hero object in the platformer kit is that the keys for the default actions (“left”, “right”, and “jump”) cannot be changed. This is not true. For an example, to change the Hero object's default jump key from SPACE to UP, remove all actions from the default key and then reassign it to the key you prefer:
kb.removeKeyActions(Keyboard.SPACE);
kb.addKeyAction("jump", Keyboard.UP);

But it's easy to find a scenario where you'd actually need a more complex way of communicating… without having to actually rename your actions… and this is the solution.

In fact it is recommended to use this as a secondary key to get an action, along with the primary key, the “string”.

Input has a “routing” functionality, used in TimeShifter or the BraidDemo. For a period, you can route all actions to a single channel using startRouting(channel) and then you can just stopRouting().

This is not used much, but in a scenario where you need to control 2 Heros alternatively, then its just a matter of routing to the first one's channel, then to the second one . Think of the Sidekick mechanic which is a good scenario to use channel routing so you don't actually have to create the following actions and assign them to your keyboard (which is a bad idea anyway) “LeftHero” “LeftSidekick”

but rather just have the default “left” be routed to channel 0, then to channel 1 alternatively when

hero.inputChannel = 0;
sidekick.inputChannel = 1;

Gamepad

With Adobe's GameInput API now available to all AIR runtimes, we've included gamepad support as of CE 3.1.8. When a gamepad is added to the application, the device is created and individual controls (button, stick) are “wrapped” into InputControllers.

For the moment, the system is not really open to many customizations and the “api” may seem a bit weird, but there is plenty to experiment with and implementation is simple, needing only a couple of lines of code.

Declaring a GamePadManager instance allow you to handle gamepad and InputControllers creation, as well as set up a default “button map” for your gamepad.

It as been successfully tested on Ouya with the Ouya controller, Mac and Win with Xbox controller. Some generic USB controllers are also reported to work.

Let's look at the basic setup first:

var gamePadManager:GamePadManager = new GamePadManager(1);
 
gamePadManager.onControllerAdded.add(function(gamepad:Gamepad):void
{
	gamepad.setStickActions(GamePadMap.STICK_LEFT, "up", "right", "down", "left");
	gamepad.setStickActions(GamePadMap.STICK_RIGHT, "zoomIn", "rotateCW", "zoomOut", "rotateCCW");
 
	gamepad.setButtonAction(GamePadMap.L1, "rotateCCW");
	gamepad.setButtonAction(GamePadMap.R1, "rotateCW");
 
	gamepad.setButtonAction(GamePadMap.L2, "zoomOut");
	gamepad.setButtonAction(GamePadMap.R2, "zoomIn");
 
	gamepad.setButtonAction(GamePadMap.BUTTON_BOTTOM, "jump");
 
	gamepad.setButtonAction(GamePadMap.START, "pause");
	gamepad.setButtonAction(GamePadMap.SELECT, "fullscreen");
 
});

So you start the GamePadManager - do that as soon as possible - and listen to the onControllerAdded signal. The listener will receive the created gamepad, set to the default map that the manager found.

Then it's just a matter of assigning your actions to sticks or buttons. It's as simple as that… assuming the mapping of the gamepad is correct.

The GamePadMap class contains “standardized” variables to identify buttons and sticks. everyone would understand L2/R2… STICK_LEFT / STICK_RIGHT …. BUTTON_BOTTOM may be a bit more confusing but to be clear the “BUTTON” prefix is a reference to the buttons on the right of a common controller, so BOTTOM is X on a ps controller for example.

DPAD, or the “arrow buttons” on the left of a common controller see its 4 directions attributed the default actions up/right/down/left but you can change them in exactly the same way as this example :

gamepad.setButtonAction(GamePadMap.DPAD_DOWN, "down");

Custom Map

You can override what the GamePadManager looks for in a controller name to setup the default maps. By defaultif the controller name (provided by the device itself) has the “XBOX” string in its name, we can assume its an xbox controller. The manager works like that.

Here's how you would simply override eveything the manager does by default to implement your own default map :

var gamePadManager:GamePadManager = new GamePadManager(1);
gamePadManager.devicesMapDictionary = new Dictionary();
gamePadManager.devicesMapDictionary["DEVICE X"] = GamePadMapDeviceX;
gamePadManager.devicesMapDictionary["DEVICE Y"] = GamePadMapDeviceY;

Here we clear up the default map dictionary the gamepad manager will use to determine what the controller is and what map it should be using… and we add our own two custom maps.

“DEVICE X” or “DEVICE Y” would be a substring of the GameInputDevice.name for the map to be applied.

here are the defaults FYI :

devicesMapDictionary["Microsoft X-Box 360"] = Xbox360GamepadMap;
devicesMapDictionary["Xbox 360 Controller"] = Xbox360GamepadMap;
devicesMapDictionary["PLAYSTATION"] = PS3GamepadMap;
devicesMapDictionary["OUYA"] = OUYAGamepadMap;

But what is this magical GamePadMapDeviceX ?

Well, you simply have to write a class following a certain structure. Let's look at the default Xbox360GamepadMap and we'll go through it to understand what's going on.

//Xbox360GamepadMap.as as of 9-11-2013
 
package citrus.input.controllers.gamepad.maps 
{
	import citrus.input.controllers.gamepad.controls.ButtonController;
	import citrus.input.controllers.gamepad.controls.StickController;
	import citrus.input.controllers.gamepad.Gamepad;
 
	public class Xbox360GamepadMap extends GamePadMap
	{
		public function Xbox360GamepadMap():void
		{
 
		}
 
		override public function setupMAC():void
		{
			setupWIN();
		}
 
		override public function setupLNX():void
		{
			setupWIN();
		}
 
		override public function setupWIN():void
		{
			var stick:StickController;
 
			stick = _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_0", "AXIS_1");
			stick.invertY = true; // AXIS_1 is inverted
			stick.threshold = 0.2;
 
			stick = _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_2", "AXIS_3");
			stick.invertY = true; // AXIS_3 is inverted
			stick.threshold = 0.2;
 
			_gamepad.registerButton(GamePadMap.L1,"BUTTON_8");
			_gamepad.registerButton(GamePadMap.R1, "BUTTON_9");
 
			_gamepad.registerButton(GamePadMap.L2, "BUTTON_10");
			_gamepad.registerButton(GamePadMap.R2, "BUTTON_11");
 
			_gamepad.registerButton(GamePadMap.L3, "BUTTON_14");
			_gamepad.registerButton(GamePadMap.R3, "BUTTON_15");
 
			_gamepad.registerButton(GamePadMap.SELECT, "BUTTON_12");
			_gamepad.registerButton(GamePadMap.START, "BUTTON_13");
 
			_gamepad.registerButton(GamePadMap.DPAD_UP,"BUTTON_16","up");
			_gamepad.registerButton(GamePadMap.DPAD_DOWN,"BUTTON_17","down");
			_gamepad.registerButton(GamePadMap.DPAD_RIGHT,"BUTTON_19","right");
			_gamepad.registerButton(GamePadMap.DPAD_LEFT,"BUTTON_18","left");
 
			_gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_7");
			_gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_5");
			_gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_4");
			_gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_6");
		}
 
		override public function setupAND():void
		{
			var stick:StickController;
			var button:ButtonController;
 
			stick = _gamepad.registerStick(GamePadMap.STICK_LEFT,"AXIS_0", "AXIS_1");
			stick.threshold = 0.2;
 
			stick = _gamepad.registerStick(GamePadMap.STICK_RIGHT,"AXIS_11", "AXIS_14");
			stick.threshold = 0.2;
 
			_gamepad.registerButton(GamePadMap.L1,"BUTTON_102");
			_gamepad.registerButton(GamePadMap.R1, "BUTTON_103");
 
			_gamepad.registerButton(GamePadMap.L2, "AXIS_17");
			_gamepad.registerButton(GamePadMap.R2, "AXIS_18");
 
			_gamepad.registerButton(GamePadMap.L3, "BUTTON_106");
			_gamepad.registerButton(GamePadMap.R3, "BUTTON_107");
 
			_gamepad.registerButton(GamePadMap.START, "BUTTON_108");
 
			button = _gamepad.registerButton(GamePadMap.DPAD_UP, "AXIS_16", "up");
			button.inverted = true;
 
			_gamepad.registerButton(GamePadMap.DPAD_DOWN,"AXIS_16","down");
			_gamepad.registerButton(GamePadMap.DPAD_RIGHT, "AXIS_15", "right");
 
			button = _gamepad.registerButton(GamePadMap.DPAD_LEFT, "AXIS_15", "left");
			button.inverted = true;
 
			_gamepad.registerButton(GamePadMap.BUTTON_TOP, "BUTTON_100");
			_gamepad.registerButton(GamePadMap.BUTTON_RIGHT, "BUTTON_97");
			_gamepad.registerButton(GamePadMap.BUTTON_BOTTOM, "BUTTON_96");
			_gamepad.registerButton(GamePadMap.BUTTON_LEFT, "BUTTON_99");
		}
 
	}
 
}

To create your own map class, the class must extend GamePadMap. the super class GamePadMap will look at the platform name the app is running in and run the right functions.

setupWIN // windows
setupMAC // mac
setupLNX // linux
setupAND // android

so if your controller works the same on all platforms…

just setup everything in setupWIN and make the others call setupWIN as well… here setupMAC and setupLNX call setupWIN, considering that for these platforms, the windows map will work ok. we are not sure as this is incomplete but at least it will try.

We have two available controllers, sticks and buttons. the code speaks for itself, you register buttons, or sticks using one of the constant strings in GamePadMap for their names ( ex: GamePadMap.DPAD_RIGHT)

So when you receive the mapped game pad on the other end of the system when a controller is added, you can refer to GamePadMap.DPAD_RIGHT and know what it is.

if some axis are inverted on sticks, you can use invertX or invertY . Buttons can also be inverted (if a button is thought of as an axis, you can use this.)

sticks and buttons also have a “threshold” property which are important because some sensible controller send a lot of “artefact” values or fast changes and without a defined threshold value, it could be like your sticks or buttons have no “dead zone” or are just never turned off - so choose a good value.

  citrus/input.txt · Last modified: 2015/05/16 10:47 by 95.131.150.147
 
Powered by DokuWiki