diff --git a/arcade/__init__.py b/arcade/__init__.py index dbf9f2bad7..75a2cdc957 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -145,6 +145,16 @@ def configure_logging(level: int | None = None): from .controller import ControllerManager from .controller import get_controllers +from .input import ActionState +from .input import ControllerAxes +from .input import ControllerButtons +from .input import InputManager +from .input import Keys +from .input import MouseAxes +from .input import MouseButtons +from .input import PSControllerButtons +from .input import XBoxControllerButtons + from .sound import Sound from .sound import load_sound from .sound import play_sound @@ -249,6 +259,15 @@ def configure_logging(level: int | None = None): ) __all__ = [ + "ActionState", + "ControllerAxes", + "ControllerButtons", + "InputManager", + "Keys", + "MouseAxes", + "MouseButtons", + "PSControllerButtons", + "XBoxControllerButtons", "AStarBarrierList", "AnimatedWalkingSprite", "TextureAnimationSprite", 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/examples/sprite_move_input_manager.py b/arcade/examples/sprite_move_input_manager.py new file mode 100644 index 0000000000..70531ead0f --- /dev/null +++ b/arcade/examples/sprite_move_input_manager.py @@ -0,0 +1,147 @@ +""" +Move Sprite With Keyboard or Controller using InputManager + +Simple program to show moving a sprite with the keyboard or controller via InputManager. +This is similar to the behavior in sprite_move_controller and sprite_move_keyboard +but combining the devices using Arcade's advanced input system. + +Artwork from https://kenney.nl + +If Python and Arcade are installed, this example can be run from the command line with: +python -m arcade.examples.sprite_move_input_manager +""" + +import arcade + +SPRITE_SCALING = 0.5 + +WINDOW_WIDTH = 1280 +WINDOW_HEIGHT = 720 +WINDOW_TITLE = "Move Sprite with InputManager Example" + +MOVEMENT_SPEED = 5 + + +class Player(arcade.Sprite): + + def update(self, delta_time: float = 1/60): + """ Move the player """ + # Move player. + # Remove these lines if physics engine is moving player. + self.center_x += self.change_x + self.center_y += self.change_y + + # Check for out-of-bounds + if self.left < 0: + self.left = 0 + elif self.right > WINDOW_WIDTH - 1: + self.right = WINDOW_WIDTH - 1 + + if self.bottom < 0: + self.bottom = 0 + elif self.top > WINDOW_HEIGHT - 1: + self.top = WINDOW_HEIGHT - 1 + + +class GameView(arcade.View): + """ + Main application class. + """ + + def __init__(self): + """ + Initializer + """ + + # Call the parent class initializer + super().__init__() + + # Set our controller to None, this will get changed if we find a connected controller + controller = None + + # Ask arcade for a list of connected controllers + controllers = arcade.get_controllers() + if controllers: + # Just use the first one in the list for now + controller = controllers[0] + + # Create a new InputManager, and assign our controller to it(if we have one) + self.input_manager = arcade.InputManager(controller) + + # Add a new horizontal movement axis to the input manager and assign the LEFT/RIGHT arrow keys and left thumbstick to it + self.input_manager.new_axis("MoveHorizontal") + self.input_manager.add_axis_input_combined("MoveHorizontal", arcade.Keys.RIGHT, arcade.Keys.LEFT) + self.input_manager.add_axis_input("MoveHorizontal", arcade.ControllerAxes.LEFT_STICK_X) + + # Same thing for vertical movement axis + self.input_manager.new_axis("MoveVertical") + self.input_manager.add_axis_input_combined("MoveVertical", arcade.Keys.UP, arcade.Keys.DOWN) + self.input_manager.add_axis_input("MoveHorizontal", arcade.ControllerAxes.LEFT_STICK_Y) + + # Variables that will hold sprite lists + self.player_list = None + + # Set up the player info + self.player_sprite = None + + # Set the background color + self.background_color = arcade.color.AMAZON + + def setup(self): + """ Set up the game and initialize the variables. """ + + # Sprite lists + self.player_list = arcade.SpriteList() + + # Set up the player + self.player_sprite = Player( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=SPRITE_SCALING, + ) + self.player_sprite.center_x = 50 + self.player_sprite.center_y = 50 + self.player_list.append(self.player_sprite) + + def on_draw(self): + """ Render the screen. """ + + # Clear the screen + self.clear() + + # Draw all the sprites. + self.player_list.draw() + + def on_update(self, delta_time): + """ Movement and game logic """ + + # Update the input manager so it has the latest values from our devices + self.input_manager.update() + + # Apply the input axes to the player + self.player_sprite.change_x = self.input_manager.axis("MoveHorizontal") * MOVEMENT_SPEED + self.player_sprite.change_y = self.input_manager.axis("MoveVertical") * MOVEMENT_SPEED + + # Call update to move the sprite + # If using a physics engine, call update player to rely on physics engine + # for movement, and call physics engine here. + self.player_list.update(delta_time) + + +def main(): + """ Main function """ + # Create a window class. This is what actually shows up on screen + window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) + + # Create and setup the GameView + game = GameView() + game.setup() + + # Show GameView on screen + window.show_view(game) + + # Start the arcade game loop + arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/input/manager.py b/arcade/input/manager.py index ccad0484d7..bc1203ade5 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,27 @@ 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. All registered inputs for the action will be removed. + 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( @@ -269,6 +362,16 @@ def add_action_input( mod_ctrl: bool = False, mod_alt: bool = False, ): + """ + Register an input to an action. + + Args: + action: The action to register the input for + input: The input to register + mod_shift: The input will only trigger if the Shift keyboard key is also held + mod_ctrl: The input will only trigger if the Control keyboard key is also held + mod_alt: The input will only trigger if the Alt keyboard key is also held + """ mapping = ActionMapping(input, mod_shift, mod_ctrl, mod_alt) self.actions[action].add_mapping(mapping) @@ -291,6 +394,12 @@ def add_action_input( self.controller_axes_to_actions[input.value].add(action) def clear_action_input(self, action: str): + """ + Clears all registered inputs for a given action. + + Args: + action: The name of the action to clear. + """ self.actions[action]._mappings.clear() _clean_dicts( action, @@ -301,14 +410,38 @@ def clear_action_input(self, action: str): ) def register_action_handler(self, handler: OneOrIterableOf[Callable[[str, ActionState], Any]]): + """ + Register a callback function for all actions from this InputManager. + + The callback function should accept a String with the name of the Action, and an ActionState. + This callback will receive all action events, regardless of if :meth:`arcade.InputManager.subscribe_to_action` has been used as well. + + Args: + handler: The callback function to register. + """ grow_sequence(self.on_action_listeners, handler, append_if=callable) def subscribe_to_action(self, name: str, subscriber: Callable[[ActionState], Any]): + """ + Subscribe a callback to given action. + + The callback function should accept an ActionState parameter. + + Args: + name: The name of the action to subscribe to. + subscriber: The callback function which will be called. + """ old = self.action_subscribers.get(name, set()) old.add(subscriber) self.action_subscribers[name] = old def new_axis(self, name: str): + """ + Create a new axis with the given name. + + Args: + name: The name of the axis + """ if name in self.axes: raise AttributeError(f"Tried to create Axis with duplicate name: {name}") @@ -317,6 +450,14 @@ def new_axis(self, name: str): self.axes_state[name] = 0.0 def add_axis_input(self, axis: str, input: InputEnum, scale: float = 1.0): + """ + Register an input to an axis. + + Args: + axis: The axis to register the input for + input: The input to register + scale: The value to multiply the input by, for non analog inputs the scale value is used literally. + """ mapping = AxisMapping(input, scale) self.axes[axis].add_mapping(mapping) @@ -333,21 +474,59 @@ def add_axis_input(self, axis: str, input: InputEnum, scale: float = 1.0): self.controller_analog_to_axes[input.value] = set() self.controller_analog_to_axes[input.value].add(axis) + def add_axis_input_combined(self, axis: str, positive: InputEnum, negative: InputEnum, scale: float = 1.0): + """ + This is a helper function that wraps :meth:`arcade.InputManager.add_axis_input` to add two inputs + with a positive and negative scale. + + For example, you can do: + add_axis_input_combined("MoveHorizontal", arcade.Keys.RIGHT, arcade.Keys.LEFT, 1.0) + instead of: + add_axis_input("MoveHorizontal", arcade.Keys.RIGHT, 1.0) + add_axis_input("MoveHorizontal", arcade.Keys.LEFT, -1.0) + + Args: + axis: The axis name to register the input for + positive: The input that will correspond to the positive side of the axis + negative: The input that will correspond to the negative side of the axis + scale: The value to multiply the input by, for non analog inputs the scale value is used literally. + """ + self.add_axis_input(axis, positive, scale) + self.add_axis_input(axis, negative, -scale) + def clear_axis_input(self, axis: str): + """ + Clear all registered inputs for the given axis. + + Args: + axis: The axis to clear + """ self.axes[axis]._mappings.clear() _clean_dicts( axis, self.keys_to_axes, self.controller_analog_to_axes, self.controller_buttons_to_axes ) - def remove_axis(self, name: str): - self.clear_axis_input(name) + def remove_axis(self, axis: str): + """ + Completely remove an axis from the manager. This will also clear the registered inputs for that axis. + + Args: + axis: The axis to remove + """ + self.clear_axis_input(axis) - to_remove = self.axes.get(name, None) + to_remove = self.axes.get(axis, None) if to_remove: - del self.axes[name] - del self.axes_state[name] + del self.axes[axis] + del self.axes_state[axis] def axis(self, name: str) -> float: + """ + Get the current value of a given axis. + + Args: + name: The axis to get the value of + """ return self.axes_state[name] def dispatch_action(self, action: str, state: ActionState): @@ -499,6 +678,9 @@ def on_trigger_motion(self, controller: Controller, trigger_name: str, value: fl self.active_device = InputDevice.CONTROLLER def update(self): + """ + Updates axis inputs, all axis values will remain unchanged unless this function is called, usually during on_update. + """ for name in self.axes.keys(): self.axes_state[name] = 0 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 -`. +`.