-
Notifications
You must be signed in to change notification settings - Fork 0
Core Concepts
Every texastoast game is the same three pieces:
game = Game(title="My Game", width=400, height=300, fps=30)
def update(dt): # advance the world by dt seconds
...
def render(): # draw the current state
...
game.set_update(update)
game.set_render(render)
game.start() # blocks until the window closesupdate and render are separate on purpose: update owns state, render
owns pixels. Keeping the split means you can call update from a test without
a window.
The UI widgets rely on it. Since 0.3.0 DialogueBox advances from
dialogue.update(dt) and all three widgets draw from their own render(),
which you call after renderer.clear(). One consequence: UI animation is now
driven by the loop, so stopping the loop freezes the typewriter — before 0.3.0
the dialogue ran on its own timer and kept typing regardless. See
UI Components.
As of 0.5.0 the recommended shape for anything beyond a single screen is to
hand both callbacks to a scene stack —
game.set_update(stack.update); game.set_render(stack.render) — and let
scenes own their slice of update/render. Pausing, menus and dialogue then
stop being flags and become scenes. See
Scenes and Game Structure.
update receives dt — the seconds elapsed since the previous frame, as a
float. Roughly 0.033 at 30 fps.
Every rate in your game should be multiplied by dt. A speed is
pixels per second, not pixels per frame:
player.move(state.dx, state.dy, dt, tilemap) # Entity does it for you
timer += dt # and so should youWithout dt, a game runs at whatever speed the machine happens to manage, and
the same code behaves differently at 30 and 60 fps. This is exactly the bug that
0.2.0 fixed — see Migrating to 0.2.0.
GameLoop.MAX_DT is 0.1. If a frame takes longer than that — you dragged the
window, hit a breakpoint, the machine slept — dt is capped at 0.1 rather than
reporting the true gap.
The alternative is worse: an unclamped dt of 5 seconds moves a 100 px/s
entity 500 px in one step, straight past whatever was in its way. Clamping means
the world briefly runs in slow motion instead of exploding.
GameLoop schedules itself with tkinter's after(). It is not a busy loop and
not a thread — it cooperates with the tkinter event loop, which is what lets key
bindings and canvas updates work at all.
game.loop.fps # measured frames per second, updated once a second
game.loop.frame_count # frames since the last fps sampleThe scheduled delay accounts for the time update and render just spent, so a
30 fps game targets a 33 ms cadence rather than 33 ms plus however long your
frame took.
An exception raised inside update or render is logged and swallowed — one
bad frame does not kill the game. Configure the texastoast logger to see them:
import logging
logging.basicConfig(level=logging.DEBUG)Game handles the window's close button, so clicking X stops the loop cleanly
rather than leaving a callback pending. Register your own cleanup with
on_close:
keyboard = KeyboardInput(game.root)
game.on_close(keyboard.destroy)Callbacks run in registration order, and one raising does not stop the rest.
quit() is idempotent — calling it twice is safe.
Config is a plain dataclass. Game builds one from its keyword arguments, or
takes one you supply:
from texastoast import Config, Game
cfg = Config(title="My Game", width=640, height=480, fps=60, bg_color="#000")
game = Game(config=cfg)Fields: title, width, height, fps, tile_size, bg_color,
grid_color, debug. Only the first five affect the window today; the rest are
there for your own use.
Game normally creates and owns its Tk window. Hand it an existing root or
frame and it builds into that instead, leaves the main loop to you, and does not
destroy the widget on quit():
frame = tk.Frame(my_app)
game = Game(width=640, height=480, root=frame)
game.start() # starts the game loop only
my_app.mainloop() # you own thisThis is also what makes the engine testable without a visible window.
texastoast · PyPI · Apache-2.0