-
Notifications
You must be signed in to change notification settings - Fork 0
Rendering and Camera
from texastoast import CanvasRenderer
renderer = CanvasRenderer(game.canvas, 640, 480)
renderer.width, renderer.height # the viewport, readable since 0.4.0The width and height are the viewport, and should match the canvas. The
renderer owns a Camera sized to match.
Pass the renderer to the UI widgets rather than repeating the size — they read it back from these properties. See UI Components.
renderer.clear() # call first, every frame
renderer.draw_tilemap(tilemap, tile_colors)
renderer.draw_rect(x, y, w, h, color, tag="")
renderer.draw_image(x, y, photo_image, anchor="nw", tag="")
renderer.draw_text(x, y, text, fill="#fff", font=("Courier", 10))
renderer.draw_hud_text(x, y, text, fill="#fff", font=("Courier", 10))Everything is world space and camera-relative except draw_hud_text, which
is screen space and ignores the camera. That is the whole difference between the
two text calls.
draw_text and draw_hud_text pass extra keyword arguments straight to
tkinter's create_text, so anchor, width, justify and friends all work.
renderer.draw_tilemap(tilemap, {0: "#7cb342", 1: "#5d4037"})
renderer.draw_tilemap(tilemap, colors, skip_tiles={0})A tile is drawn when its id has an entry in tile_colors. Ids with no entry are
left transparent — that is how you make a tile invisible, and it is why there is
no special-cased "empty" id. skip_tiles skips ids that do have a color, for
when you want to hide a layer temporarily.
Only tiles inside the camera's view are drawn, so map size costs you nothing at render time.
In 0.1.x, id 0 was skipped unconditionally regardless of the color map. See Migrating to 0.2.0.
The canvas paints in call order, so draw back to front:
def render():
renderer.clear()
renderer.draw_tilemap(tilemap, TILE_COLORS) # ground
renderer.draw_rect(...) # entities
hud.render() # overlays, last
dialogue.render()
menu.render()
renderer.present() # end every frame with thisclear() deletes everything, including dialogue and menu graphics — which is
why the widgets are frame-driven and you call their render() after it. See
UI Components.
Added in 0.4.0. CanvasRenderer satisfies two typing.Protocols that
describe what the engine asks of a drawing backend:
from texastoast import Renderer, UISurface
isinstance(renderer, Renderer) # True — world-space drawing
isinstance(renderer, UISurface) # True — screen-space widget drawingThey are structural, so a backend implements them by having the methods; there
is nothing to subclass. Renderer covers camera, width, height,
clear(), present(), and the draw_* calls. UISurface covers
begin_group(), clear_group(), ui_rect() and ui_text() — the screen-space
half the UI widgets are written against.
tkinter is the only backend today. The protocols exist so that the eventual SDL/framebuffer backend — the one a console-class device would need — has a contract to implement rather than a rewrite to negotiate.
renderer.present() # no-op on tkinterThe tkinter Canvas is retained-mode, so there is nothing to flip and present()
does nothing. Call it at the end of every render function anyway. A buffered
backend swaps its off-screen buffer there, and that is the one call which cannot
be retrofitted later without editing every game ever written against the engine.
renderer.begin_group("hud") # discard what this group drew last frame
renderer.ui_rect(8, 8, 120, 10, fill="#333333", group="hud")
renderer.ui_text(8, 8, "HP", fill="#cccccc", group="hud")
renderer.clear_group("hud") # remove it entirelyOn tkinter a group is a canvas tag. On an immediate-mode backend begin_group
would be a no-op, because clear() already wiped the frame. Widgets are written
against the group model so the same widget code works either way.
camera = renderer.camera
camera.follow(target_x, target_y, map_width=800, map_height=600, dt=dt)
camera.set_position(x, y)
camera.world_to_screen(wx, wy) # -> (sx, sy)
camera.screen_to_world(sx, sy) # -> (wx, wy)
camera.is_visible(x, y, w, h) # -> boolfollow centers on the target and eases toward it. Pass map_width and
map_height to clamp the view inside the map so it never shows past the edge;
omit them (or pass 0) for an unclamped camera.
camera.x/camera.y are the top-left of the view in world coordinates, and are
plain attributes you can read or set.
Pass the frame's dt to follow. As of 0.5.0 omitting it raises
TypeError (it warned throughout 0.4.x).
camera.follow(player.center_x, player.center_y, dt=dt) # correct
camera.follow(player.center_x, player.center_y) # TypeErrordt stays last in the signature, so correct 0.4.x call sites — keyword
dt=dt and full-positional five-argument calls — work unchanged.
The reason is the same one behind Entity.move() taking dt: without it the
easing is applied once per frame, so the camera converges twice as fast at
60 fps as at 30, and the game feels different on different machines. With dt,
smoothing is treated as a per-frame factor at 30 fps, converted to a time
constant and integrated over the real elapsed time — so the camera lags by the
same distance at any frame rate.
Camera.smoothing defaults to 0.1: ease 10% of the remaining distance per
frame at 30 fps.
Camera(640, 480, smoothing=1.0) # instant snap
Camera(640, 480, smoothing=0.05) # slow, floaty30 fps is the reference rate simply because that is where the old per-frame
behaviour and the dt behaviour agree exactly, which made the 0.3.0 change a
no-op for anyone already running at the default frame rate.
from texastoast import SpriteSheet
sheet = SpriteSheet("characters.png", frame_width=16, frame_height=16)
frame = sheet.get_frame(game.root, col=0, row=0)
renderer.draw_image(player.x, player.y, frame)Frames are cropped on demand and cached, so calling get_frame every frame is
fine. An out-of-range col/row raises ValueError rather than returning a
blank image.
sheet.cols, sheet.rows, sheet.total_frames
sheet.frame_width, sheet.frame_heightWith Pillow installed
(pip install "texastoast[sprites]") cropping is pixel-accurate and handles
transparency. Without it, tkinter's PhotoImage.copy() is used, which is more
limited — notably it only reads GIF and PNG and has no alpha compositing.
tkinter drops a PhotoImage as soon as Python stops referencing it, and the
image silently disappears from the canvas. SpriteSheet holds its own frames,
but if you build images yourself, keep them:
frames = [make_frame(i) for i in range(4)] # module-level, not a localThe same applies to load_image:
from texastoast.render import load_image
background = load_image(game.root, "bg.png") # must stay referencedtexastoast · PyPI · Apache-2.0