TX/Tutorials and Guides/TX UserGuide/Input
From TDN
Contents |
|
[edit] IntroductionTorque X provides several layers of input management. The reason for this multi-layer system is it allow you to hook into input at a low level or a very high level, depending on your needs. We give only a cursory overview of the lower levels of the input system and pay more attention to the high level systems which you will likely be using more often. [edit] Input Devices and Input EventsAt the lowest level is the input device level. These are low level systems which package input coming into the system and send them out as input events. One of the main reasons for routing all input through the input system is so that we can journal the input and play it back (along with other input) in a deterministic way for debugging. It also makes all input look the same at the lowest level. This is handy because it allows you to subscribe to input events and monitor all input coming into the system. [edit] Input ManagerThe next level of input is the input manager. The input manager listens to all input events and routes them to input maps and the GUI system. Generally, you will not need to deal with input at the event level or the input manager level. [edit] Input MapsInput maps are the first high level system for dealing with input. Input maps allow you to map input events to specific callbacks. For example, the following lines map key presses to zoom a camera in and out:
int keyboardId = InputManager.Instance.FindDevice("keyboard");
InputMap.Global.ProcessBind(keyboardId, (int)Microsoft.Xna.Framework.Input.Keys.Z, ZoomIn);
InputMap.Global.ProcessBind(keyboardId, (int)Microsoft.Xna.Framework.Input.Keys.X, ZoomOut);
Code to map the same methods to the X and Y buttons on gamepad 0: |
int gamepadId = InputManager.Instance.FindDevice("gamepad0");
InputMap.Global.ProcessBind(gamepadId, (int)XGamePadDevice.GamePadObjects.GamePadObjects.X,ZoomIn);
InputMap.Global.ProcessBind(gamepadId, (int)XGamePadDevice.GamePadObjects.GamePadObjects.Y,ZoomOut);
|
|
The code above sets up bindings on the global input map. You can also create stacks of input maps and push and pop entire maps all at once as you transition from one part of your game to another. The input map stack is kept on the input manager, so if you wanted to create the above input map and push it onto the stack you'd do it like so:
InputMap map = new InputMap();
int keyboardId = InputManager.Instance.FindDevice("keyboard");
map.ProcessBind(keyboardId, (int)Microsoft.Xna.Framework.Input.Keys.Z, ZoomIn);
map.ProcessBind(keyboardId, (int)Microsoft.Xna.Framework.Input.Keys.X, ZoomOut);
int gamepadId = InputManager.Instance.FindDevice("gamepad0");
map.ProcessBind(gamepadId, (int)XGamePadDevice.GamePadObjects.GamePadObjects.X,ZoomIn);
map.ProcessBind(gamepadId, (int)XGamePadDevice.GamePadObjects.GamePadObjects.Y,ZoomOut);
// now push the map
InputManager.Instance.PushInputMap(map);
When you want to remove input map (even if others have been pushed in the meantime): // pop the map InputManager.Instance.PopInputMap(map); [edit] Move ManagerInput maps provide a nice higher level grouping for mapping input events to actions. How would one go about hooking a game object up to input? One method would be to create a number of methods on the object like MoveX and MoveY which can then be hooked up to an input map. E.g.,
class MyObj
{
void MoveX(float val) { _move.x = val; }
void MoveY(float val) { _move.y = val; }
void UpdateAnimation(float dt)
{
_velocity += _move * MoveScale;
_velocity.X = MathHelper.Clamp(_velocity.X,-MaxVel,MaxVel);
_velocity.Y = MathHelper.Clamp(_velocity.Y,-MaxVel,MaxVel);
}
// ... for wiki formatting ...
}
That becomes tedious very quickly because of the need to store the move on the object every time input occurs. Not only that, how does one handle an object which wants to be able to respond to keyboard input (wasd) the same way it responds to the game pad? What if one wants a key press to slowly change the value of a move (rather than to snap all the way from +1 to -1, for example) but a stick to change the value immediately? Obviously the management of this for each game object can become very tedious indeed. This is where the MoveManager comes in. Instead of the above, you create an input map which maps to the MoveManager and you associate your game object with the move manager. In addition, you configure the move manager to have the type of move that you want and let it remap keyboard input to stick input if needed. Here's an example: |
// hook up game pad thumbsticks
int gamepadId = InputManager.Instance.FindDevice("gamepad0");
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbX, MoveMapTypes.StickAnalogHorizontal, 0);
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.LeftThumbY, MoveMapTypes.StickAnalogVertical, 0);
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.RightThumbX, MoveMapTypes.StickAnalogHorizontal, 1);
inputMap.BindMove(gamepadId, (int)XGamePadDevice.GamePadObjects.RightThumbY, MoveMapTypes.StickAnalogVertical, 1);
// hook up keyboard to act like sticks
int keyboardId = InputManager.Instance.FindDevice("keyboard");
inputMap.BindMove(keyboardId, (int)Keys.D, MoveMapTypes.StickDigitalRight, 0);
inputMap.BindMove(keyboardId, (int)Keys.A, MoveMapTypes.StickDigitalLeft, 0);
inputMap.BindMove(keyboardId, (int)Keys.W, MoveMapTypes.StickDigitalUp, 0);
inputMap.BindMove(keyboardId, (int)Keys.S, MoveMapTypes.StickDigitalDown, 0);
inputMap.BindMove(keyboardId, (int)Keys.Right, MoveMapTypes.StickDigitalRight, 1);
inputMap.BindMove(keyboardId, (int)Keys.Left, MoveMapTypes.StickDigitalLeft, 1);
inputMap.BindMove(keyboardId, (int)Keys.Up, MoveMapTypes.StickDigitalUp, 1);
inputMap.BindMove(keyboardId, (int)Keys.Down, MoveMapTypes.StickDigitalDown, 1);
|
|
The first block of code binds the game pad thumbsticks (for "gamepad0") to the move manager while the second block of code binds some keys on the keyboard to the same destination. What this means is that the move manager will generate moves (see below for how you receive these moves) which have the thumbsticks and keyboard bound to sticks on the move (accessed via move.Sticks[0] and move.Sticks[1]). The MoveMapType enum controls what kind of binding it is. Typically, the choice is between an analog or a digital binding. E.g., a thumbstick is an analog device (since it takes on a continuous range of values), so we bind the thumb stick to a move stick using an analog mapping. Button presses and keypresses are digital devices (they are either up or down) so we bind them to the stick using a digital mapping. Note that both keyboard and thumbstick are mapped to a move "stick". In this way, the move stick is an abstraction of the gamepad stick -- they aren't the same thing. The move manager can be configured to handle each of these two input types differently. Analog input to a move stick can be rescaled by setting a function curve on it. For example, the following code will configure the move manager to square the input on the horizontal stick:
float [] scalevalues = { 0, 0.25f, 1.0f };
MoveManager.Instance.ConfigureStickHorizontalScaling(0,scalevalues);
The way this scale function works is a function which passes through the points (0,0), (0.5, 0.25), (1,1) is found and used to scale the input (the magnitude of the input is scaled but the sign is kept the same). You can supply more than 3 values in the scale function. If you supplied 4 then the x coordinates assumed would be 0, 0.33, 0.66, and 1.0. Similarly, you can configure how long it takes a button press to change the move stick tracked by the move manager. Consider, |
float [] rampUpValue = null;
float rampUpTime = 0.25f;
float [] rampDownValue = {1, 0.25, 0 };
float rampDownTime = 0.5f;
MoveManager.Instance.ConfigureStickHorizontalTracking(0, rampUpTime, rampUpValue, rampDownTime, rampDownValue);
|
|
This code tells the move manager to take a 1/4 second to move the stick all the way one direction or another. No curve is supplied for the ramp up value so it is done linearly. It takes 1/2 second to return the stick to the center position. Since we pass in a ramp down function, we can see that in half this time (1/4 second) we should return almost the entire way (from 1.0 to 0.25) and that over the next 1/4 second we'll return the rest of the way to 0. [edit] Receiving a Move on your game objectOnce you've set up the move manager to generate moves just like you want them, you are ready to receive moves. You do this by having a component on your object which has a TickCallback. So, for example, in SpaceShooter, PlayerControlComponent._OnRegister contains the following line of code: ProcessList.Instance.AddTickCallback(Owner, this); And PlayerControlComponent also implements ProcessTick to actually process input:
public void ProcessTick(Move move, float elapsed)
{
// ...
if (move != null && move.Buttons.Count > 0 && move.Sticks.Count > 0)
{
if (move.Buttons[1].Pushed)
_switchWeapon(0);
else if (move.Buttons[2].Pushed)
_switchWeapon(1);
else if (move.Buttons[3].Pushed)
_switchWeapon(2);
else if (move.Buttons[4].Pushed)
_switchWeapon(3);
if (move.Sticks[0].X != 0 || move.Sticks[0].Y != 0)
{
// handle left stick
...
}
if (move.Sticks[1].X != 0 || move.Sticks[1].Y != 0)
{
// handle right stick
...
}
...
}
Although this code deals specifically with move sticks, the sticks on the move may be driven by keyboard input, the dpad on the gamepad, or an actual thumbstick. Or they could be a completely different input device like a driving wheel, once the XNA framework includes other input devices. The scaling and timing curves could also be configured according to user preference without affecting the game object code. See below for a more detailed discussion of ProcessTick in the context of how objects are updated in the engine. [edit] PlayerManagerSetting things up so that you have an input map which binds to a move manager which sends moves to your game object involves a few specific steps. 1. You must create an input map with a number of move binds. 2. You need to bind some move manager to the input map (inputMap.MoveManager = moveManager). 3. You must configure a move manager to handle moves how you want them (e.g., ConfigureStickHorizontalScaling and ConfigureStickHorizontalTracking as shown above). 4. You must associate a game object with a move manager (moveManager.Consumer = player).
|
| |
||
[edit] Note |
|
|
||||
|
// Set playerObject as the controllable object PlayerManager.Instance.GetPlayer(playerIndex).ControlObject = playerObject; // Get input map for this player and configure it InputMap inputMap = PlayerManager.Instance.GetPlayer(playerIndex).InputMap; // Get move manager for this player and configure it MoveManager moveManager = PlayerManager.Instance.GetPlayer(playerIndex).MoveManager; By using the PlayerManager you don't need to worry about how the input map, move manager, and player object interact. Simply setting the input map on a player in the player manager will set it up to work with that player (you can either use the one that initially exists for that player or set your own). Setting the control object on a player object makes that object be the recipient of that player's moves. The PlayerManager also provides a convenient place to associate data with players in the game (associate the data with the player not with the game objects the player controls). For example:
PlayerManager.Instance.GetPlayer(0).SetData("score",1000);
String name;
PlayerManager.Instance.GetPlayer(0).GetData("name", out name);
PlayerManager.Player player = PlayerManager.Instance.GetPlayer(killedObject);
if (player != null)
{
// our player was killed...decrement lives and respawn
int lives;
player.GetData("lives",out lives);
player.SetData("lives",--lives);
if (lives>0)
RespawnPlayer(player);
else
GameOver(player);
}
|



