Skip to content

TheCheapestPixels/panda3d-keybindings

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

panda3d-keybindings

Panda3D comes with a nice API suite to work with input devices. In particular, it has one for USB HIDs, and one for mouse and keyboard. What it does not have is a mechanism to build an abstraction over these devices, so that a developer can define them in terms of a set of buttons and axes, and it is a matter of configuration how actual inputs on devices are mapped to those abstract inputs. A game's logic should not be concerned with details like...

  • whether a 2D axis gets its values from a gamepad's stick or its four buttons, from WASD, or a dance pad.
  • how the player wants the inputs from the devices combined. There may be a different list of priorities for different abstract inputs with regard what devices should be checked. A player may prefer to control character movement on a gamepad, but functions like invoking and working in menus with the keyboard.
  • how input is preprocessed. Badly manufactured sticks create noise near the center, and may require a dead zone. An axis' amplitude may need to be scaled or squared.
  • devices connecting or disconnecting. From a game developer's perspective, these events should be dealt with under the hood.
  • how devices are identified. A player may use two flight sticks for a space simulator. If they're of different makes, they can be identified "uniquely", and should be mappable independent of one another. Even with two identical sticks, there should be a way to check which is which ("Press trigger on left stick"), and label them accordingly. NOTE: Not implemented yet. May currently be impossible to do cleanly. Uncleanly, Vendor/Product IDs could be used for devices of different makes.
  • providing an interface to work with the mappings. NOTE: Menu to display the current configuration exists, and functionality to save the current configuration back to file. The DeviceListener's API to change bindings, and menu functionality to do so, are missing. See examples/menu/.
  • if the state, when polled at different times during a frame, is still the same; It just should be. This is quite an edge case, but may cause hard to reproduce bugs.

Status

This project's state is alpha, as features are still being added and its specifications are liable to change. That being said, it is close to reaching beta.

Installation

pip install panda3d-keybindings

Concepts

  • A virtual input is an input with a semantic to the game, like jumping, turning around moving, etc.; It has
    • a type, which is one of
      • button: True if the button is pressed, False otherwise.
      • trigger: True for the frame in which the button is pressed.
      • repeater: True whenever its interval elapses and the button is still pressed.
      • axis: A float.
      • axis2d: panda3d.core.Vec2.
      • axis3d: panda3d.core.Vec3.
    • a list of mappings ordered by priority in which they are checked for their device being present, and whether they have a non-zero / False input value.
    • a sensor definition for each mapping. This defines the buttons / axes used, and specifies post-processing that is to be done on them.
  • A context is a set of virtual inputs that is read together. It is an organizational unit to make it easy for the application to activate or deactivate parts of the user input interface. For example, opening the game's ingame menu may activate the menu context, and deactivate the character_movement one.
  • When a device is connected, it is assigned to a player, or kept unassigned for the time being. Players will only be able to read data from devices assigned to them. NOTE: Currently only single-player assigners exist off-the-shelf.
  • There's a configuration file that defines for each player and each context the virtual_inputs and in what order to read their mappings. If no readable device is present for a virtual_input, its value will be None, otherwise the first mapping with a value other than a zero value or False determines the final value. If all devices have a value of zero or False, that will be returned. In other words, the highest-priority mapping that the player uses is used. NOTE: Currently no concept of players exists in the config file.

Example

Setting up an application for use with this module is easy:

from direct.showbase.ShowBase import ShowBase
from keybindings.device_listener import add_device_listener
from keybindings.device_listener import SinglePlayerAssigner

ShowBase()
add_device_listener(
    assigner=SinglePlayerAssigner(),
)

Now there is a base.device_listener. It assumes that the configuration file is named keybindings.config and is present in the application's base.main_dir, and it creates a task at sort=-10 that freezes this frame's input state. Other names file names and ways to handle freezing can be configured. NOTE: Don't remember off the top of my head how true that is.

A keybinding configuration could look like this:

context demo_context
  button demo_button
    gamepad         face_a
    flight_stick    trigger
    keyboard        q

When the context demo_context is read, ...

base.device_listener.read_context('demo_context')

...the result may look like this:

{'demo_button': False}

This means that due to the config snippet above, the device listener has checked whether a gamepad is connected; If so, the state of face_a is used, if not, the flight_stick is tested next, and so on. In this example, a device has been found and the button has not been pressed.

Configuration File in Detail

As mentioned above, this is a simple configuration file:

context demo_context
  button demo_button
    gamepad         face_a
    flight_stick    trigger
    keyboard        q

