- βΒ Previous
- Building a Terminal Renderer
Get Latest Code
π Beginning of Lesson 7 β Two Options
Each lesson starts with a new machine, you need to pull the latest code first, then either resume your own progress or start from the official snapshot.
Clone your fork (replace <YOUR_GITHUB_USERNAME>):
git clone https://github.com/<YOUR_GITHUB_USERNAME>/snake-tutorial.git
cd ~/sandbox/snake-tutorial
π °οΈ Option A β Resume from Your Fork (Recommended)
Check out your saved progress to a branch:
git checkout -b lesson-07-work origin/lesson-06-work
β You're now ready to start Lesson 7 from your own progress.
π ±οΈ Option B β Start from Official Course Snapshot
Check out and push to your origin the starting point for Lesson 7 :
git remote add upstream https://github.com/GNAT-Academic-Program/snake-tutorial.git
git fetch upstream
git checkout -b lesson-07-work upstream/lesson-07-start
git push -u origin lesson-07-work
β You're now starting Lesson 7 from the clean official reference code.
Building Transform Components
Let's build proper game entity machinery to model our snake! We'll create 2D position types and a transform component to track object locations in the game world.
Creating Position and Transform Types
- Add position types to
noki.ads- we need to model a 2D point and a transform component:type Pt2_T is record X : Float := 0.0; Y : Float := 0.0; end record; type Transform_T is record Pt : Pt2_T := (0.0, 0.0); end record;
Why Float? While the terminal uses integer coordinates, we'll use floats for smooth movement calculations. We'll convert to integers when rendering.
What's a Transform? In game development, a transform component stores an entity's position (and often rotation and scale). Here we start simple with just position.
Creating the Snake Entity Type
- Define the Snake type - add this to the end of
snake.ads:type Snake_T is record Transform : Transform_T; Texture : Texture_T := [[Tongue_Pix, Head_Pix, Empty_Pix], [Empty_Pix, Torso_Pix, Empty_Pix], [Empty_Pix, Torso_Pix, Torso_Pix]]; end record;
Why combine them? This creates a proper game entity - the snake now has both visual representation (texture) and spatial data (transform). This is a fundamental game development pattern.
Updating the Player
- Replace the old player in
snake_game.adb- remove this:Player : Texture_T := [[Tongue_Pix, Head_Pix, Empty_Pix], [Empty_Pix, Torso_Pix, Empty_Pix], [Empty_Pix, Torso_Pix, Torso_Pix]]; - Add the new player - replace with:
Player : Snake_T;
Default initialization: Notice we don't provide values - Ada uses the default values from the type definition. The transform starts at (0.0, 0.0) and the texture uses our predefined snake graphic.
- Update the render call - change this:
To this:
Render (Player);Render (Player.Texture);
Record access syntax: The . operator accesses record fields. Player.Texture gets the texture component from our Snake_T record.
Build and test:
alr build
bin/snake_game
β Expected result: The game still works exactly the same, but now has a cleaner entity structure ready for position-based movement!
Implementing Position-Based Rendering
Now let's make the snake move! We'll update the renderer to draw textures at specific positions and animate the snake across the screen.
Staying in the Play State
- Keep the game loop running - edit
snake_game.adband remove this line:Game_State := Game_Over;
Why? We want to stay in the Play state indefinitely to see continuous animation. Use Ctrl-C, Esc, or Q to quit.
Adding Position to the Renderer
- Update the Render signature in
noki.ads:procedure Render (T : Texture_T; P : Pt2_T); - Update the implementation in
noki.adb- change the procedure declaration to match:procedure Render (T : Texture_T; P : Pt2_T) is
Parameter order: We pass the texture first, then the position. This reads naturally as "render this texture at this position".
Converting Float Position to Integer Coordinates
- Add coordinate conversion - in the declarative region of
Render(after theMove_Cursorfunction), add:Dx : Integer := Integer (P.X); Dy : Integer := Integer (P.Y);
Why convert? Our Pt2_T uses floats for smooth calculations, but terminal coordinates must be integers. The Integer() conversion truncates the decimal part.
- Apply the position offset - change this line:
To this:
Ada.Text_IO.Put (Move_Cursor (Y + 1, Positive'First));Ada.Text_IO.Put (Move_Cursor (Y + 1 + Dy, Positive'First + Dx));
Offset logic: Dx and Dy shift the texture from its base coordinates. Y + 1 + Dy means: base row + 1-based indexing + vertical offset.
Testing Position-Based Rendering
- Update the render call in
snake_game.adb:Render (Player.Texture, Player.Transform.Pt);
New signature: Now we're passing both the texture and its position from the transform component.
Animating the Snake
- Add diagonal movement - update the
Playcase insnake_game.adb:when Play => Player.Transform.Pt.X := @ + 1.0; Player.Transform.Pt.Y := @ + 1.0; Render (Player.Texture, Player.Transform.Pt);
The @ symbol: This is Ada's "target name" feature - it refers to the current value of the assignment target. @ + 1.0 means "take the current value and add 1.0 to it". It's shorthand for Player.Transform.Pt.X := Player.Transform.Pt.X + 1.0.
Movement pattern: Adding 1.0 to both X and Y each frame creates diagonal movement down and to the right.
Hiding the Terminal Cursor
- Add cursor hiding function - in
noki.ads, add this new ANSI sequence:function Hide_Cursor return String is (CSI & "?25l");
Why hide the cursor? Hiding the terminal cursor creates a cleaner visual experience.
ANSI sequence ?25l: This is the standard ANSI escape code to make the cursor invisible. We'll restore it later when the program exits.
- Hide cursor before game starts - in
snake_game.adb, just before entering the game loop, add:Ada.Text_IO.Put (Hide_Cursor);
Timing matters: We hide the cursor once at the start, not in every frame. This is more efficient and prevents flickering.
Build and run:
alr build
bin/snake_game
β Expected result: The snake moves diagonally across the screen with no visible cursor! The animation is clean and smooth, and you can quit with 'q' or Escape.
Improving Terminal Rendering
Let's optimize our terminal rendering to remove flickering and improve visual quality. We'll fix screen clearing, add proper IO flushing, and ensure graceful program termination.
Adding Text IO Support
- Import Ada.Text_IO - at the top of
snake_game.adb, add this with clause:with Ada.Text_IO;
Why needed? We're about to use Ada.Text_IO.Put and Ada.Text_IO.Flush directly for better terminal control. This import makes those procedures available.
Fixing Screen Clear
- Replace Log with direct output - in
snake_game.adb, at the beginning of the game loop, change this:To this:noki.Log (Clear_Screen);Ada.Text_IO.Put (Clear_Screen);
Why this matters: Log adds a newline after each call, which interferes with the ANSI escape sequence. Using Ada.Text_IO.Put directly emits a pure clear screen command without extra characters.
ANSI escape codes: Clear_Screen is an ANSI escape sequence that terminal emulators interpret as a command. Extra newlines can break these commands or cause visual artifacts.
Adding IO Flush
- Flush the output buffer - at the end of the game loop in
snake_game.adb, just before thedelay 0.1;line, add:Ada.Text_IO.Flush;
What is flushing? Output in Ada (and most languages) is buffered for performance. Flush forces all pending output to be written immediately to the terminal.
Why flush? Without flushing, rendered frames might stay in the buffer and appear out of sync. Flushing ensures smooth, consistent frame updates and eliminates visual lag.
Implementing Graceful Exit
- Remove the forced exit - edit
snake_game.adband remove these lines:andwith GNAT.OS_Lib;GNAT.OS_Lib.OS_Exit (0);
Why remove it? OS_Exit terminates immediately without cleanup, leaving the terminal in a bad state. We want graceful shutdown instead.
- Update the input task exit - edit
noki.adband modify the quit key handling:when 'q' | Character'Val (27) => Input_Cmd.Set (Quit); exit;
Character'Val (27): This is the Escape key's ASCII code. When either 'q' or Escape is pressed, we set the quit command and exit the task loop cleanly.
- Add cursor restoration - in
noki.ads, add the show cursor sequence:function Show_Cursor return String is (CSI & "?25h");
Symmetry: This is the opposite of Hide_Cursor. The ANSI sequence ?25h restores cursor visibility.
- Clean up on exit - in
snake_game.adb, just after the game loop exits, add:Ada.Text_IO.Put (Show_Cursor); Ada.Text_IO.Put (Clear_Screen); Ada.Text_IO.Flush;
Why these three calls?
Show_Cursorrestores the cursor so the terminal works normally after the gameClear_Screenremoves game artifacts from the terminalFlushensures these commands execute immediately before program termination
Build and run:
alr build
bin/snake_game
β Expected result: Smooth animation with no flickering! The snake moves cleanly across the screen, and when you quit (press 'q' or Escape), the terminal returns to its normal state with the cursor visible and screen cleared.
Save Your Progress
π― End of Lesson 7 β Save Your Progress
To keep your work safe and accessible, push it to your fork.
Make sure you're at the root of the repo:
cd ~/sandbox/snake-tutorial
Make sure you're on your work branch:
git status
- π¨ If you're NOT on branch
lesson-07-work, run:git checkout -b lesson-07-work
Stage and commit your progress:
git add .
git commit -m "Lesson 7 progress"
Push to your fork:
git push origin lesson-07-work
β Expected result: Your fork now has a lesson-7-work branch with your code so far.
Level up your Server Side game β Join 15,000 engineers who receive insightful learning materials straight to their inbox
- βΒ Previous
- Building a Terminal Renderer