Source code for arcade.future.input.manager
# type: ignore
from __future__ import annotations
from collections.abc import Callable
from enum import Enum
from typing import Any, TypeVar
import pyglet
from pyglet.input.base import Controller
from typing_extensions import TypedDict
import arcade
from arcade.future.input import inputs
from arcade.future.input.input_mapping import (
Action,
ActionMapping,
Axis,
AxisMapping,
serialize_action,
serialize_axis,
)
from arcade.future.input.inputs import InputEnum, InputType
from arcade.future.input.raw_dicts import RawAction, RawAxis
from arcade.types import OneOrIterableOf
from arcade.utils import grow_sequence
[docs]
class RawInputManager(TypedDict):
actions: list[RawAction]
axes: list[RawAxis]
controller_deadzone: float
_K = TypeVar("_K")
_V = TypeVar("_V")
def _clean_dicts(to_remove: _V, *dicts_to_clean: dict[_K, set[_V]]) -> None:
"""Clean a dictionary in-place.
This helps simplify serialization code for controller config.
"""
to_discard = []
for to_clean in dicts_to_clean:
if to_discard:
to_discard.clear()
for key, set_value in to_clean.items():
set_value.discard(to_remove)
if len(set_value) == 0:
to_discard.append(key)
for key in to_discard:
del to_clean[key]
[docs]
class InputManager:
def __init__(
self,
controller: Controller | None = None,
allow_keyboard: bool = True,
action_handlers: OneOrIterableOf[Callable[[str, ActionState], Any]] = tuple(),
controller_deadzone: float = 0.1,
):
self.actions: dict[str, Action] = {}
# We don't use defaultdict here since:
# 1. These attributes are unprotected
# 2. That means sets would be created on *any* access by the user
# 2. Those leftover sets would complicate serialization
self.keys_to_actions: dict[int, set[str]] = {}
self.controller_buttons_to_actions: dict[str, set[str]] = {}
self.controller_axes_to_actions: dict[str, set[str]] = {}
self.mouse_buttons_to_actions: dict[int, set[str]] = {}
self.on_action_listeners: list[Callable[[str, ActionState], Any]] = []
self.action_subscribers: dict[str, set[Callable[[ActionState], Any]]] = {}
self.axes: dict[str, Axis] = {}
self.axes_state: dict[str, float] = {}
self.keys_to_axes: dict[int, set[str]] = {}
self.controller_buttons_to_axes: dict[str, set[str]] = {}
self.controller_analog_to_axes: dict[str, set[str]] = {}
self.window = arcade.get_window()
self.register_action_handler(action_handlers)
self._allow_keyboard = allow_keyboard
if self._allow_keyboard:
self.window.push_handlers(
self.on_key_press,
self.on_key_release,
self.on_mouse_press,
self.on_mouse_release,
)
self.active_device = None
if self._allow_keyboard:
self.active_device = InputDevice.KEYBOARD
self.controller = None
self.controller_deadzone = controller_deadzone
if controller:
self.controller = controller
self.controller.open()
self.controller.push_handlers(
self.on_button_press,
self.on_button_release,
self.on_stick_motion,
self.on_dpad_motion,
self.on_trigger_motion,
)
self.active_device = InputDevice.CONTROLLER
[docs]
def serialize(self) -> RawInputManager:
raw_actions = []
for action in self.actions.values():
raw_actions.append(serialize_action(action))
raw_axes = []
for axis in self.axes.values():
raw_axes.append(serialize_axis(axis))
return {
"actions": raw_actions,
"axes": raw_axes,
"controller_deadzone": self.controller_deadzone,
}
[docs]
@classmethod
def parse(cls, raw: RawInputManager) -> InputManager:
final = cls(controller_deadzone=raw["controller_deadzone"])
for raw_action in raw["actions"]:
name = raw_action["name"]
final.new_action(name)
for raw_mapping in raw_action["mappings"]:
input_instance = inputs.parse_instance(raw_mapping)
final.add_action_input(
name,
input_instance,
raw_mapping["mod_shift"],
raw_mapping["mod_ctrl"],
raw_mapping["mod_alt"],
)
for raw_axis in raw["axes"]:
name = raw_axis["name"]
final.new_axis(name)
for raw_mapping in raw_axis["mappings"]:
input_instance = inputs.parse_instance(raw_mapping)
final.add_axis_input(name, input_instance, raw_mapping["scale"])
return final
[docs]
def copy_existing(self, existing: InputManager):
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()
self.mouse_buttons_to_actions = existing.mouse_buttons_to_actions.copy()
self.axes = existing.axes.copy()
self.axes_state = existing.axes_state.copy()
self.controller_buttons_to_axes = existing.controller_buttons_to_axes.copy()
self.controller_analog_to_axes = existing.controller_analog_to_axes.copy()
self.controller_deadzone = existing.controller_deadzone
[docs]
@classmethod
def from_existing(
cls,
existing: InputManager,
controller: pyglet.input.Controller | None = None,
) -> 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
[docs]
def bind_controller(self, controller: Controller):
if self.controller:
self.controller.remove_handlers()
self.controller = controller
self.controller.open()
self.controller.push_handlers(
self.on_button_press,
self.on_button_release,
self.on_stick_motion,
self.on_dpad_motion,
self.on_trigger_motion,
)
self.active_device = InputDevice.CONTROLLER
[docs]
def unbind_controller(self):
if not self.controller:
return
self.controller.remove_handlers(
self.on_button_press,
self.on_button_release,
self.on_stick_motion,
self.on_dpad_motion,
self.on_trigger_motion,
)
self.controller.close()
self.controller = None
if self._allow_keyboard:
self.active_device = InputDevice.KEYBOARD
@property
def allow_keyboard(self):
return self._allow_keyboard
@allow_keyboard.setter
def allow_keyboard(self, value: bool):
if self._allow_keyboard == value:
return
self._allow_keyboard = value
if self._allow_keyboard:
self.window.push_handlers(
self.on_key_press,
self.on_key_release,
self.on_mouse_press,
self.on_mouse_release,
)
else:
self.window.remove_handlers(self)
[docs]
def remove_action(self, name: str):
self.clear_action_input(name)
to_remove = self.actions.get(name, None)
if to_remove:
del self.actions[name]
[docs]
def add_action_input(
self,
action: str,
input: InputEnum,
mod_shift: bool = False,
mod_ctrl: bool = False,
mod_alt: bool = False,
):
mapping = ActionMapping(input, mod_shift, mod_ctrl, mod_alt)
self.actions[action].add_mapping(mapping)
if mapping._input_type == InputType.KEYBOARD:
# input is guaranteed to be an instance of Keys enum at this point
if input.value not in self.keys_to_actions:
self.keys_to_actions[input.value] = set()
self.keys_to_actions[input.value].add(action)
elif mapping._input_type == InputType.CONTROLLER_BUTTON:
if input.value not in self.controller_buttons_to_actions:
self.controller_buttons_to_actions[input.value] = set()
self.controller_buttons_to_actions[input.value].add(action)
elif mapping._input_type == InputType.MOUSE_BUTTON:
if input.value not in self.mouse_buttons_to_actions:
self.mouse_buttons_to_actions[input.value] = set()
self.mouse_buttons_to_actions[input.value].add(action)
elif mapping._input_type == InputType.CONTROLLER_AXIS:
if input.value not in self.controller_axes_to_actions:
self.controller_axes_to_actions[input.value] = set()
self.controller_axes_to_actions[input.value].add(action)
[docs]
def clear_action_input(self, action: str):
self.actions[action]._mappings.clear()
_clean_dicts(
action,
self.keys_to_actions,
self.controller_buttons_to_actions,
self.controller_axes_to_actions,
self.mouse_buttons_to_actions,
)
[docs]
def register_action_handler(self, handler: OneOrIterableOf[Callable[[str, ActionState], Any]]):
grow_sequence(self.on_action_listeners, handler, append_if=callable)
[docs]
def subscribe_to_action(self, name: str, subscriber: Callable[[ActionState], Any]):
old = self.action_subscribers.get(name, set())
old.add(subscriber)
self.action_subscribers[name] = old
[docs]
def new_axis(self, name: str):
if name in self.axes:
raise AttributeError(f"Tried to create Axis with duplicate name: {name}")
axis = Axis(name)
self.axes[name] = axis
self.axes_state[name] = 0.0
[docs]
def add_axis_input(self, axis: str, input: InputEnum, scale: float = 1.0):
mapping = AxisMapping(input, scale)
self.axes[axis].add_mapping(mapping)
if mapping._input_type == InputType.KEYBOARD:
if input.value not in self.keys_to_axes:
self.keys_to_axes[input.value] = set()
self.keys_to_axes[input.value].add(axis)
elif mapping._input_type == InputType.CONTROLLER_BUTTON:
if input.value not in self.controller_buttons_to_axes:
self.controller_buttons_to_axes[input.value] = set()
self.controller_buttons_to_axes[input.value].add(axis)
elif mapping._input_type == InputType.CONTROLLER_AXIS:
if input.value not in self.controller_analog_to_axes:
self.controller_analog_to_axes[input.value] = set()
self.controller_analog_to_axes[input.value].add(axis)
[docs]
def clear_axis_input(self, axis: str):
self.axes[axis]._mappings.clear()
_clean_dicts(
axis, self.keys_to_axes, self.controller_analog_to_axes, self.controller_buttons_to_axes
)
[docs]
def remove_axis(self, name: str):
self.clear_axis_input(name)
to_remove = self.axes.get(name, None)
if to_remove:
del self.axes[name]
del self.axes_state[name]
[docs]
def dispatch_action(self, action: str, state: ActionState):
arcade.get_window().dispatch_event("on_action", action, state)
for listener in self.on_action_listeners:
listener(action, state)
if action in self.action_subscribers:
for subscriber in tuple(self.action_subscribers[action]):
subscriber(state)
[docs]
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> None:
if not self._allow_keyboard:
return
self.active_device = InputDevice.KEYBOARD
mouse_buttons_to_actions = tuple(self.mouse_buttons_to_actions.get(button, set()))
for action_name in mouse_buttons_to_actions:
action = self.actions[action_name]
hit = True
for mapping in tuple(action._mappings):
if mapping._modifiers:
for mod in mapping._modifiers:
if not modifiers & mod:
hit = False
if hit:
self.dispatch_action(action_name, ActionState.PRESSED)
[docs]
def on_key_press(self, key: int, modifiers: int) -> None:
if not self._allow_keyboard:
return
self.active_device = InputDevice.KEYBOARD
keys_to_actions = tuple(self.keys_to_actions.get(key, set()))
for action_name in keys_to_actions:
action = self.actions[action_name]
hit = True
for mapping in tuple(action._mappings):
if mapping._modifiers:
for mod in mapping._modifiers:
if not modifiers & mod:
hit = False
if hit:
self.dispatch_action(action_name, ActionState.PRESSED)
[docs]
def on_mouse_release(self, x: int, y: int, button: int, modifiers: int) -> None:
if not self._allow_keyboard:
return
mouse_buttons_to_actions = tuple(self.mouse_buttons_to_actions.get(button, set()))
for action_name in mouse_buttons_to_actions:
action = self.actions[action_name]
hit = True
for mapping in tuple(action._mappings):
if mapping._modifiers:
for mod in mapping._modifiers:
if not modifiers & mod:
hit = False
if hit:
self.dispatch_action(action_name, ActionState.RELEASED)
[docs]
def on_key_release(self, key: int, modifiers: int) -> None:
if not self._allow_keyboard:
return
# What, why are we doing any of this repeat tuple conversion in here?
keys_to_actions = tuple(self.keys_to_actions.get(key, set()))
for action_name in keys_to_actions:
action = self.actions[action_name]
hit = True
for mapping in tuple(action._mappings):
if mapping._modifiers:
for mod in mapping._modifiers:
if not modifiers & mod:
hit = False
if hit:
self.dispatch_action(action_name, ActionState.RELEASED)
[docs]
def on_button_press(self, controller: Controller, button_name: str):
self.active_device = InputDevice.CONTROLLER
buttons_to_actions = tuple(self.controller_buttons_to_actions.get(button_name, set()))
for action_name in buttons_to_actions:
self.dispatch_action(action_name, ActionState.PRESSED)
[docs]
def on_button_release(self, controller: Controller, button_name: str):
buttons_to_actions = tuple(self.controller_buttons_to_actions.get(button_name, set()))
for action_name in buttons_to_actions:
self.dispatch_action(action_name, ActionState.RELEASED)
[docs]
def on_stick_motion(self, controller: Controller, name: str, motion: pyglet.math.Vec2):
x_value, y_value = motion.x, motion.y
if name == "leftx":
self.window.dispatch_event(
"on_stick_motion",
self.controller,
"leftxpositive" if x_value > 0 else "leftxnegative",
x_value,
y_value,
)
elif name == "lefty":
self.window.dispatch_event(
"on_stick_motion",
self.controller,
"leftypositive" if y_value > 0 else "leftynegative",
x_value,
y_value,
)
elif name == "rightx":
self.window.dispatch_event(
"on_stick_motion",
self.controller,
"rightxpositive" if x_value > 0 else "rightxpositive",
x_value,
y_value,
)
elif name == "righty":
self.window.dispatch_event(
"on_stick_motion",
self.controller,
"rightypositive" if y_value > 0 else "rightynegative",
x_value,
y_value,
)
axes_to_actions = self.controller_axes_to_actions.get(name, set())
if (
x_value > self.controller_deadzone
or x_value < -self.controller_deadzone
or y_value > self.controller_deadzone
or y_value < -self.controller_deadzone
):
self.active_device = InputDevice.CONTROLLER
for action_name in axes_to_actions:
self.dispatch_action(action_name, ActionState.PRESSED)
return
for action_name in axes_to_actions:
self.dispatch_action(action_name, ActionState.RELEASED)
[docs]
def on_dpad_motion(self, controller: Controller, motion: pyglet.math.Vec2):
self.active_device = InputDevice.CONTROLLER
[docs]
def on_trigger_motion(self, controller: Controller, trigger_name: str, value: float):
self.active_device = InputDevice.CONTROLLER
[docs]
def update(self):
for name in self.axes.keys():
self.axes_state[name] = 0
if self.controller and self.active_device == InputDevice.CONTROLLER:
for name, axis in self.axes.items():
for mapping in tuple(axis._mappings):
if mapping._input_type == InputType.CONTROLLER_AXIS:
scale = mapping._scale
input = getattr(self.controller, mapping._input.value) # type: ignore
if input > self.controller_deadzone or input < -self.controller_deadzone:
self.axes_state[name] = input * scale
if mapping._input_type == InputType.CONTROLLER_BUTTON:
if getattr(self.controller, mapping._input.value): # type: ignore
self.axes_state[name] = mapping._scale
elif self.active_device == InputDevice.KEYBOARD and self._allow_keyboard:
for name, axis in self.axes.items():
for mapping in tuple(axis._mappings):
if mapping._input_type == InputType.KEYBOARD:
if self.window.keyboard[mapping._input.value]:
self.axes_state[name] = mapping._scale
elif mapping._input_type == InputType.MOUSE_AXIS:
self.axes_state[name] = (
self.window.mouse[mapping._input.name.lower()] * mapping._scale
)
elif mapping._input_type == InputType.MOUSE_BUTTON:
if self.window.mouse[mapping._input.value]:
self.axes_state[name] = mapping._scale