The context header indicates the name of the context.

The virtual input header below it defines both its type and name. As mentioned above, valid types are button, trigger, axis, axis2d, and axis3d. There is also repeater, which takes two additional arguments, separated by : characters. The first is the initial cooldown, the second the repeating cooldown. When its button is pressed, and then kept pressed, it will return True in the first frame, then again for one frame after the initial cooldown has passed, and thereafter whenever the repeating cooldown has passed. For example, a repeater that fires after one second, and then every half second, would read repeater:1.0:0.5.

The mapping lines each start with a device name as managed by the assigner (by default Panda3D's device type names are used, plus callback, see below), and then has one sensor for each dimension of the input. button, trigger, repeater, and axis are one-dimensional, and axis2d and axis3d are two- and three-dimensional respectively. However, in the case of axes, pairs of buttons can be used instead. For example:

context demo_context
  axis turning
    gamepad         right_x
    keyboard        arrow_left arrow_right

The arrow buttons will now be read, and their combined value of -1, 0, or 1 will be determined.

Sensor names are as provided by Panda3D. Access to the mouse is given via the sensors mouse_x, mouse_y, mouse_x_delta, and mouse_y_delta, with the two latter tracking frame-to-frame changes (without respect to frame time). For keyboard keys, raw keys may be accessed by prefixing the name with raw-. NOTE: Raw keys will be supported in Panda3D 1.11.

Each sensor may also be post-processed after being read. Each such step is indicated with a flag, some of which may bear a numeric argument, and they are again separated by : characters. For example, right_x:flip would invert the axis (multiplying it with -1), while right_x:deadzone=0.02 would turn all results between -0.02 and 0.02 to 0.0.

  • flip multiplies an axis value (float) with -1, and has no argument.
  • scale multiplies an axis value with its argument.
  • button< and button> turn axis values into button values (boolean), returning True if the axis value is greater / smaller or equal to the argument; e.g. right_x:button>=0.75 will trigger when the stick is pressed far enough to the right.
  • exp magnifies the magnitude to the power of the argument. For example right_x:exp=2 would square the axis value, but preserve its sign; -0.5 would be turned into -0.25, while -1, 0, and 1 are preserved.
  • deadzone, as explained above, turns axis values within the argument's range into 0.0. Without this, a stick could read at a very low value, but still be the final value, while the player actually wants to use a lower-priority device.

Controlling the Read

There are two aspects about reading and freezing the state: When it is done, and how much time it should assume to have passed.

By default, a task is created at sort=-10. If you want to want to use another value, you can pass a dict or arguments to add_device_listener to be passed on to the task creation.

add_device_listener(task_args=dict(sort=-1, priority=1))

If you want instead to control yourself when the input is frozen, you can pass task=False, and then call base.device_listener.read() yourself.

Either way by default globalClock.dt will be used to determine how much time has elapsed. If you want to determine that by yourself as well (which I would warn against; We're talking about inputs here, not the game world's clock), you will have to use your own call as described above, and pass a dt argument indicating the elapsed time. For a trivial example, see examples/minimal/main_2_manual_task.py.

Callbacks

So that's all fine and dandy for typical input devices. What if you want to treat something else entirely as an input device? As long as you can provide a function that takes no arguments, and returns a valid axis or button state, we have you covered.

You can pass a dict with name -> function entries during startup:

add_device_listener(
    callbacks=dict(
	    my_sensor=read_sensors_value,
),
)

...or you can add and remove them at runtime:

base.device_listener.set_callback('my_sensor', read_sensors_value)
base.device_listener.del_callback('my_sensor')

Then in the keybindings.config, use callback as device type, e.g.:

context demo_context
  button my_weird_button
    callback        my_sensor

If no function is currently provided for a sensor, it will be treated like a disconnected device.

An example showing how DirectGui widgets can be used as sensors is provided in examples/callbacks/.

TODO

  • doubleclick and multiclick virtual input types
  • speed/acceleration-based postprocessors. Click if axis changes fast enough.
  • Click-and-drag support for mouse
  • Uniquely identifying devices, and remove the NOTE above
  • Changing bindings at run time
    • Update DeviceListener / Assigner API
    • Add menu functionality
    • Remove the NOTE above
  • Sphinx documentation
  • Throw events
  • setup.py: Go over packages= again.
  • Multiplayer; Might need a full refactor.
    • Assigner
    • config file
    • Remove the NOTEs above

About

Abstract keybindings for Panda3D

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 100.0%