Advanced Input Tutorial
This page dives into AxisPy’s input systems beyond basics: rebindable actions, merged axes, gamepad APIs, mouse coordinate mapping, multitouch, and gestures. It uses the actual Input and InputMap APIs called each frame by the runtime player.
Overview
Input— low-level keyboard, mouse, gamepad, touch and gesture access.InputMap— high-level, rebindable actions with per-frame edge detection.
The runtime RuntimePlayer calls Input.update() and InputMap.update() once per frame. If embedding AxisPy differently, call both in your main loop before querying input.
Editor workflow (no code)
Configure Input Actions (Project Settings) - Project → Project Settings → Input Actions tab. - Click “Add Action” to create a named action (e.g., “jump”, “fire”, “move_left”). - Select the action row → click “Add Key” to bind keys (e.g., Space, Arrow keys, gamepad buttons by name). - The table shows Action Name and bound Keys. These are saved in
project.configand auto-loaded byInputMapat runtime.Using InputMap in scripts (Script Editor) - The Input Actions you define in Project Settings map to
InputMap.register()calls internally. - In the Script Editor, useInputMap.is_pressed("action_name"),is_just_pressed(), oris_just_released()to respond to your configured actions. - No need to hardcode keycodes in scripts—reference the action names you defined.Testing input in the editor - Enter Play Mode; Input and InputMap update automatically per frame. - Use
self.logger.info()to verify actions trigger in the Console dock. - Gamepad input works when a controller is connected; checkInput.get_joystick_count()to confirm detection.
Rebindable Actions with InputMap
Map semantic actions (“jump”, “fire”) to one or more keys. Query held/edge states per frame.
import pygame
from core.input_map import InputMap
class ActionSetup:
def on_start(self):
# One-time bindings (could also load from config)
InputMap.register("jump", [pygame.K_SPACE, pygame.K_w])
InputMap.register("dash", [pygame.K_LSHIFT])
def on_update(self, dt: float):
if InputMap.is_just_pressed("jump"):
self.logger.info("Jump!")
if InputMap.is_pressed("dash"):
self.logger.debug("Dashing...")
if InputMap.is_just_released("dash"):
self.logger.debug("Dash released")
Load from config at startup:
# Example config dict (e.g. from project file)
cfg = {"input_actions": {"jump": [32, 119], "fire": [102]}} # 32=SPACE, 119='w', 102='f'
InputMap.load_from_config(cfg)
Merged Axes (Keyboard + Gamepad)
Use get_axis() to read continuous axes that merge keyboard and the first gamepad’s left stick.
Input.get_axis("Horizontal")→ -1..1 (left=-1, right=+1; or left-stick X)Input.get_axis("Vertical")→ -1..1 (up=+1, down=-1; merges W/S, arrow keys, and left-stick Y inverted so up is +)
from core.input import Input
class AnalogMove:
SPEED = 240
def on_update(self, dt: float):
from core.components import Transform
t = self.entity.get_component(Transform)
if not t:
return
t.x += Input.get_axis("Horizontal") * self.SPEED * dt
t.y -= Input.get_axis("Vertical") * self.SPEED * dt
Gamepad API
Use the direct gamepad helpers for buttons, axes, and D-pad (hat). Deadzone can be tuned globally.
import pygame
from core.input import Input
class GamepadExample:
def on_start(self):
# Optional: tweak deadzone used by get_joy_axis()
Input.set_joystick_deadzone(0.20)
def on_update(self, dt: float):
# Detect connected pads and names
count = Input.get_joystick_count()
if count > 0:
jid = Input.get_joystick_ids()[0]
name = Input.get_joystick_name(jid)
self.logger.info("Pad", id=jid, name=name)
# Buttons: held / edge detection
if Input.get_joy_button(Input.JOY_A):
self.logger.debug("A held")
if Input.get_joy_button_down(Input.JOY_START):
self.logger.info("Start pressed")
if Input.get_joy_button_up(Input.JOY_B):
self.logger.info("B released")
# Sticks and triggers (normalized with deadzone)
lx = Input.get_joy_axis(Input.JOY_LEFT_X)
ly = Input.get_joy_axis(Input.JOY_LEFT_Y) # Note: up is negative here
# D-pad (hat) returns a tuple (-1..1, -1..1)
hx, hy = Input.get_joy_hat()
Mouse Coordinates and Mapping
Input.get_mouse_position()returns raw window coordinates from the OS.Input.get_game_mouse_position()returns coordinates mapped to the engine’s design/render surface.
The runtime sets a window→render mapper every frame, so use get_game_mouse_position for gameplay/UI picking.
from core.input import Input
class MousePick:
def on_update(self, dt: float):
wx, wy = Input.get_mouse_position()
gx, gy = Input.get_game_mouse_position()
self.logger.debug("mouse", window=(wx, wy), game=(gx, gy))
If you run AxisPy in a custom loop, you can provide your own mapper:
from core.input import Input
def my_mapper(window_x, window_y):
# Convert to your game-space (return None to indicate out of bounds)
return (window_x * 0.5, window_y * 0.5)
Input.set_mouse_mapper(my_mapper)
Multitouch and Gestures
Read current touches and frame-specific changes, or use the high-level gesture recognizer.
from core.input import Input
class TouchAndGestures:
def on_update(self, dt: float):
# Touch points
touches = Input.get_touches()
for fid, tp in touches.items():
self.logger.debug("touch", id=fid, pos=(tp.x, tp.y), pressure=tp.pressure)
for tp in Input.get_touches_started():
self.logger.info("touch start", id=tp.finger_id)
for tp in Input.get_touches_moved():
self.logger.debug("touch move", id=tp.finger_id)
for tp in Input.get_touches_ended():
self.logger.info("touch end", id=tp.finger_id)
# Gestures (recognized this frame)
g = Input.get_gesture()
if g.tap:
self.logger.info("tap")
if g.double_tap:
self.logger.info("double_tap")
if g.long_press:
self.logger.info("long_press")
if g.swipe:
self.logger.info("swipe", dir=g.swipe_direction, vel=g.swipe_velocity)
if g.pinch:
self.logger.info("pinch", scale=g.pinch_scale, center=g.pinch_center)
if g.rotate:
self.logger.info("rotate", angle=g.rotate_angle, center=g.rotate_center)
Raw Pygame Events (Advanced)
For uncommon cases, you can read raw SDL/pygame events captured this frame.
import pygame
from core.input import Input
class RawEvents:
def on_update(self, dt: float):
for event in Input.get_events():
if event.type == pygame.MOUSEWHEEL:
self.logger.info("wheel", x=event.x, y=event.y)
Testing with a Provider Stub
Inject a provider to override input for tests or headless runs. Any methods you implement will override Input behavior; others fall back to defaults.
class TestProvider:
def __init__(self):
self._pressed = set()
def get_key(self, key_code):
import pygame
return key_code in self._pressed
def update(self):
self._pressed.add(32) # pretend SPACE is held
from core.input import Input
Input.set_provider(TestProvider())
# ... run a few frames ...
Input.clear_provider()
Script Editor snippets you may need
Quick WASD + Space movement
import pygame
from core.input import Input
from core.components import Transform
class WASDMove:
SPEED = 300
def on_update(self, dt: float):
t = self.entity.get_component(Transform)
if not t:
return
dx = (Input.get_key(pygame.K_d) - Input.get_key(pygame.K_a))
dy = (Input.get_key(pygame.K_s) - Input.get_key(pygame.K_w))
t.x += dx * self.SPEED * dt
t.y += dy * self.SPEED * dt
Mouse click to spawn
import pygame
from core.input import Input
from core.components import Transform
class ClickSpawner:
def on_update(self, dt: float):
if Input.get_mouse_button_down(pygame.BUTTON_LEFT):
mx, my = Input.get_game_mouse_position()
bullet = self.spawn_prefab("prefabs/bullet.json", x=mx, y=my)
self.logger.info("spawned", at=(mx, my))
Toggle pause with a key
import pygame
from core.input_map import InputMap
class PauseToggle:
def on_start(self):
InputMap.register("pause", [pygame.K_ESCAPE, pygame.K_p])
self._paused = False
def on_update(self, dt: float):
if InputMap.is_just_pressed("pause"):
self._paused = not self._paused
self.logger.info("paused" if self._paused else "resumed")