This example shows a complete terminal game built with bevy_ratatui. It is intentionally larger
than the small examples in this repository so the full bevy_ratatui application flow remains
visible: terminal events enter as Bevy messages, systems update resources in an explicit order, and
a final system projects those resources into a Ratatui frame.
Run it with:
cargo run --example snakeThe reusable part of this example is the path through the Bevy schedules:
RatatuiPluginsinitializes the terminal context and emits terminal events such asKeyMessage.- A
PreUpdatesystem translates those terminal-specific messages into the application's ownGameCommandmessages. - Chained
Updatesystems synchronize terminal-dependent resources, read commands, and advance application state in a deliberate order. - A
PostUpdatesystem borrowsRatatuiContextand callsdraw, so rendering sees the state produced by the current update without owning or mutating the game rules.
This input-resource-render flow applies to dashboards, editors, and other terminal applications as
well as games. The turn queue, crash grace, and partial-block animation are Snake policies rather
than requirements imposed by bevy_ratatui.
The first reading path follows the reusable bevy_ratatui integration:
main.rswires the Bevy app and acts as the Rustdoc table of contents.controls.rstranslatesKeyMessagevalues intoGameCommandmessages. This keeps terminal key codes out of the game rules.game_loop.rsowns the Bevy runtime glue andGameTiming: applying commands, advancing logical movement, and managing the wall-clock collision grace period.terminal.rslays out and renders the current game state with Ratatui.
The game-rule modules can be read without following terminal rendering details:
game.rsowns state transitions for movement, scoring, collision, restart, and resize.snake.rsowns logical segments, the latest movement snapshot, direction, and queued turns.geometry.rsowns board coordinates and movement directions shared by the rules, controls, and renderer.
The remaining modules support terminal sizing and the optional smooth-rendering details:
playfield.rsderives the logical board and availability state from the terminal size so update and rendering follow the same environmental policy.hud.rsrenders score, controls, overlays, and the small-terminal fallback.terminal_cells.rsowns the wide-cell projection and the foreground, background, and Unicode glyph rules for individual terminal cells.snake_rendering.rsrenders stable body cells and smoothly interpolated head and tail cells, including fractional coverage calculations.
The later modules are not prerequisites for understanding how RatatuiPlugins, messages,
resources, schedules, and RatatuiContext::draw fit together.
The logical board fills the terminal area left after the HUD and controls. A terminal cell is usually taller than it is wide, so the renderer draws each logical board cell as two side-by-side terminal cells. This makes vertical and horizontal movement feel closer in physical distance without making the arena so dense that play becomes empty and hard to read.
The rules still move on whole board cells, but rendering draws the head and tail between the previous and current cell positions using Unicode eighth-block glyphs. The middle body stays on the stable logical path. That split keeps motion responsive without making turns flicker as neighboring body segments round through a corner at different times. The moving head leaves body-colored underlay in the old head cell. The moving tail reveals board background in the old tail cell while leaving body-colored underlay in the current tail cell so it stays connected to the rest of the snake. Collision, food placement, and scoring never depend on fractional coordinates. The fruit uses a double-width emoji in the same two-column cell shape as the snake.
The example restarts the current round after a terminal resize changes the playable board. Snake segments and food are stored as board coordinates, so resizing in place would need a policy for scaling or clipping an active snake. Restarting keeps the example predictable and preserves the high score for the session. If the terminal becomes too small to fit the minimum board, the game loop suspends without changing the player's pause state. Returning to the same valid dimensions continues the current round instead of allowing the snake to move invisibly behind the size warning.
The controls use a small turn queue instead of a single pending direction. If the snake is moving
right and the player presses Up, Left before the next movement tick, the game applies Up on the
next tick and Left on the tick after that. Reversal checks compare a new input to the last queued
direction, so Right, Up, Left is valid but Right, Up, Down is not.
Direction keys accept terminal key-repeat events, but pause and restart only accept the initial
press. Repeating a movement direction is harmless and responsive; repeating a toggle or reset would
make one physical key hold produce several state changes.
The game also gives the player a short crash-grace window. Taneli Armanto
described the original idea as a tiny delay that made fast edge turns less
frustrating. In another interview, he described that delay as a few extra
milliseconds right before a crash where the player could still turn and continue. That same
interview describes Snake speed as the delay between position steps, not as a fixed 60 FPS render
loop. This example models the crash delay as a short wall-clock timer in game_loop.rs, separate
from both the render frame and the normal movement tick. A late turn must actually move to safety;
turning into another collision ends the round instead of granting another grace window.
The repeating movement timer reports how many logical intervals elapsed during each render frame. The loop processes up to four of those steps, preserving normal game speed through short scheduler stalls while preventing a debugger pause or suspended terminal from producing an unbounded burst of moves. Pause and terminal-size suspension do not reset the timer, so the partial head and tail stay at their current visual positions until play resumes. Collision completes the previous interpolation before showing crash grace, avoiding a one-cell visual rewind when a failed step does not update the snake path.
game_loop.rs contains Bevy systems, but only the systems that drive the game loop; input
translation and rendering stay near their own concepts. Module docs explain ownership and the
overall model. Item docs keep policy, invariants, and edge-case rationale close to the constants,
resources, fields, and helpers that implement those decisions so readers can distinguish reusable
Bevy patterns from choices made specifically for Snake.
The example has unit tests for the framework-independent rules, key-repeat policy, timing boundaries, terminal-size suspension, interpolation underlays, and terminal-size policy:
cargo test --example snakeThe example also participates in workspace target checks:
cargo check --workspace --all-targets