Page MenuHomePhabricator

Input
Updated 791 Days AgoPublic

NOTE: This page is incomplete.

Input objects are a way to access a particular variable, e.g. a bool, to use as input. Typically the variable will change state based on some hardware button press or value change, but it may also be a virtual mapping.

There are three main classes that are closest to the user when dealing with input. They are:

  • InputManager
  • InputDevice
  • Input<T>

I'll explain these objects in reverse order so you can understand how they relate to one another.

Input<T>

Input<T> is the closes thing to direct device input that you'll get in Echo.

The Input<T> class is a templated class that maintains a pointer to a variable representing the state of the input on a device. For example, a button will either be pressed or not which is represented as true or false respectively. Input objects have overloads to make accessing and referencing the variables easier, including implicit conversions so you can use them as the type they represent.

The template parameter is whatever type your planning on using as input. Normally it is a bool for a button, or f32 for some kind of value change, but it may also be a more complicated type such as Vector3 or Quaternion. Check the documentation for the specific input you're referencing to know what type you should be using.

Normally you be dealing with shared pointers to input devices. Here is an example of reading the state of an input device for a button that was acquired beforehand (more on acquiring the input devices later).

shared_ptr< Input<bool> > mMyButton;

//... mMyButton is assigned somewhere

//Assuming the 
if(mMyButton)
{
	bool isButtonPressed = *mMyButton;
	//use isButtonPressed (or you could have used *mMyButton directly).
}

You can do the same with different types as well.

InputDevice

InputDevice is just a collection of Input objects which are given names. The names are used to refer to the input objects that you're interested in.

If you have an InputDevice, you can obtain the Input objects directly from it using GetInput() which has the following signature:

shared_ptr< Input<T> > GetInput<T>(const std::string& inputName)

inputName is used to refer to a specific input name. For example, if we had a keyboard input device and wanted to get the space bar button we could do the following:

shared_ptr< Input<bool> > spacebar = aKeyboard->GetInput<bool>("Space");

The type you're interested in acquiring, in this case bool, must match the target input. So the following would not work:

shared_ptr< Input<f32> > spacebar = aKeyboard->GetInput<f32>("Space");
//spacebar would be null after this call.

This is because the buttons on a keyboard are mapped to bool variable and not floating point variables.

Using a device directly to acquire input means that you'll need the input device first. Rather than using the InputDevice directly you can use an InputManager.

InputManager

InputManager objects are given InputDevice objects from which they can source input objects by name.

Input can be acquired by specifying the name of the device and the name of the input separated by a colon. For example:

shared_ptr< Input<T> > input = inputManager->GetInput<T>("InputDeviceName:InputName");

The format of the string is:

InputDeviceName[(deviceNumber)]:InputName

So in the case of multiple devices you can specify the device number in brackets following the device name. The input number is base 1. So the first device has the device number 1. The square brackets denotes that (deviceNumber) is optional. For example:

shared_ptr< Input<bool> > aButton = inputManager->GetInput<bool>("Controller(2):A");

This example gets the A button key from the second controller. If a device or input isn't found then the method will return a null pointer.

