-
Notifications
You must be signed in to change notification settings - Fork 0
Scenes and Game Structure
Added in 0.5.0.
Modality as a stack instead of a pile of flags. Before 0.5.0 every texastoast
game hand-rolled its modal state: a paused global, a showing_dialogue
global, an update() that early-returned while either was set, and a keypress
handler that dispatched down an if-chain. The scene stack subsumes all of it —
pushing a scene freezes the scenes below by construction, so the flags
simply stop existing.
There is no base class to subclass. A scene needs update(dt) and
render(); everything else is optional and detected by presence:
| Optional member | Meaning |
|---|---|
on_enter() / on_exit()
|
Pushed onto / removed from the stack |
on_pause() / on_resume()
|
Covered by / re-exposed from under a push |
handle_key(event) |
Receives key events via dispatch_key (top scene only) |
update_below = True |
The scene under this one keeps updating |
render_below = True |
The scene under this one keeps rendering |
Scene in texastoast.scene is a typing.Protocol for type checking; a
plain class or even a SimpleNamespace works at runtime.
The stack is a system you wire, not a framework that owns you:
from texastoast import SceneStack
stack = SceneStack()
stack.push(WorldScene())
game.set_update(stack.update)
game.set_render(stack.render)
game.bind_key("<Key>", stack.dispatch_key)
game.start()dispatch_key forwards the raw event to the top scene's handle_key, if it
has one. The stack never imports tkinter and never binds anything — you can
ignore dispatch_key entirely and keep a global handler if you prefer.
By default only the top scene updates. Push a pause scene and the world
freezes because its update is simply never called — no flag, no
early-return.
The *_below flags opt the scene underneath back in, and they live on the
overlay because translucency is a property of the overlay's design:
class PauseScene:
render_below = True # the frozen world stays visible underneath
class AmbientDialogue:
render_below = True
update_below = True # the world keeps animating while text typesBoth walks run bottom-to-top: render is painter's order, and update runs
the world before the overlay so the overlay reads a finished frame. Flags
chain — dialogue over pause over world renders all three if each overlay sets
render_below — and stop at the first scene that doesn't set them.
push, pop, replace, and clear never take effect immediately: they
queue and apply at the start of the next update(). One rule, two
consequences worth knowing:
- A scene can pop itself (or push over itself) mid-
updatewithout corrupting the frame. - An op issued from a key event — which tkinter delivers between frames —
applies at the next
update(), i.e. before that frame renders. Pressing Escape shows the pause menu the same frame.
Lifecycle hooks fire as the queue drains: push → old top on_pause, new
scene on_enter; pop → on_exit, exposed scene on_resume; replace →
on_exit/on_enter only (the scene below never became top); clear →
on_exit top-down.
The frame-driven UI contract composes unchanged: a pause scene owns its
Menu and calls menu.render() from its own render(); a dialogue scene
owns its DialogueBox and drives dialogue.update(dt) from its update().
Show on on_enter, hide on on_exit:
class PauseScene:
render_below = True
def __init__(self, world):
self._world = world
self._menu = Menu(renderer)
def on_enter(self):
self._menu.show(["Resume", "Restart", "Quit"],
on_select=self._on_select, on_cancel=stack.pop,
title="PAUSED")
def on_exit(self):
self._menu.hide()
def update(self, dt):
pass
def render(self):
self._menu.render()
def handle_key(self, event):
key = event.keysym
if key in ("Up", "w", "W"):
self._menu.move_up()
elif key in ("Down", "s", "S"):
self._menu.move_down()
elif key in ("z", "Z", "Return"):
self._menu.confirm()
elif key in ("x", "X", "Escape"):
self._menu.cancel()
return TrueThe other half of game structure: before 0.5.0 nothing iterated entities.
EntityGroup is that loop:
from texastoast import EntityGroup
entities = EntityGroup()
player = entities.add(Entity(x=60, y=60), "player") # add() returns the entity
entities.add(npc, "npc", "vendor") # tags live in the group
entities.update(dt) # from your scene's update
for e in entities.sorted_by_y(): # painter's order, by feet line
renderer.draw_image(e.x, e.y, sprite_for(e))
entities.by_tag("npc")
entities.select(lambda e: e.x > 80)Two removal paths, both safe mid-update (adds and removes defer until the pass ends — mutating a list while iterating it skips the neighbor, the classic first bug of every entity system):
self.alive = False # inside the entity's own update() — culled after
entities.remove(npc) # external despawnMembership is duck-typed: anything with update(dt) qualifies, so timers and
particle effects join without inheriting from Entity. The group never
draws — rendering stays yours.
The 0.4.0 game_template.py and the 0.5.0 rewrite are the before/after. The
mapping:
| Flag-era pattern | Scene-era pattern |
|---|---|
paused = True + early-return in update |
stack.push(PauseScene(...)) |
if dialogue.active: ... return key chain |
DialogueScene.handle_key, reached only while it is top |
showing_dialogue global |
the DialogueScene's existence on the stack |
| un-pause in a menu callback | stack.pop() |
| draw menu last in a shared render() |
render_below = True + the scene's own render |
See examples/game_template.py
for the complete reference wiring — world, pause, and dialogue as scenes,
zero module-level state flags.
texastoast · PyPI · Apache-2.0