Source code for simvx.core.input.tick
"""What one physics tick was given: :class:`TickInput` and the pieces it holds.
A frame runs zero, one or several physics ticks, and an input edge belongs to
exactly one moment. Held state is a question about now and answers the same
whenever it is asked inside one tick; an edge is a question about a change, and
handing the same change to three ticks makes a game jump three times on a fast
monitor and not at all on a slow one.
So each tick is given a snapshot. Held state is the state at that tick. **Edges
are populated on the frame's first tick and empty on the rest**, so a frame
running three ticks delivers each press once, to the first, and a frame that
runs no tick carries its edges to the next frame's first tick. Inside
``on_fixed_update`` the ordinary queries read this rather than the frame::
def on_fixed_update(self, dt):
if Input.just_pressed("jump") and self.is_on_floor(): # once, whatever the framerate
self.velocity.y = -450.0
self.velocity.x = Input.value("move").x * 250.0 # the state at this tick
A ``TickInput`` holds no node references and nothing live: every field is a
frozen set, an immutable mapping or a plain value, so it pickles, it can be
recorded, and a test can hand one straight to a tick
(``tree.physics_tick(dt, tick_input=...)``) rather than driving a device to
produce it.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from time import monotonic
from typing import TYPE_CHECKING, NamedTuple
from ..math.types import Vec2
if TYPE_CHECKING:
from .events import PointerState
__all__ = ["ActionState", "ControllerSnapshot", "InputEdge", "TickInput"]
[docs]
class ActionState(NamedTuple):
"""What one action reads for one player.
The three answers the three action queries give, kept together because
they are written together: a button action bound to a trigger is open or
not, reads as a bool, and has an analogue strength.
"""
#: Whether the action is open: held for a button, past the deadzone for a
#: continuous one.
pressed: bool
#: The value in the type the action's bindings gave it: ``bool``, ``float``
#: or :class:`~simvx.core.math.types.Vec2`.
value: bool | float | Vec2
#: How hard it is pushed, 0.0 to 1.0, which is the magnitude of the value.
strength: float
[docs]
class InputEdge(NamedTuple):
"""One action opening or closing, with who it belonged to and when.
Four required fields rather than three and a default. The player matters
because action state is per player, and a default of 0 on a co-op edge is
wrong rather than absent. The time is the device event's, on the monotonic
clock every other input time is on, and a plausible-looking 0.0 would say
the press happened at process start; a tick reading an edge one tick late
is exactly the case that wants the real one.
"""
action: str
pressed: bool
#: Who the action belonged to. Player 0 is the only one a single-player
#: game ever sees.
player: int
time: float
[docs]
class ControllerSnapshot(NamedTuple):
"""One pad as a tick was given it: the indices held and where each axis was.
Indices as the pad reports them, which its layout names: the standard
positions for a pad a mapping database laid out, the driver's own for one
it did not.
"""
buttons: frozenset[int] = frozenset()
axes: Mapping[int, float] = {}
[docs]
@dataclass(slots=True, kw_only=True)
class TickInput:
"""The input one physics tick sees, read through ``Input`` inside the tick.
``Input.tick`` is this object while ``on_fixed_update`` is running and
``None`` everywhere else, so a tick can read the whole snapshot at once
(to record it, to hand it to a replay) as well as query through ``Input``.
The device half (``keys``, ``mouse_buttons``, ``controllers``, the pointer
fields) is honest: it says what the machine was doing, whoever the input
went to. ``actions`` and ``edges`` are routed: they are what *this tree*
was given, resolved through its bindings.
"""
#: The keys physically down, as :class:`~.enums.Key` codes.
keys: frozenset[int] = frozenset()
#: The mouse buttons physically down, as the mouse's own indices.
mouse_buttons: frozenset[int] = frozenset()
#: The connected pads, keyed by the engine's id for each. Empty where none is connected,
#: which is what a game with no controller pays for this field.
controllers: Mapping[int, ControllerSnapshot] = field(default_factory=dict)
#: What each action read at this tick, keyed by ``(player, action)``.
actions: Mapping[tuple[int, str], ActionState] = field(default_factory=dict)
#: Where the pointer was, in screen coordinates.
mouse_position: tuple[float, float] = (0.0, 0.0)
#: How far it moved over the frame this tick belongs to.
mouse_delta: tuple[float, float] = (0.0, 0.0)
#: The frame's wheel travel, ``(x, y)``.
scroll_delta: tuple[float, float] = (0.0, 0.0)
#: Every live pointer, by pointer id, as ``Input.pointers`` reads it. Only
#: the frame's first tick sees a press or a release on one.
pointers: Mapping[int, PointerState] = field(default_factory=dict)
#: The action edges of the frame this tick opens, and empty on every later
#: tick of the same frame.
edges: tuple[InputEdge, ...] = ()
#: When the snapshot was taken, on the monotonic clock.
time: float = field(default_factory=monotonic)