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 input`s 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
Raw data
{
"_id": null,
"home_page": "https://github.com/TheCheapestPixels/panda3d-keybindings",
"name": "panda3d-keybindings",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.5",
"maintainer_email": "",
"keywords": "panda3d keybinding keybindings keymapping",
"author": "TheCheapestPixels",
"author_email": "TheCheapestPixels@gmail.com",
"download_url": "",
"platform": null,
"description": "panda3d-keybindings\n===================\n\nPanda3D comes with a nice API suite to work with input devices. In\nparticular, it has one for USB HIDs, and one for mouse and keyboard.\nWhat it does not have is a mechanism to build an abstraction over these\ndevices, so that a developer can define them in terms of a set of\nbuttons and axes, and it is a matter of configuration how actual inputs\non devices are mapped to those abstract inputs. A game's logic should\nnot be concerned with details like...\n\n* whether a 2D axis gets its values from a gamepad's stick or its four\n buttons, from WASD, or a dance pad.\n* how the player wants the inputs from the devices combined. There may\n be a different list of priorities for different abstract inputs with\n regard what devices should be checked. A player may prefer to control\n character movement on a gamepad, but functions like invoking and\n working in menus with the keyboard.\n* how input is preprocessed. Badly manufactured sticks create noise near\n the center, and may require a dead zone. An axis' amplitude may need\n to be scaled or squared.\n* devices connecting or disconnecting. From a game developer's\n perspective, these events should be dealt with under the hood.\n* how devices are identified. A player may use two flight sticks for a\n space simulator. If they're of different makes, they can be identified\n \"uniquely\", and should be mappable independent of one another. Even\n with two identical sticks, there should be a way to check which is\n which (\"Press trigger on left stick\"), and label them accordingly.\n NOTE: Not implemented yet. May currently be impossible to do cleanly.\n Uncleanly, Vendor/Product IDs could be used for devices of different\n makes.\n* providing an interface to work with the mappings.\n NOTE: Menu to display the current configuration exists, and\n functionality to save the current configuration back to file. The\n DeviceListener's API to change bindings, and menu functionality to do\n so, are missing. See `examples/menu/`.\n* if the state, when polled at different times during a frame, is still\n the same; It just should be. This is quite an edge case, but may cause\n hard to reproduce bugs.\n\n\nStatus\n------\n\nThis project's state is alpha, as features are still being added and its\nspecifications are liable to change. That being said, it is close to\nreaching beta.\n\n\nInstallation\n------------\n\n`pip install panda3d-keybindings`\n\n\nConcepts\n--------\n\n* A `virtual input` is an input with a semantic to the game, like\n jumping, turning around moving, etc.; It has\n * a type, which is one of\n * `button`: `True` if the button is pressed, `False` otherwise.\n * `trigger`: `True` for the frame in which the button is pressed.\n * `repeater`: `True` whenever its interval elapses and the button is\n still pressed.\n * `axis`: A `float`.\n * `axis2d`: `panda3d.core.Vec2`.\n * `axis3d`: `panda3d.core.Vec3`.\n * a list of mappings ordered by priority in which they are checked for\n their device being present, and whether they have a non-zero / False\n input value.\n * a sensor definition for each mapping. This defines the buttons /\n axes used, and specifies post-processing that is to be done on them.\n* A `context` is a set of `virtual input`s that is read together. It is\n an organizational unit to make it easy for the application to activate\n or deactivate parts of the user input interface. For example, opening\n the game's ingame menu may activate the `menu` context, and deactivate\n the `character_movement` one.\n* When a device is connected, it is assigned to a `player`, or kept\n unassigned for the time being. Players will only be able to read data\n from devices assigned to them.\n NOTE: Currently only single-player assigners exist off-the-shelf.\n* There's a configuration file that defines for each `player` and each\n `context` the `virtual_inputs` and in what order to read their\n mappings. If no readable device is present for a `virtual_input`, its\n value will be `None`, otherwise the first mapping with a value other\n than a zero value or `False` determines the final value. If all\n devices have a value of zero or `False`, that will be returned.\n In other words, the highest-priority mapping that the player uses is\n used.\n NOTE: Currently no concept of players exists in the config file.\n\n\nExample\n-------\n\nSetting up an application for use with this module is easy:\n\n from direct.showbase.ShowBase import ShowBase\n from keybindings.device_listener import add_device_listener\n from keybindings.device_listener import SinglePlayerAssigner\n\n ShowBase()\n add_device_listener(\n assigner=SinglePlayerAssigner(),\n )\n\nNow there is a `base.device_listener`. It assumes that the configuration\nfile is named `keybindings.config` and is present in the application's\n`base.main_dir`, and it creates a task at `sort=-10` that freezes this\nframe's input state. Other names file names and ways to handle freezing\ncan be configured.\nNOTE: Don't remember off the top of my head how true that is.\n\nA keybinding configuration could look like this:\n\n context demo_context\n button demo_button\n gamepad face_a\n flight_stick trigger\n keyboard q\n\nWhen the context `demo_context` is read, ...\n\n base.device_listener.read_context('demo_context')\n\n...the result may look like this:\n\n {'demo_button': False}\n\nThis means that due to the config snippet above, the device listener has\nchecked whether a gamepad is connected; If so, the state of `face_a` is\nused, if not, the `flight_stick` is tested next, and so on. In this\nexample, a device has been found and the button has not been pressed.\n\n\nConfiguration File in Detail\n----------------------------\n\nAs mentioned above, this is a simple configuration file:\n\n context demo_context\n button demo_button\n gamepad face_a\n flight_stick trigger\n keyboard q\n\nThe `context` header indicates the name of the context.\n\nThe virtual input header below it defines both its type and name. As\nmentioned above, valid types are `button`, `trigger`, `axis`, `axis2d`,\nand `axis3d`. There is also `repeater`, which takes two additional\narguments, separated by `:` characters. The first is the initial\ncooldown, the second the repeating cooldown. When its button is pressed,\nand then kept pressed, it will return `True` in the first frame, then\nagain for one frame after the initial cooldown has passed, and\nthereafter whenever the repeating cooldown has passed. For example, a\nrepeater that fires after one second, and then every half second, would\nread `repeater:1.0:0.5`.\n\nThe mapping lines each start with a device name as managed by the\nassigner (by default Panda3D's device type names are used, plus\n`callback`, see below), and then has one sensor for each dimension of\nthe input. `button`, `trigger`, `repeater`, and `axis` are\none-dimensional, and `axis2d` and `axis3d` are two- and\nthree-dimensional respectively. However, in the case of axes, pairs of\nbuttons can be used instead. For example:\n\n context demo_context\n axis turning\n gamepad right_x\n keyboard arrow_left arrow_right\n\nThe arrow buttons will now be read, and their combined value of -1, 0,\nor 1 will be determined.\n\nSensor names are as provided by Panda3D. Access to the mouse is given\nvia the sensors `mouse_x`, `mouse_y`, `mouse_x_delta`, and\n`mouse_y_delta`, with the two latter tracking frame-to-frame changes\n(without respect to frame time). For keyboard keys, raw keys may be\naccessed by prefixing the name with `raw-`. NOTE: Raw keys will be\nsupported in Panda3D `1.11`.\n\nEach sensor may also be post-processed after being read. Each such step\nis indicated with a flag, some of which may bear a numeric argument, and\nthey are again separated by `:` characters. For example, `right_x:flip`\nwould invert the axis (multiplying it with -1), while\n`right_x:deadzone=0.02` would turn all results between -0.02 and 0.02 to\n0.0.\n\n* `flip` multiplies an axis value (float) with -1, and has no argument.\n* `scale` multiplies an axis value with its argument.\n* `button<` and `button>` turn axis values into button values (boolean),\n returning True if the axis value is greater / smaller or equal to the\n argument; e.g. `right_x:button>=0.75` will trigger when the stick is\n pressed far enough to the right.\n* `exp` magnifies the magnitude to the power of the argument. For\n example `right_x:exp=2` would square the axis value, but preserve its\n sign; -0.5 would be turned into -0.25, while -1, 0, and 1 are\n preserved.\n* `deadzone`, as explained above, turns axis values within the\n argument's range into 0.0. Without this, a stick could read at a very\n low value, but still be the final value, while the player actually\n wants to use a lower-priority device.\n\n\nControlling the Read\n--------------------\n\nThere are two aspects about reading and freezing the state: When it is\ndone, and how much time it should assume to have passed.\n\nBy default, a task is created at `sort=-10`. If you want to want to use\nanother value, you can pass a dict or arguments to `add_device_listener`\nto be passed on to the task creation.\n\n add_device_listener(task_args=dict(sort=-1, priority=1))\n\nIf you want instead to control yourself when the input is frozen, you\ncan pass `task=False`, and then call `base.device_listener.read()`\nyourself.\n\nEither way by default `globalClock.dt` will be used to determine how\nmuch time has elapsed. If you want to determine that by yourself as well\n(which I would warn against; We're talking about inputs here, not the\ngame world's clock), you *will* have to use your own call as described\nabove, and pass a `dt` argument indicating the elapsed time. For a\ntrivial example, see `examples/minimal/main_2_manual_task.py`.\n\n\nCallbacks\n---------\n\nSo that's all fine and dandy for typical input devices. What if you want\nto treat something else entirely as an input device? As long as you can\nprovide a function that takes no arguments, and returns a valid axis or\nbutton state, we have you covered.\n\nYou can pass a dict with name -> function entries during startup:\n\n add_device_listener(\n callbacks=dict(\n \t my_sensor=read_sensors_value,\n\t),\n )\n\n...or you can add and remove them at runtime:\n\n base.device_listener.set_callback('my_sensor', read_sensors_value)\n base.device_listener.del_callback('my_sensor')\n\nThen in the keybindings.config, use `callback` as device type, e.g.:\n\n context demo_context\n button my_weird_button\n callback my_sensor\n\nIf no function is currently provided for a sensor, it will be treated\nlike a disconnected device.\n\nAn example showing how DirectGui widgets can be used as sensors is\nprovided in `examples/callbacks/`.\n\n\nTODO\n----\n\n* `doubleclick` and `multiclick` virtual input types\n* speed/acceleration-based postprocessors. Click if axis changes fast\n enough.\n* Click-and-drag support for mouse\n* Uniquely identifying devices, and remove the NOTE above\n* Changing bindings at run time\n * Update DeviceListener / Assigner API\n * Add menu functionality\n * Remove the NOTE above\n* Sphinx documentation\n* Throw events\n* `setup.py`: Go over `packages=` again.\n* Multiplayer; Might need a full refactor.\n * Assigner\n * config file\n * Remove the NOTEs above\n\n\n",
"bugtrack_url": null,
"license": "",
"summary": "A more abstract interface for using input devices in Panda3D.",
"version": "0.3a1",
"project_urls": {
"Homepage": "https://github.com/TheCheapestPixels/panda3d-keybindings"
},
"split_keywords": [
"panda3d",
"keybinding",
"keybindings",
"keymapping"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "871806ea6fca5936a4aad1f70ccf01e517536fda6788d289bc926a1c55dcd36a",
"md5": "92754336e1844b41b850e33cba25b885",
"sha256": "3df9014d2da05914e9cdb01b1dfdb7d808bdaf21b20df12b5621241ecda302e8"
},
"downloads": -1,
"filename": "panda3d_keybindings-0.3a1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "92754336e1844b41b850e33cba25b885",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.5",
"size": 12075,
"upload_time": "2023-05-28T14:30:16",
"upload_time_iso_8601": "2023-05-28T14:30:16.377925Z",
"url": "https://files.pythonhosted.org/packages/87/18/06ea6fca5936a4aad1f70ccf01e517536fda6788d289bc926a1c55dcd36a/panda3d_keybindings-0.3a1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-28 14:30:16",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "TheCheapestPixels",
"github_project": "panda3d-keybindings",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"requirements": [],
"lcname": "panda3d-keybindings"
}