Gameplay scripts are Python classes inheriting the pite module, executed embedded in the engine process (CPython 3.12).
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- One file exports one class; attach with
path+classin the scene file.snake_caseeverywhere. _ready()runs once when the node attaches;_process(delta)runs every frame (deltain seconds, capped at 0.1). Missing methods are no-ops, never errors.positionis a mutableVec2proxy backed directly by the engine's node transform. You can mutate.xand.yin-place (self.position.x += 10), reassign with a tuple or vector (self.position = (100, 200)), or unpack as a tuple (x, y = self.position).Sprite2Daddstexture: str,Timeraddswait_time: floatand an engine-ownedtimeoutsignal: every elapsedwait_timeseconds the engine firestimeoutand restarts the clock (looping). Non-positivewait_timenever fires. Connect during_readylikeButton.pressed— no declaration needed,Labeladdstext: str(same direct update — set it and the screen updates).ButtonextendsNode2Dwithtextplus asizeprop ([w, h], default[120, 40]).- Declarative node descriptors: declare child or node references as class attributes:
Descriptors resolve once on first access and cache the proxy. A missing node raises
sprite = pite.child("Sprite2D") target = pite.node("../Target")
KeyError. - The
get_nodeproxy also exposestext, so any node can drive a label:self.get_node("../HpLabel").text = f"HP: {self.hp}". Repeatedget_nodecalls are automatically cached. - Talk across nodes with direct calls through
get_node, which returns a proxy to the live node:
self.get_node("../Player").take_damage(1)Paths resolve against node names: .. (parent), . (self), Child (named child), /root/… (absolute). A miss raises KeyError.
For the type checker get_node returns a plain Node: anything beyond the declared surface (custom signals, script methods, per-type members) is dynamically typed instead of warning, and a miss still raises at runtime.
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.get_node(".").pressed.connect(self.on_pressed)
def on_pressed(self):
self.get_node("../Player").hit.emit(1)if pite.held.right:
self.position.x += self.speed * delta
if pite.pressed.space:
self.position.x += 10.0pite.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. - 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.
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.
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.
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").
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 queries relative toup_direction(default(0.0, -1.0)).
class Ball(pite.RigidBody2D):
def _ready(self):
self.apply_impulse((0.0, -200.0))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.
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)
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.
- A script exception is reported with node path, file, and line; the node keeps its last good state and the game keeps running. Repeat offenders are parked after the first error (see the console).
- Saving a
.pyfile hot-reloads it: the module re-executes, the class rebinds to existing nodes,positionis preserved, and_readyis not called again. A failed reload keeps the old code running.
import piteplus the stdlib always work. Each script's own directory is prepended tosys.path, so sibling modules (import utils) import in dev.- Third-party packages live in project-root
vendor/(pite vendor add pkg==verpins them into[vendor]with hashes;list/verify/removemanage the lock). Trees and wheels both import: the runtime putsvendor/onsys.pathin dev and export alike, so resolution is identical everywhere. Zipped is the default; wheels with native modules unpack (they can't load from a zip). - Exports drop
site-packagesentirely: anything outside stdlib +pite+ siblings +vendor/dies in the export.pite checkwarns per offending import. - Sibling modules outside
vendor/still need[export] includeto be staged. Safe rule:pite+ stdlib + siblings + vendor.