Skip to content

Scenes and Game Structure

magmacrunchmedia edited this page Aug 24, 2026 · 1 revision

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.

A scene is anything

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.

Wiring

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.

Modality

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 types

Both 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.

Deferred operations

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-update without 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.

Scenes own their widgets

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 True

Entity groups

The 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 despawn

Membership 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.

Migrating from flags

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.

Clone this wiki locally