If you're using the Application class to build your application/game then you'll have an InputManager with some default devices already installed so you can access input straight away without worrying about setting up anything. Application::GetInputManager() returns a shared_ptr<InputManager>. Check that the pointer is valid then use it to acquire input. Here is the previous example for getting keyboard input from within the Application scope (e.g. if you've inherited from it).

if(GetInputManager())
{
	shared_ptr< Input<bool> > spacebar = GetInputManager()->GetInput<bool>("Keyboard:Space");
}

It is recommended that if you're planning on regularly using input that you don't use GetInput() all the time. Instead store a copy of the pointer and use it when you need to. Here is a simple example (with other implementation details omitted) that is a better design.

/**
* Updates the position of the player object and fires a shot if the shoot button is pressed.
*/
class PlayerInputTask : public Task
{
public:
	const f32 MAX_POSITION=100;
	const f32 MIN_POSITION=-100;
	PlayerInputTask(Player& player, shared_ptr< Input<f32> > horizontalInput, shared_ptr< Input<bool> > shootButton) :
		mPlayer(player),
		mHorizontalInput(horizontalInput),
		mShootButton(shootButton)
	{
	}
	void Update(Seconds lastFrameTime)
	{
		if(mHorizontalInput)
		{
			f32 position = *mHorizontalInput;
			position = std::min(position,MAX_POSITION);
			position = std::max(position,MIN_POSITION);
			mPlayer.SetHorizontalPosition(position);
		}
		if(mShootButton && *mShootButton)
		{
			mPlayer.Shoot();
		}
	}
private:
	Player& mPlayer;
	shared_ptr< Input<f32> > mHorizontalInput;
	shared_ptr< Input<bool> > mShootButton;
};

class MyGame : public Application
{
public:
	/**
	* Setup the player object and update task. This method would be called from somewhere after initial initialisation has occurred.
	*/
	void SetupPlayer()
	{
		mPlayerInputTask=make_shared<PlayerInputTask>(mPlayer,
			GetInputManager()->GetInput<f32>("Controller:XPosition"))
			GetInputManager()->GetInput<bool>("Controller:A"));
		AddTask(*mPlayerInputTask);
	}
private:
	Player mPlayer;
	shared_ptr<PlayerInputTask> mPlayerInputTask;
};

This is a very simple example and probably not what you would do. Typically f32 inputs have a range of 0 to 1 or -1 to 1, unless it is a cursor position. So in this example it would work if Controller:XPosition was the a cursor's X position.

NOTE: TODO: Explain about other input types such as f32, Vector3 and Quaternion.

An application isn't restricted to a single InputManager, but creating a manager won't automatically fill make all devices from other managers available. Instead you'll have to install your own devices. Normally you'll use the Application's InputManager to acquire input from available devices and which will be passed to MappedInputDevices for mapping purposes (see below). If those mapped devices are to be available to other parts of an application then it may be appropriate to group them in another InputManager. I'll leave that decision to you though.

MappedInputDevice

MappedInputDevice is a higher level object that allows you to map one input to another, including mapping digital to analogue input (bool to f32) and vice versa.

Mapped devices are useful for setting up a virtual device that you can remap later. This has the advantage of your application always dealing with the same device and input names. For example, we may like to create a device that has input that describes actions rather than buttons, such as Player:Jump or Player:Shoot. The device could be configured independent of the code using the input. So rather than using "Keyboard:Space" for jump, you could use "Player:Jump". This gives you the flexibility of reconfiguring the device without changing code all over the place. It also means that the input is more descriptive when you use it.

Mapped devices are simple to use. You simply create a device then Create input for it with an assigned mapping.

shared_ptr<MappedInputDevice> mappedDevice = shared_ptr<MappedInputDevice>(new MappedInputDevice("Player",GetInputManager()));
mappedDevice->CreateDigitalInput("Left","Keyboard:Left");
mappedDevice->CreateDigitalInput("Right","Keyboard:Right");
mappedDevice->CreateDigitalInput("Up","Keyboard:Up");
mappedDevice->CreateDigitalInput("Down","Keyboard:Down");
mappedDevice->CreateAnalogInput("HorizontalLook","Controller:XAxis");
mappedDevice->CreateAnalogInput("VerticalLook","Controller:YAxis");

The mapped device needs an input manager to find the inputs.

This device now has six inputs configured. As you can see you don't need to restrict the mapping to a single device. This example probably isn't practical as it looks like the player would be moving with the keyboard and using a controller to look.

The mappedDevice can then be used in one of two ways.

  1. Add the mapped device to an InputManager to make the inputs available through the manager. This will ensure the device is updated while the InputManager is active.
  2. Use the device directly (as earlier example showed) and add the device to a TaskManager (as the device is a Task)

Either way the device needs to be added to some form of TaskManager (InputManager is a TaskGroup so it fulfils this criteria) to make sure the input mappings are updated.

InputDevice call backs

InputDevice objects have a call back feature that allows you to register functions to be called when input state changes are detected in an update.

Lets say we have a controller device and we want to register a call back for when the Start button is pressed so we can pause a task group that represents a game state.

