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):
x, y = self.position
self.position = (x + self.speed * delta, y)- 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 plain(x, y)float tuple, synced both ways with the scene tree each frame.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 two-way sync — set it and the screen updates).ButtonextendsNode2Dwithtextplus asizeprop ([w, h], default[120, 40]).- The
get_nodeproxy also exposestext, so any node can drive a label:self.get_node("../HpLabel").text = f"HP: {self.hp}". - 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)takes a bound method of a live node; when that node drops, its connections go with it — emitting afterwards simply skips it, no crash.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("ArrowRight"):
x += self.speed * delta
if pite.pressed("Space"):
x += 10.0held(key)is true while down (continuous movement);pressed(key)fires on the single frame it goes down (single steps);released(key)on the frame it comes up.mouse()returns the cursor(x, y)in pixels.- Key names:
A–Z,0–9,ArrowLeft/Right/Up/Down,Space,Enter,Escape,Tab,Backspace,Shift,Control,Alt, plusMouseLeft/MouseRight/MouseMiddle. Input is pumped on the main thread and visible inside_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; only 16-bit.wavis 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.
- 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.