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)