/**
* Pause a task group when the call back is called.
*/
class PauseHandler
{
public:
	PauseHandler(TaskGroup& gameState) : mGameState(gameState){}
	void ButtonChanged (const bool& v)
	{
		if(v)
		{
			if(mGameState->GetPaused())
			{
				mGameState->Resume();
			}else
			{
				mGameState->Pause();
			}
		}
	}
private:
	TaskGroup& mGameState;
};


// Elsewhere... assuming a PauseHandler has been created.
shared_ptr<InputDevice> device = GetInputManager()->GetDevice("Controller");
device->AddChangeCallback<bool>("Start",boost::bind(&PauseHandler::ButtonChanged,&mPauseHandler, _1));

By registering call backs you can receive input notifications when the device is updated and a state change is detected rather than checking for a state change yourself.

The down side to registering call backs on devices is that they will always be invoked for that device. Here I present a solution to this problem using the various things we've learnt so far.

Using MappedInputDevices for different game state

To solve the problem of device call backs being called regardless of game state you could either perform state checking in your call back, which doesn't make for very nice code, or you can create a MappedInputDevice for each game state and register different call backs as needed. Let's look at the following simple example where we are using the devices to represent different player states, each device is active at different times allowing for different behaviour.

class Player : public TaskGroup
{
public:
	Player(shared_ptr<InputManager> inputManager) :
		mOnGroundInput("OnGround",inputManager),
		mInAirInput("InAir",inputManager)
	{
		//Configure some input devices for the states
		mOnGroundInput.AddDigitalInput("Jump","Controller:A");
		mInAirInput.AddDigitalInput("Parachute","Controller:A");
		
		//Map the input callbacks
		mOnGroundInput.AddChangeCallback<bool>("Jump",boost::bind(&Player::JumpButton,this, _1));
		mInAirInput.AddChangeCallback<bool>("Parachute",boost::bind(&Player::ParachuteButton,this, _1));

		// Add the devices to the Player (it is a TaskGroup). We're not interested in adding it to the
		// InputManager because nothing needs to use our device input.
		AddTask(mOnGroundInput);
		AddTask(mInAirInput);
		
		// Set initial activation state of devices. We'll assume we're falling and that the physics system
		// will cause the OnCollideWithGround() method to be called to switch these states around.
		mOnGroundInput.Pause();
		mInAirInput.Resume();
	}
	void Jump()
	{
		//We will only ever jump if we're on the ground so change the state.
		mOnGround.Pause();
		mFalling.Resume();
		
		//Perform vertical impulse on physics body
	}
	
	// The implementation of these three methods is magic and I can't reveal any secrets.
	void Shoot();
	void OpenParachute();
	void CloseParachute();
	
	//Assuming this will be called when the player hits the ground.
	void OnCollideWithGround()
	{
		CloseParachute();
		mOnGround.Resume();
		mFalling.Pause();
	}
private:
	MappedInputDevice mOnGroundInput;
	MappedInputDevice mInAirInput;
	
	void JumpButton(const bool& state)
	{
		if(state)
		{
			Jump();
		}
	}
	
	void ParachuteButton(const bool& state)
	{
		if(state)
		{
			OpenParachute();
		}else
		{
			CloseParachute();
		}
	}
};

class MyGame : public Application
{
public
	MyGame()
	{
		//Initialisation omitted to reduce size.
		mPlayer=make_shared<Player>(GetInputManager());
		AddTask(*mPlayer);
	}
private:
	shared_ptr<Player> mPlayer;
}

As you can see, two input devices are created and they both use the same button, but each maps to a different call back for that button. The Player class controls the activation state of the devices to ensure that only one is active at once.

You probably wouldn't use the device as the state directly in this way but you might use other tasks to represent states which might have their own mapped devices and control animations and sounds.

Last Author
0xseantasker
Last Edited
Feb 27 2022, 11:12 PM

Event Timeline

0xseantasker edited the content of this document. (Show Details)
0xseantasker edited the content of this document. (Show Details)
0xseantasker changed the visibility from "All Users" to "Public (No Login Required)".Mar 6 2019, 4:46 PM