Scripting API (Python)¶
Gameplay logic in Pite is authored in Python scripts that inherit from engine node classes in the embedded pite module. Scripts execute directly within the engine process via embedded CPython 3.12, providing high performance with full access to standard Python features.
import pite
class Player(pite.Node2D):
speed: float = 200.0
def _ready(self):
self.position = (100.0, 200.0)
def _process(self, delta: float):
if pite.held.right:
self.position.x += self.speed * delta
Script Architecture and Lifecycle¶
- Class Mapping: Each Python script file defines a single node class. Scripts are attached to scene nodes via the scene's
[node.script]definition (pathandclass). Class and module names follow standard Python conventions (snake_casefilenames andPascalCaseclasses). _ready(self): Invoked once when the node enters the active scene tree. Use this for one-time initialization, finding child nodes, and connecting signals._process(self, delta: float): Invoked every frame during variable-rate rendering updates. Thedeltaparameter provides the elapsed frame duration in seconds (clamped to a maximum of0.1to avoid physics tunneling during stalls)._physics_process(self, delta: float): Invoked at a fixed 60 Hz rate (delta = 1/60) for deterministic physics simulation and character movement.- Graceful Lifecycle: Missing lifecycle methods (
_ready,_process,_physics_process) are safe no-ops and never produce errors.
Core Node Properties¶
Node2D¶
All 2D spatial objects inherit from pite.Node2D:
position: Vec2: Mutable 2D position vector relative to the parent. Supports in-place component mutation (self.position.x += 10), assignment via tuples or vectors (self.position = (100, 200)), and tuple unpacking (x, y = self.position).rotation: float: Orientation in radians. Rotates descendants around the node's local origin.
Visual Elements¶
Sprite2D: InheritsNode2D. Addstexture: str(res://path to image),atlas: str(res://path to.atlas.json), andframe: str(active frame name within the atlas).Camera2D: InheritsNode2D. Addszoom: float(default1.0). Centers the game viewport around the camera's global position.
User Interface Elements¶
Control: Base UI class inheritingNode2D. Providesfont_size: float(text size in world units, default16.0) andcolor: str(hex string e.g."#ffffff").Label: InheritsControl. Addstext: strfor rendering text centered on the node position.Button: InheritsControl. Addstext: str,size: Vec2(interactive bounding box),disabled: bool, and a built-inpressedsignal.
Calling Superclass _process
If your script overrides _process on a Timer or Button subclass, you must invoke super()._process(delta) to allow the engine to process internal timer accumulation and click polling.
Timers¶
Timer: Standalone timing node providingwait_time: floatandcurrent_elapsed: float. Fires the engine-ownedtimeoutsignal each timewait_timeseconds elapse (looping automatically). Non-positivewait_timevalues pause the timer.
Node References and Navigation¶
Declarative Descriptors¶
Declare node references cleanly as class attributes using descriptors:
Descriptors resolve on first access and cache the resulting node proxy. If the target node is missing, access raises a KeyError.
Dynamic Traversal via get_node¶
Traverse the scene hierarchy dynamically using relative or absolute path strings:
- Paths are relative to the calling node:
.(self),..(parent),ChildName(direct child), or/root/…(absolute path from root). - If a target node path does not exist,
get_noderaises aKeyError. - Repeated lookups through
get_nodeare automatically cached by the engine runtime. - For dynamic label updates, any node proxy exposes a convenient
textproperty:
Custom Properties¶
Nodes can access project-level custom properties declared under [props] in pite.toml:
# Read custom prop (returns None if unset):
spawn = self.get_custom_property("spawn_group")
# Write custom prop (type-checked against manifest, raises ValueError on mismatch):
self.set_custom_property("spawn_group", "elite_guards")
# Remove custom prop (returns True if it was present):
was_present = self.remove_custom_property("spawn_group")
Schema properties (position, text, etc.) are rejected by get_custom_property/set_custom_property/remove_custom_property with ValueError — use direct typed attributes instead.
Signals¶
Nodes react to each other with typed signals. Declare them as class variables; access them as members:
class Player(pite.Node2D):
hit = pite.signal(int)
def _process(self, delta):
if pite.pressed.space:
self.hit.emit(1)
pite.signal(...)takes payload types, bare or as strings (pite.signal(int, "str")). Bothhit = pite.signal(int)and the annotatedhit: pite.signal(int)form declare the same signal.- Emitting the wrong arity or type fails loudly; emitting an undeclared signal fails loudly. There are no untyped string signals.
connect(handler)connects any Python callable (bound methods, lambdas, free functions).Buttonclicks arrive as a zero-payloadpressedsignal fired by the engine (press-inside plus release-inside).pressedbelongs to theButtontype itself — just connect it, no declaration needed (re-declaring it is harmless):
class HitButton(pite.Button):
def _ready(self):
self.pressed.connect(self.on_pressed)
def on_pressed(self):
self.get_node("../Player").hit.emit(1)
Input¶
if pite.held.right:
self.position.x += self.speed * delta
if pite.pressed.space:
self.position.x += 10.0
pite.held.<key>is true while down (continuous movement);pite.pressed.<key>fires on the single frame it goes down (single steps);pite.released.<key>on the frame it comes up.mouse()returns aVec2with.xand.y(and tuple unpackingx, y = pite.mouse()).- Key attributes:
space,enter,escape,tab,backspace,shift,ctrl,alt,left,right,up,down,mouse_left,mouse_right,mouse_middle,a–z,num0–num9. pite.screen_to_world((screen_x, screen_y)) -> (world_x, world_y)transforms screen pixel coordinates to world coordinates using the current camera view (used e.g. for button hit-testing).- String-based input calls (e.g.,
pite.held("Space")) are deprecated and raiseTypeError. Input is pumped on the main thread and visible inside_processand_physics_process.
Audio¶
voice = pite.play("res://sfx/hit.wav", volume=0.8)
pite.set_volume(voice, 0.5)
if not pite.is_playing(voice):
pass
pite.stop(voice)
play(path, volume=1.0)decodes the file and returns a voice id;.wav(8/16/24/32-bit int and float),.ogg(Vorbis),.mp3, and.flacare supported. Output goes to the OS default audio device; on machines with no audio device playback is simulated (a warning is logged; same API, no sound). A missing or undecodable file raises loudly — the game keeps running.stop(id)on an unknown id is a silent no-op, as isset_volume(clamped to0.0–1.0);is_playingon unknown ids isFalse.pite checkvalidates everyplay("…")literal in project scripts: the file must exist and decode.
Tilemaps¶
class Level(pite.TileMap2D):
def _ready(self):
# Cell queries and mutations
tile = self.get_cell(0, 0, layer=0)
self.set_cell(0, 0, "grass", layer=0)
self.erase_cell(0, 0, layer=0)
self.clear(layer=None)
# Coordinate math
col, row = self.world_to_map(pite.mouse())
pos = self.map_to_world(col, row)
# Tilemap properties & introspection
tile_w, tile_h = self.tile_size
layers = self.layers
cells = self.get_used_cells(layer=0)
tile_size -> tuple[int, int]: Tile dimensions(width, height)in pixels.layers -> list[str]: List of layer names in the tilemap.get_cell(col, row, layer=0) -> str | None: Returns the atlas frame name at(col, row).set_cell(col, row, frame, layer=0): Places an atlas frame at(col, row).erase_cell(col, row, layer=0) -> bool: Erases cell at(col, row).clear(layer=None): Clears all cells in the specified layer (or all layers if omitted).world_to_map(pos_or_x, y=None) -> tuple[int, int]: Converts world position to grid cell(col, row).map_to_world(col, row) -> Vec2: Converts grid cell(col, row)to world center position.get_used_cells(layer=0) -> list[tuple[int, int]]: Returns list of populated(col, row)coordinates.
Physics¶
Pite includes a pure-Rust 2D physics subsystem built on rapier2d. Simulation runs in a fixed 60 Hz loop (dt = 1/60), executing _physics_process(delta) across active scripts.
Nodes¶
StaticBody2D: Non-moving collision body (floors, walls).CharacterBody2D: Kinematic character controller withmove_and_slide().RigidBody2D: Dynamic simulation body affected by gravity and forces.Area2D: Trigger zones with entry/exit overlap signals.CollisionShape2D: Child of any physics body or area defining geometric collider (shape:"box","circle","capsule").
Character Movement¶
class Player(pite.CharacterBody2D):
speed: float = 200.0
def _physics_process(self, delta: float):
if pite.held.right:
self.velocity.x = self.speed
elif pite.held.left:
self.velocity.x = -self.speed
else:
self.velocity.x = 0.0
# Gravity
self.velocity.y += 980.0 * delta
if pite.pressed.space and self.is_on_floor:
self.velocity.y = -400.0
self.move_and_slide()
move_and_slide() -> bool: Advances shape alongvelocity * dtusing obstacle sliding collision resolution (ignoring sensor areas). Updatesvelocity,position, and collision contact flags.is_on_floor: bool,is_on_wall: bool,is_on_ceiling: bool: Collision contact flags relative toup_direction(default(0.0, -1.0)). These are attributes, not methods —if self.is_on_floor:(no parens). Calling them as methods raisesTypeError.
RigidBody2D Dynamics¶
apply_impulse((x, y)): Instantaneous momentum change scaled by mass.apply_force((x, y)): Continuous force application over current step.- Configurable properties:
mass: float,gravity_scale: float,lock_rotation: bool.
Area2D Triggers¶
class Coin(pite.Area2D):
def _ready(self):
self.body_entered.connect(self.on_body)
def on_body(self, body_id: str):
print(f"Collected by {body_id}")
Built-in signals:
body_entered(body_id: str)body_exited(body_id: str)area_entered(area_id: str)area_exited(area_id: str)
Spatial Queries (Raycasts)¶
hit = pite.raycast((x, y), (target_x, target_y), mask=1)
if hit is not None:
print(f"Hit {hit['body']} at {hit['point']} with normal {hit['normal']}")
Returns None if no solid obstacle intersects, or a dict containing:
point:(float, float)world coordinate of intersection.normal:(float, float)surface normal at intersection point.distance:floatdistance from ray origin.collider:strnode ID ofCollisionShape2D.body:strnode ID of parent physics body.
Error Handling and Live Reload¶
- Fault Tolerance: When an unhandled exception occurs in a script callback, Pite logs the complete traceback including node path, source file, and line number. The node preserves its last valid state and the game session continues running without crashing. Repeated failures on the same callback are throttled to keep the console clean and readable.
- Hot Reloading: Modifying and saving a
.pyfile triggers an immediate hot reload: the module re-executes, class definitions rebind to existing node instances in memory, transforms and physics velocities are preserved, and_ready()is not re-run. If a reloaded script contains syntax or import errors, the previous working version continues executing safely.
Module Imports and Dependency Vendoring¶
- Standard Library & Engine API:
import piteand all CPython 3.12 standard library modules are always available. Each script's parent directory is prepended tosys.path, allowing sibling modules (e.g.import utils) to resolve directly. - Third-Party Dependencies: Third-party packages must be vendored into the project's
vendor/directory usingpite vendor add package==version. The CLI pins the package and records its SHA-256 hash in[vendor]. Both pure-Python wheels and source trees are supported, andvendor/is automatically added tosys.pathin both editor and exported builds. - Export Isolation: Standalone game exports deliberately exclude host
site-packagesto guarantee portable, self-contained builds. Any dependency outside the standard library,pite, project scripts, andvendor/will not be bundled. Usepite checkto audit imports and verify that all external dependencies are properly vendored. - Auxiliary Code Files: Sibling Python modules located outside standard script directories should be declared under
[export] includeinpite.tomlto ensure they are packaged during export.