diff --git a/arcade/application.py b/arcade/application.py index 2a883df819..2a163febd7 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -680,7 +680,7 @@ def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> EVENT_H modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ pass @@ -705,7 +705,7 @@ def on_mouse_drag( Which button is pressed modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ return self.on_mouse_motion(x, y, dx, dy) @@ -730,7 +730,7 @@ def on_mouse_release(self, x: int, y: int, button: int, modifiers: int) -> EVENT - ``arcade.MOUSE_BUTTON_MIDDLE`` modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ return EVENT_UNHANDLED @@ -831,7 +831,7 @@ def on_key_press(self, symbol: int, modifiers: int) -> EVENT_HANDLE_STATE: Key that was just pushed down modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ return EVENT_UNHANDLED @@ -853,7 +853,7 @@ def on_key_release(self, symbol: int, modifiers: int) -> EVENT_HANDLE_STATE: symbol (int): Key that was released modifiers (int): Bitwise 'and' of all modifiers (shift, ctrl, num lock) active during this event. - See :ref:`keyboard_modifiers`. + See :ref:`pg_simple_input_keyboard_modifiers`. """ return EVENT_UNHANDLED @@ -1440,7 +1440,7 @@ def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> bool | modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ pass @@ -1465,7 +1465,7 @@ def on_mouse_drag( Which button is pressed _modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ self.on_mouse_motion(x, y, dx, dy) return False @@ -1492,7 +1492,7 @@ def on_mouse_release(self, x: int, y: int, button: int, modifiers: int) -> bool modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) - active during this event. See :ref:`keyboard_modifiers`. + active during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ pass @@ -1547,7 +1547,7 @@ def on_key_press(self, symbol: int, modifiers: int) -> bool | None: Key that was just pushed down modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) active - during this event. See :ref:`keyboard_modifiers`. + during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ return False @@ -1570,7 +1570,7 @@ def on_key_release(self, symbol: int, modifiers: int) -> bool | None: Key that was released modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) active - during this event. See :ref:`keyboard_modifiers`. + during this event. See :ref:`pg_simple_input_keyboard_modifiers`. """ return False diff --git a/arcade/input/manager.py b/arcade/input/manager.py index ccad0484d7..a89054096c 100644 --- a/arcade/input/manager.py +++ b/arcade/input/manager.py @@ -65,6 +65,28 @@ class InputDevice(Enum): class InputManager: + """ + The InputManager is responsible for managing input for a given device, this can be the keyboard/mouse or a controller. + + In general, you can share one InputManager for one controller and the keyboard/mouse, there are even utilities to handle + automatically switching between them as the active device. However if you intend to have multiple controllers connected + to your game, each controller should have it's own InputManager. + + For runnable examples of how to use this, please see Arcdade's + :ref:`built-in InputManager examples `. + + Args: + controller: + Either a Pyglet Controller object or None if you only want to use the keyboard/mouse. + allow_keyboard: + Whether to allow keyboard input, defaults to True, can be changed safely after initialization. + action_handlers: + Either one or a collection of functions that will be called for every action that is triggered. + :py:meth:`InputManager.subscribe_to_action` may be preferred to subscribe to individual actions instead. + controller_deadzone: + The deadzone for controller input, defaults to 0.1. If changes to axis values are within this + range from the underlying hardware, they will be ignored. + """ def __init__( self, controller: Controller | None = None, @@ -127,6 +149,20 @@ def __init__( self.active_device = InputDevice.CONTROLLER def serialize(self) -> RawInputManager: + """ + Serializes the current state of the InputManager into a RawInputManager dictionary which can easily be saved to JSON. + + This does not include current values of inputs, but rather the structure of the InputManager. Including: + - Actions: All registered actions + - Axes: All registered axis inputs + - Current Mappings: All current mappings of underlying inputs to actions/axis + + The output dictionary of this function can be passed to :meth:`arcade.InputManager.parse` to create a new InputManager + from a serialized one. + + Returns: + A RawInputManager dictionary representing the current state of the InputManager. + """ raw_actions = [] for action in self.actions.values(): raw_actions.append(serialize_action(action)) @@ -141,6 +177,13 @@ def serialize(self) -> RawInputManager: @classmethod def parse(cls, raw: RawInputManager) -> InputManager: + """ + Create a new InputManager from a serialized dictionary. Can be used in combination with the :meth:`arcade.InputManager.serialize` to + save/load input configurations. + + Returns: + A new InputManager with the state defined in the provided RawInputManager dictionary. + """ final = cls(controller_deadzone=raw["controller_deadzone"]) for raw_action in raw["actions"]: @@ -169,6 +212,16 @@ def parse(cls, raw: RawInputManager) -> InputManager: return final def copy_existing(self, existing: InputManager): + """ + Copies the state of another InputManager into this one. Note that this does not create a new InputManager, but modifies the one on which it is called. + + This does not copy current input values, just the structure/mappings of the InputManager. + + If you want to create a new InputManager from an existing one, use :meth:`arcade.InputManager.from_existing` + + Args: + existing: The InputManager to copy from. + """ self.actions = existing.actions.copy() self.keys_to_actions = existing.keys_to_actions.copy() self.controller_buttons_to_actions = existing.controller_buttons_to_actions.copy() @@ -185,19 +238,38 @@ def from_existing( existing: InputManager, controller: pyglet.input.Controller | None = None, ) -> InputManager: + """ + Create a new InputManager from an existing one. This does not copy current input values, just the structure/mappings of the InputManager. + + If you want to create a new InputManager from a serialized dictionary, use :meth:`arcade.InputManager.parse` + + Args: + existing: The InputManager to copy from. + controller: The controller to use for this InputManager. If None, no Controller will be bound. + + Returns: + A new InputManager with the state defined in the provided existing InputManager. + """ new = cls( allow_keyboard=existing.allow_keyboard, controller=controller, controller_deadzone=existing.controller_deadzone, ) new.copy_existing(existing) - new.actions = existing.actions.copy() return new def bind_controller(self, controller: Controller): + """ + Bind a controller to this InputManager. If a controller is already bound, it will be unbound first. + + Upon binding a controller it will be set as the active device. + + Args: + controller: The controller to bind to this InputManager. + """ if self.controller: - self.controller.remove_handlers() + self.unbind_controller() self.controller = controller self.controller.open() @@ -211,6 +283,9 @@ def bind_controller(self, controller: Controller): self.active_device = InputDevice.CONTROLLER def unbind_controller(self): + """ + Unbind the currently bound controller from this InputManager. + """ if not self.controller: return @@ -229,6 +304,11 @@ def unbind_controller(self): @property def allow_keyboard(self): + """ + Whether the keyboard is allowed for this InputManager. This also effects mouse input. + + If this is false then all keyboard and mouse input will be ignored regardless of if there are mappings for them. + """ return self._allow_keyboard @allow_keyboard.setter @@ -251,14 +331,28 @@ def new_action( self, name: str, ): + """ + Create a new action with the given name. If an action with the same name already exists, this will do nothing. + + Args: + name: The name of the action to create. + """ + if name in self.actions: + return + action = Action(name) self.actions[name] = action def remove_action(self, name: str): - self.clear_action_input(name) + """ + Remove the specified action. If the action does not exist, this will do nothing. + Args: + name: The name of the action to remove. + """ to_remove = self.actions.get(name, None) if to_remove: + self.clear_action_input(name) del self.actions[name] def add_action_input( diff --git a/doc/index.rst b/doc/index.rst index bac6112449..3754f22ddd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -121,7 +121,7 @@ help improve Arcade. :caption: Manual programming_guide/sprites/index - programming_guide/keyboard + programming_guide/input/index programming_guide/sound programming_guide/textures programming_guide/event_loop diff --git a/doc/programming_guide/input/advanced_input.rst b/doc/programming_guide/input/advanced_input.rst new file mode 100644 index 0000000000..eecd149e46 --- /dev/null +++ b/doc/programming_guide/input/advanced_input.rst @@ -0,0 +1,138 @@ +.. _pg_advanced_input: + +Advanced Input +============== + +Advanced Input in Arcade is handled through the use of an :class:`arcade.InputManager` + +Key Concepts +------------ + +Actions +^^^^^^^ + +Actions are essentially named actions which can have inputs mapped to them. For example, you might have a ``Jump`` action +with the Spacebar and the bottom controller face button mapped to it. You can then subscribe a callback to this action, which +will be hit whenever the action is triggered, regardless of the underlying input source. + +Axis Inputs +^^^^^^^^^^^ + +Axis Inputs are named inputs similar to actions, but are used for generally analog inputs or more "constant" inputs. These are +intended to be polled for their state, rather than being notified via a callback. Generally these inputs would be used to map onto +analog devices such as thumbsticks, or triggers on controllers, however as we will demonstrate later you can also use buttons or keyboard +input to control these. These inputs generally make it simple to handle something like movement with either keyboard input or a controller. + +A Small Example +--------------- + +Create an InputManager +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + input_manager = arcade.InputManager() + + input_manager.new_action("Jump") + input_manager.add_action_input("Jump", arcade.Keys.SPACE) + input_manager.add_action_input("Jump", arcade.ControllerButtons.BOTTOM_FACE) + + input_manager.new_axis("Move") + input_manager.add_axis_input("Move", arcade.Keys.LEFT, scale=-1.0) + input_manager.add_axis_input("Move", arcade.Keys.RIGHT, scale=1.0) + input_manager.add_axis_input("Move", arcade.ControllerAxes.LEFT_STICK_X, scale=1.0) + +The above block of code demonstrates how you would create an :class:`arcade.InputManager` and create an action for jumping, and +an axis for moving. You'll notice for the movement axis, we assign the left and right keyboard keys with a different scale, but for the +controller input, we only define a positive scale value. This is because a controller feeds us analog input that might range anywhere from +-1.0 to 1.0. When the input is a controller axis, Arcade will multiply the input against the specified scale value, so in this example +using a scale of 1.0 means we get the exact value from the controller. + +However when we assign keyboard keys, or buttons of any kind to an axis, all we know from the underlying input is wether that key/button is +pressed or not, but there is no value to multiply against a scale. In the case of a key/button being added to an axis input, Arcade will +use the scale specified as the value for the axis. + +Handling the Jump Action +^^^^^^^^^^^^^^^^^^^^^^^^ + +For handling actions from our InputManager, we have two options: + +- The global :meth:`arcade.Window.on_action` method which can be added to any :class:`arcade.Window`, :class:`arcade.View`, or :class:`arcade.Section` and will receive notification of all actions. +- A callback function registered to our ``Jump`` action. + +The global :meth:`arcade.Window.on_action` approach: + +.. code-block:: python + + def on_action(self, action: str, state: arcade.ActionState): + if (action == "Jump"): + do_player_jump() + +.. note:: + + If you want to have the ``on_action`` function be on a class other than the Window, View, or Section. You can use :meth:`arcade.InputManager.register_action_handler` to + explicitly register the function to the InputManager. However if the function is on the Window, View, or Section it will receive the actions automatically. + +The callback function approach: + +.. code-block:: python + + def handle_jump(state: arcade.ActionState): + do_player_jump() + + input_manager.subscribe_to_action("Jump", handle_jump) + +Handling the Move Axis +^^^^^^^^^^^^^^^^^^^^^^ + +For handling axis inputs, it is important that we make sure the input manager is being updated. You will need to manually do this as part of your :meth:`arcade.Window.on_update` +function, or via somewhere else that is called every update. + +.. code-block:: python + + input_manager.update() + +When the InputManager is updated, it will update the values of every Axis input within it. You can then poll it simply by using the :meth:`arcade.InputManager.axis` function. +Below is an example of getting the axis value and one way you might use it to move a player. + +.. code-block:: + + player.change_x = input_manager.axis("Move") * PLAYER_MOVEMENT_SPEED + +Active Device Switching +----------------------- + +One question you might have had, is that if we are handling inputs on the "Move" axis for both the keyboard and a controller, which devices input will be used? +The answer depends on a couple different factors. + +It is possible to have never bound a controller to the InputManager, in which case the controller inputs will be ignored. If there is no controller bound, and the ``allow_keyboard`` option +of the InputManager has been set to false, then all Axis values will just return 0, and no actions will ever be triggered. + +However in the scenario that ``allow_keyboard`` is true, and we have a controller bound, the InputManager has somewhat intelligent active device switching which will prioritize the last device that was used. +For example if the controller is currently active, and the user pressed a key on their keyboard, Arcade will switch the active device to the keyboard, so the controller input will be ignored for axis inputs. +If the player then presses a button on their controller, or moves a stick out of the deadzone, then it will switch back to the controller as the active device and ignore keyboard inputs. + +Controller Binding and Multiple Players +--------------------------------------- + +One thing we haven't covered yet, is how the InputManager actually gets a controller bound to it. To keep the InputManager flexible, it does not do this automatically on it's own, and it is up to you to provide +a :class:`pyglet.input.Controller` object to it. See the full examples below for more code on how to create a new Controller. + +Once you have a controller object, you can either bind it to an InputManager during creation by passing it to the ``controller`` argument. Or you can use the :meth:`arcade.InputManager.bind_controller` function after +the InputManager has been created. If you want to unbind the controller, you can use :meth:`arcade.InputManager.unbind_controller`. + +If your game is intended to support multiple players via multiple controllers. The general idea is that you would have one InputManager per controller/player. A common approach to this would be to construct one InputManager +with all of your desired actions/axis inputs, and then create a new one using the :meth:`arcade.InputManager.from_existing` function, as shown below. This function will copy all of the actions/axis from the specified +InputManager into the new one, but ignore the controller binding, allowing you to bind a different controller to the newly created manager. + +.. code-block:: python + + # Not real code, see Pyglet input docs for more on Controller management + controller_one = Controller() + controller_two = Controller() + + input_manager_one = arcade.InputManager(controller_one) + input_manager_one.new_action("Jump") + input_manager_one.add_action_input("Jump", arcade.Keys.SPACE) + + input_manager_two = arcade.InputManager.from_existing(input_manager_one, controller_two) \ No newline at end of file diff --git a/doc/programming_guide/input/index.rst b/doc/programming_guide/input/index.rst new file mode 100644 index 0000000000..47f33cf943 --- /dev/null +++ b/doc/programming_guide/input/index.rst @@ -0,0 +1,25 @@ +.. _pg_input: + +Input Handling +============== + +Arcade has a number of different options for handling input, but they fall into two main categories: + +:ref:`The Simple Way `: This is what you will generally see used in most of Arcade's example code. This way +of working is mostly directly talking with the underlying windowing library, and can work fine for keyboard/mouse +but starts to require a lot of manual work when you want more complex systems, especially when making extensive use +of controllers or mixing different types of input devices(like supporting both keyboard/mouse and controllers). + +:ref:`The Advanced Way `: This is where the advanced input system comes in. The advanced input system provides a very rigidly defined but much more +capable interface for handling input. This system allows defining custom actions, which can be linked to multiple different +input sources(for example a keypress or a button on a controller can trigger the same action). It also supports things like +joystick input from controllers, has utilities for switching between input devices, and more. While this system is more +capable, it has more boilerplate to get started with, and is less flexible than the simple one if you want to build your own +system for input on top of something. + + +.. toctree:: + :maxdepth: 1 + + simple_input + advanced_input \ No newline at end of file diff --git a/doc/programming_guide/keyboard.rst b/doc/programming_guide/input/simple_input.rst similarity index 54% rename from doc/programming_guide/keyboard.rst rename to doc/programming_guide/input/simple_input.rst index 0231b8f26d..3f711dfe36 100644 --- a/doc/programming_guide/keyboard.rst +++ b/doc/programming_guide/input/simple_input.rst @@ -1,27 +1,49 @@ -Keyboard -======== +.. _pg_simple_input: + +Simple Input +============ + +This section will cover simple input handling in Arcade, which consists of just keyboard/mouse devices. + +There are two possible approaches to this to be aware of: + +* Event Based +* Polling + +These two approaches work somewhat different, and will require different levels of code on your end to handle them. +However these approaches are not mutually exclusive, you can use both at the same time for different purposes where +one might be preferable to the other. + +Event Based +----------- -.. _keyboard_events: +With the event based approach, your game will register handlers that Arcade will call whenever an input happens. -Events ------- +For example, when you press the ``A`` button on your keyboard, Arcade would send an event to any registered handlers +for key press events. And then similarly when the key is released, Arcade will send an event to handlers registered for +key release events. -What is a keyboard event? -^^^^^^^^^^^^^^^^^^^^^^^^^ +Using this system if you want to know if a button is currently held down, it would be up to your application to track the +state of the ``A`` key, changing it when your handlers receive a call for press and release events. -Keyboard events are Arcade's representation of physical keyboard interactions. +Polling +------- -For example, if your keyboard is working correctly and you type the letter A -into the window of a running Arcade game, it will see two separate events: +In contrast to the event based approach where Arcade notifies your application of input events, polling is the opposite way +around. With polling you can ask Arcade at any point in time what the state of a given key or mouse button is. This can be +useful when you want to modify some action that is being taken based on if a certain key is currently pressed or not, but +if you rely exclusively on polling, you may not always respond immediately to input actions if you don't poll often enough. -#. a key press event with the key code for ``A`` -#. a key release event with the key code for ``A`` +Whereas with the event based approach, Arcade will trigger your handlers immediately when the event happens. -.. _keyboard_event_handlers: +Keyboard +-------- + +.. _pg_simple_input_keyboard: How do I handle keyboard events? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -You must implement key event handlers. These functions are called whenever a +You must implement key event handler functions. These functions are called whenever a key event is detected: * :meth:`arcade.Window.on_key_press` @@ -31,7 +53,7 @@ You need to implement your own versions of the above methods on your subclass of :class:`arcade.Window`. The :ref:`arcade.key ` module contains constants for specific keys. -For runnable examples, see the following: +For runnable examples, see the following, and look for the ``on_key_press`` and ``on_key_release`` functions: * :ref:`sprite_move_keyboard` * :ref:`sprite_move_keyboard_better` @@ -40,10 +62,23 @@ For runnable examples, see the following: .. note:: If you are using :class:`Views `, you can also implement key event handler methods on them. -.. _keyboard_modifiers: +How do I poll for keyboard state? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If you need to ask Arcade what the state of a given key is, you can do so through your :class:`arcade.Window` class. + +.. code-block:: python + + import arcade + + window = arcade.get_window() + a_key_pressed = window.keyboard[arcade.keys.A] + if a_key_pressed: + print("The A key is pressed") Modifiers ---------- +^^^^^^^^^ + +.. _pg_simple_input_keyboard_modifiers: What is a modifier? ^^^^^^^^^^^^^^^^^^^ @@ -69,7 +104,7 @@ How do I use modifiers? As long as you don't need to distinguish between the left and right versions of modifiers keys, you can rely on the ``modifiers`` argument of :ref:`key event -handlers `. +handlers `. For every key event, the current state of all modifiers is passed to the handler method through the ``modifiers`` argument as a single integer. For each @@ -113,4 +148,4 @@ specific modifier keys are currently pressed! Instead, you have to use specific key codes for left and right versions from :ref:`arcade.key ` to :ref:`track press and release events -`. +`.