Learn Zig Series (#162) - Sprite Rendering and Tile Maps
Published on HivePostify by @scipio · Mon Sep 07 2026
Learn Zig Series (#162) - Sprite Rendering and Tile Maps
What will I learn? - What a sprite actually is -- a small pre-drawn image you stamp onto the screen wholesale, in stead of rebuilding it from lines and polygons every single frame; - A Sprite type and a blit routine written from scratch, with edge clipping so a sprite half-way off the screen never reads or writes memory out of bounds; - Simple color-key transparency (skip fully transparent pixels) so non-rectangular shapes sit cleanly on any background -- and why true alpha blending is a separate, later problem; - Tile maps: describing a whole game world as a compact grid of small integer indices into a shared tile set, and why that representation is so cheap in memory; - Drawing a tile map with a scrolling camera, and culling so you only ever touch the handful of tiles actually on screen, not the thousands that are not; - How Zig's []const slices, a sentinel index and checked indexing make the whole thing genuinely hard to misuse; - Testing strategies that pin the real bugs (a transparent pixel that should not have drawn, an off-screen blit that corrupts memory, an optimized path that disagrees with the naive one), plus how C, Rust and Go write the same inner loop.
Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Zig 0.14+ distribution (download from ziglang.org) -- the code here is written and tested against Zig 0.16; - The Framebuffer, Rgba and clipping setPixel/getPixel from episode 156, and the DoubleBuffer from episode 161 -- today we stamp sprites into a back buffer and present it; - Comfort with slices (episode 5), allocators and errdefer (episodes 7 and 26), and the testing habits from episode 12; - The ambition to learn Zig programming.
Difficulty - Advanced
Curriculum (of the Learn Zig Series): - [Zig Programming Tutorial - ep001 - Intro](https://hive.blog/programming/@scipio/zig-programming-tutoroial-ep001-intro) - [Learn Zig Series (#2) - Hello Zig, Variables and Types](https://hive.blog/hive-196387/@scipio/learn-zig-series-2-hello-zig-variables-and-types) - [Learn Zig Series (#3) - Functions and Control Flow](https://hive.blog/hive-196387/@scipio/learn-zig-series-3-functions-and-control-flow) - [Learn Zig Series (#4) - Error Handling (Zig's Best Feature)](https://hive.blog/hive-196387/@scipio/learn-zig-series-4-error-handling-zigs-best-feature) - [Learn Zig Series (#5) - Arrays, Slices, and Strings](https://hive.blog/hive-196387/@scipio/learn-zig-series-5-arrays-slices-and-strings) - [Learn Zig Series (#6) - Structs, Enums, and Tagged Unions](https://hive.blog/hive-196387/@scipio/learn-zig-series-6-structs-enums-and-tagged-unions) - [Learn Zig Series (#7) - Memory Management and Allocators](https://hive.blog/hive-196387/@scipio/learn-zig-series-7-memory-management-and-allocators) - [Learn Zig Series (#8) - Pointers and Memory Layout](https://hive.blog/hive-196387/@scipio/learn-zig-series-8-pointers-and-memory-layout) - [Learn Zig Series (#9) - Comptime (Zig's Superpower)](https://hive.blog/hive-196387/@scipio/learn-zig-series-9-comptime-zigs-superpower) - [Learn Zig Series (#10) - Project Structure, Modules, and File I/O](https://hive.blog/hive-196387/@scipio/learn-zig-series-10-project-structure-modules-and-file-io) - [Learn Zig Series (#11) - Mini Project: Building a Step Sequencer](https://hive.blog/hive-196387/@scipio/learn-zig-series-11-mini-project-building-a-step-sequencer) - [Learn Zig Series (#12) - Testing and Test-Driven Development](https://hive.blog/hive-196387/@scipio/learn-zig-series-12-testing-and-test-driven-development) - [Learn Zig Series (#13) - Interfaces via Type Erasure](https://hive.blog/hive-196387/@scipio/learn-zig-series-13-interfaces-via-type-erasure) - [Learn Zig Series (#14) - Generics with Comptime Parameters](https://hive.blog/hive-196387/@scipio/learn-zig-series-14-generics-with-comptime-parameters) - [Learn Zig Series (#15) - The Build System (build.zig)](https://hive.blog/hive-196387/@scipio/learn-zig-series-15-the-build-system-buildzig) - [Learn Zig Series (#16) - Sentinel-Terminated Types and C Strings](https://hive.blog/hive-196387/@scipio/learn-zig-series-16-sentinel-terminated-types-and-c-strings) - [Learn Zig Series (#17) - Packed Structs and Bit Manipulation](https://hive.blog/hive-196387/@scipio/learn-zig-series-17-packed-structs-and-bit-manipulation) - [Learn Zig Series (#18b) - Addendum: Async Returns in Zig 0.16](https://hive.blog/hive-196387/@scipio/learn-zig-series-18b-addendum-async-returns-in-zig-016) - [Learn Zig Series (#19) - SIMD with @Vector](https://hive.blog/hive-196387/@scipio/learn-zig-series-19-simd-with-vector) - [Learn Zig Series (#20) - Working with JSON](https://hive.blog/hive-196387/@scipio/learn-zig-series-20-working-with-json) - [Learn Zig Series (#21) - Networking and TCP Sockets](https://hive.blog/hive-196387/@scipio/learn-zig-series-21-networking-and-tcp-sockets) - [Learn Zig Series (#22) - Hash Maps and Data Structures](https://hive.blog/hive-196387/@scipio/learn-zig-series-22-hash-maps-and-data-structures) - [Learn Zig Series (#23) - Iterators and Lazy Evaluation](https://hive.blog/hive-196387/@scipio/learn-zig-series-23-iterators-and-lazy-evaluation) - [Learn Zig Series (#24) - Logging, Formatting, and Debug Output](https://hive.blog/hive-196387/@scipio/learn-zig-series-24-logging-formatting-and-debug-output) - [Learn Zig Series (#25) - Mini Project: HTTP Status Checker](https://hive.blog/hive-196387/@scipio/learn-zig-series-25-mini-project-http-status-checker) - [Learn Zig Series (#26) - Writing a Custom Allocator](https://hive.blog/hive-196387/@scipio/learn-zig-series-26-writing-a-custom-allocator) - [Learn Zig Series (#27) - C Interop: Calling C from Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-27-c-interop-calling-c-from-zig) - [Learn Zig Series (#28) - C Interop: Exposing Zig to C](https://hive.blog/hive-196387/@scipio/learn-zig-series-28-c-interop-exposing-zig-to-c) - [Learn Zig Series (#29) - Inline Assembly and Low-Level Control](https://hive.blog/hive-196387/@scipio/learn-zig-series-29-inline-assembly-and-low-level-control) - [Learn Zig Series (#30) - Thread Safety and Atomics](https://hive.blog/hive-196387/@scipio/learn-zig-series-30-thread-safety-and-atomics) - [Learn Zig Series (#31) - Memory-Mapped I/O and Files](https://hive.blog/hive-196387/@scipio/learn-zig-series-31-memory-mapped-io-and-files) - [Learn Zig Series (#32) - Compile-Time Reflection with @typeInfo](https://hive.blog/hive-196387/@scipio/learn-zig-series-32-compile-time-reflection-with-typeinfo) - [Learn Zig Series (#33) - Building a State Machine with Tagged Unions](https://hive.blog/hive-196387/@scipio/learn-zig-series-33-building-a-state-machine-with-tagged-unions) - [Learn Zig Series (#34) - Performance Profiling and Optimization](https://hive.blog/hive-196387/@scipio/learn-zig-series-34-performance-profiling-and-optimization) - [Learn Zig Series (#35) - Cross-Compilation and Target Triples](https://hive.blog/hive-196387/@scipio/learn-zig-series-35-cross-compilation-and-target-triples) - [Learn Zig Series (#36) - Mini Project: CLI Task Runner](https://hive.blog/hive-196387/@scipio/learn-zig-series-36-mini-project-cli-task-runner) - [Learn Zig Series (#37) - Markdown to HTML: Tokenizer and Lexer](https://hive.blog/hive-196387/@scipio/learn-zig-series-37-markdown-to-html-tokenizer-and-lexer) - [Learn Zig Series (#38) - Markdown to HTML: Parser and AST](https://hive.blog/hive-196387/@scipio/learn-zig-series-38-markdown-to-html-parser-and-ast) - [Learn Zig Series (#39) - Markdown to HTML: Renderer and CLI](https://hive.blog/hive-196387/@scipio/learn-zig-series-39-markdown-to-html-renderer-and-cli) - [Learn Zig Series (#40) - Key-Value Store: In-Memory Store](https://hive.blog/hive-196387/@scipio/learn-zig-series-40-key-value-store-in-memory-store) - [Learn Zig Series (#41) - Key-Value Store: Write-Ahead Log](https://hive.blog/hive-196387/@scipio/learn-zig-series-41-key-value-store-write-ahead-log) - [Learn Zig Series (#42) - Key-Value Store: TCP Server](https://hive.blog/hive-196387/@scipio/learn-zig-series-42-key-value-store-tcp-server) - [Learn Zig Series (#43) - Key-Value Store: Client Library and Benchmarks](https://hive.blog/hive-196387/@scipio/learn-zig-series-43-key-value-store-client-library-and-benchmarks) - [Learn Zig Series (#44) - Image Tool: Reading and Writing PPM/BMP](https://hive.blog/hive-196387/@scipio/learn-zig-series-44-image-tool-reading-and-writing-ppmbmp) - [Learn Zig Series (#45) - Image Tool: Pixel Operations](https://hive.blog/hive-196387/@scipio/learn-zig-series-45-image-tool-pixel-operations) - [Learn Zig Series (#46) - Image Tool: CLI Pipeline](https://hive.blog/hive-196387/@scipio/learn-zig-series-46-image-tool-cli-pipeline) - [Learn Zig Series (#47) - Build a Shell: Parsing Commands](https://hive.blog/hive-196387/@scipio/learn-zig-series-47-build-a-shell-parsing-commands) - [Learn Zig Series (#48) - Build a Shell: Process Spawning](https://hive.blog/hive-196387/@scipio/learn-zig-series-48-build-a-shell-process-spawning) - [Learn Zig Series (#49) - Build a Shell: Built-in Commands](https://hive.blog/hive-196387/@scipio/learn-zig-series-49-build-a-shell-built-in-commands) - [Learn Zig Series (#50) - Build a Shell: Job Control and Signals](https://hive.blog/hive-196387/@scipio/learn-zig-series-50-build-a-shell-job-control-and-signals) - [Learn Zig Series (#51) - HTTP Server: Accept Loop and Parsing](https://hive.blog/hive-196387/@scipio/learn-zig-series-51-http-server-accept-loop-and-parsing) - [Learn Zig Series (#52) - HTTP Server: Router and Responses](https://hive.blog/hive-196387/@scipio/learn-zig-series-52-http-server-router-and-responses) - [Learn Zig Series (#53) - HTTP Server: Static Files and MIME](https://hive.blog/hive-196387/@scipio/learn-zig-series-53-http-server-static-files-and-mime) - [Learn Zig Series (#54) - HTTP Server: Middleware and Logging](https://hive.blog/hive-196387/@scipio/learn-zig-series-54-http-server-middleware-and-logging) - [Learn Zig Series (#55) - ECS Game Engine: Architecture](https://hive.blog/hive-196387/@scipio/learn-zig-series-55-ecs-game-engine-architecture) - [Learn Zig Series (#56) - ECS Game Engine: Component Storage](https://hive.blog/hive-196387/@scipio/learn-zig-series-56-ecs-game-engine-component-storage) - [Learn Zig Series (#57) - ECS Game Engine: Systems and Queries](https://hive.blog/hive-196387/@scipio/learn-zig-series-57-ecs-game-engine-systems-and-queries) - [Learn Zig Series (#58) - ECS Game Engine: Terminal Rendering](https://hive.blog/hive-196387/@scipio/learn-zig-series-58-ecs-game-engine-terminal-rendering) - [Learn Zig Series (#59) - Assembler: Instruction Encoding](https://hive.blog/hive-196387/@scipio/learn-zig-series-59-assembler-instruction-encoding) - [Learn Zig Series (#60) - Assembler: Two-Pass Assembly](https://hive.blog/hive-196387/@scipio/learn-zig-series-60-assembler-two-pass-assembly) - [Learn Zig Series (#61) - Assembler: Disassembler and Binary Inspector](https://hive.blog/hive-196387/@scipio/learn-zig-series-61-assembler-disassembler-and-binary-inspector) - [Learn Zig Series (#62) - File Systems: Reading Directories and Metadata](https://hive.blog/hive-196387/@scipio/learn-zig-series-62-file-systems-reading-directories-and-metadata) - [Learn Zig Series (#63) - File Watching: Detecting Changes](https://hive.blog/hive-196387/@scipio/learn-zig-series-63-file-watching-detecting-changes) - [Learn Zig Series (#64) - Process Management: Fork, Exec, Wait](https://hive.blog/hive-196387/@scipio/learn-zig-series-64-process-management-fork-exec-wait) - [Learn Zig Series (#65) - Pipes and Inter-Process Communication](https://hive.blog/hive-196387/@scipio/learn-zig-series-65-pipes-and-inter-process-communication) - [Learn Zig Series (#66) - Shared Memory and Semaphores](https://hive.blog/hive-196387/@scipio/learn-zig-series-66-shared-memory-and-semaphores) - [Learn Zig Series (#67) - Signal Handling Deep Dive](https://hive.blog/hive-196387/@scipio/learn-zig-series-67-signal-handling-deep-dive) - [Learn Zig Series (#68) - Unix Domain Sockets](https://hive.blog/hive-196387/@scipio/learn-zig-series-68-unix-domain-sockets) - [Learn Zig Series (#69) - Daemonization: Background Services](https://hive.blog/hive-196387/@scipio/learn-zig-series-69-daemonization-background-services) - [Learn Zig Series (#70) - Timers and Scheduling](https://hive.blog/hive-196387/@scipio/learn-zig-series-70-timers-and-scheduling) - [Learn Zig Series (#71) - Resource Limits and Capabilities](https://hive.blog/hive-196387/@scipio/learn-zig-series-71-resource-limits-and-capabilities) - [Learn Zig Series (#72) - System Call Wrappers](https://hive.blog/hive-196387/@scipio/learn-zig-series-72-system-call-wrappers) - [Learn Zig Series (#73) - seccomp and Sandboxing](https://hive.blog/hive-196387/@scipio/learn-zig-series-73-seccomp-and-sandboxing) - [Learn Zig Series (#74) - ptrace: Process Tracing](https://hive.blog/hive-196387/@scipio/learn-zig-series-74-ptrace-process-tracing) - [Learn Zig Series (#75) - Reading Kernel State from /proc and /sys](https://hive.blog/hive-196387/@scipio/learn-zig-series-75-reading-kernel-state-from-proc-and-sys) - [Learn Zig Series (#76) - Mini Project: Process Monitor](https://hive.blog/hive-196387/@scipio/learn-zig-series-76-mini-project-process-monitor) - [Learn Zig Series (#77) - Mini Project: File Sync Tool - Part 1](https://hive.blog/hive-196387/@scipio/learn-zig-series-77-mini-project-file-sync-tool-part-1) - [Learn Zig Series (#78) - Mini Project: File Sync Tool - Part 2: Delta Transfer](https://hive.blog/hive-196387/@scipio/learn-zig-series-78-mini-project-file-sync-tool-part-2-delta-transfer) - [Learn Zig Series (#79) - Mini Project: File Sync Tool - Part 3: Network Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-79-mini-project-file-sync-tool-part-3-network-protocol) - [Learn Zig Series (#80) - Mini Project: File Sync Tool - Part 4: Polish](https://hive.blog/hive-196387/@scipio/learn-zig-series-80-mini-project-file-sync-tool-part-4-polish) - [Learn Zig Series (#81) - UDP Sockets and Datagrams](https://hive.blog/hive-196387/@scipio/learn-zig-series-81-udp-sockets-and-datagrams) - [Learn Zig Series (#82) - DNS Resolver from Scratch](https://hive.blog/hive-196387/@scipio/learn-zig-series-82-dns-resolver-from-scratch) - [Learn Zig Series (#83) - DNS Server Implementation](https://hive.blog/hive-196387/@scipio/learn-zig-series-83-dns-server-implementation) - [Learn Zig Series (#84) - HTTP/1.1 Deep Dive](https://hive.blog/hive-196387/@scipio/learn-zig-series-84-http11-deep-dive) - [Learn Zig Series (#85) - HTTP/2 Frames and Streams](https://hive.blog/hive-196387/@scipio/learn-zig-series-85-http2-frames-and-streams) - [Learn Zig Series (#86) - TLS via C Interop](https://hive.blog/hive-196387/@scipio/learn-zig-series-86-tls-via-c-interop) - [Learn Zig Series (#87) - WebSocket Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-87-websocket-protocol) - [Learn Zig Series (#88) - WebSocket Server](https://hive.blog/hive-196387/@scipio/learn-zig-series-88-websocket-server) - [Learn Zig Series (#89) - MQTT Messaging Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-89-mqtt-messaging-protocol) - [Learn Zig Series (#90) - Protocol Buffers Serialization](https://hive.blog/hive-196387/@scipio/learn-zig-series-90-protocol-buffers-serialization) - [Learn Zig Series (#91) - MessagePack Format](https://hive.blog/hive-196387/@scipio/learn-zig-series-91-messagepack-format) - [Learn Zig Series (#92) - gRPC Service in Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-92-grpc-service-in-zig) - [Learn Zig Series (#93) - SOCKS5 Proxy](https://hive.blog/hive-196387/@scipio/learn-zig-series-93-socks5-proxy) - [Learn Zig Series (#94) - NAT Traversal and Hole Punching](https://hive.blog/hive-196387/@scipio/learn-zig-series-94-nat-traversal-and-hole-punching) - [Learn Zig Series (#95) - Mini Project: Chat Server - Protocol Design](https://hive.blog/hive-196387/@scipio/learn-zig-series-95-mini-project-chat-server-protocol-design) - [Learn Zig Series (#96) - Mini Project: Chat Server - Server Core](https://hive.blog/hive-196387/@scipio/learn-zig-series-96-mini-project-chat-server-server-core) - [Learn Zig Series (#97) - Mini Project: Chat Server - Client TUI](https://hive.blog/hive-196387/@scipio/learn-zig-series-97-mini-project-chat-server-client-tui) - [Learn Zig Series (#98) - Mini Project: Chat Server - Rooms and History](https://hive.blog/hive-196387/@scipio/learn-zig-series-98-mini-project-chat-server-rooms-and-history) - [Learn Zig Series (#99) - Mini Project: DNS-over-HTTPS Proxy](https://hive.blog/hive-196387/@scipio/learn-zig-series-99-mini-project-dns-over-https-proxy) - [Learn Zig Series (#100) - Mini Project: Port Scanner](https://hive.blog/hive-196387/@scipio/learn-zig-series-100-mini-project-port-scanner) - [Learn Zig Series (#101) - Mini Project: HTTP Load Tester - Part 1](https://hive.blog/hive-196387/@scipio/learn-zig-series-101-mini-project-http-load-tester-part-1) - [Learn Zig Series (#102) - Mini Project: HTTP Load Tester - Part 2](https://hive.blog/hive-196387/@scipio/learn-zig-series-102-mini-project-http-load-tester-part-2) - [Learn Zig Series (#103) - Mini Project: Reverse Proxy - Routing](https://hive.blog/hive-196387/@scipio/learn-zig-series-103-mini-project-reverse-proxy-routing) - [Learn Zig Series (#104) - Mini Project: Reverse Proxy - Load Balancing](https://hive.blog/hive-196387/@scipio/learn-zig-series-104-mini-project-reverse-proxy-load-balancing) - [Learn Zig Series (#105) - Mini Project: Reverse Proxy - Health Checks](https://hive.blog/hive-196387/@scipio/learn-zig-series-105-mini-project-reverse-proxy-health-checks) - [Learn Zig Series (#106) - Linked Lists: Singly and Doubly](https://hive.blog/hive-196387/@scipio/learn-zig-series-106-linked-lists-singly-and-doubly) - [Learn Zig Series (#107) - Skip Lists](https://hive.blog/hive-196387/@scipio/learn-zig-series-107-skip-lists) - [Learn Zig Series (#108) - B-Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-108-b-trees) - [Learn Zig Series (#109) - Red-Black Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-109-red-black-trees) - [Learn Zig Series (#110) - Tries: Prefix Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-110-tries-prefix-trees) - [Learn Zig Series (#111) - Bloom Filters](https://hive.blog/hive-196387/@scipio/learn-zig-series-111-bloom-filters) - [Learn Zig Series (#112) - Cuckoo Filters](https://hive.blog/hive-196387/@scipio/learn-zig-series-112-cuckoo-filters) - [Learn Zig Series (#113) - Ring Buffers: Lock-Free](https://hive.blog/hive-196387/@scipio/learn-zig-series-113-ring-buffers-lock-free) - [Learn Zig Series (#114) - Memory Pools](https://hive.blog/hive-196387/@scipio/learn-zig-series-114-memory-pools) - [Learn Zig Series (#115) - Slab Allocators](https://hive.blog/hive-196387/@scipio/learn-zig-series-115-slab-allocators) - [Learn Zig Series (#116) - Sorting Algorithms in Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-116-sorting-algorithms-in-zig) - [Learn Zig Series (#117) - Binary Search Variations](https://hive.blog/hive-196387/@scipio/learn-zig-series-117-binary-search-variations) - [Learn Zig Series (#118) - Graph Representation](https://hive.blog/hive-196387/@scipio/learn-zig-series-118-graph-representation) - [Learn Zig Series (#119) - BFS and DFS](https://hive.blog/hive-196387/@scipio/learn-zig-series-119-bfs-and-dfs) - [Learn Zig Series (#120) - Dijkstra and A](https://hive.blog/hive-196387/@scipio/learn-zig-series-120-dijkstra-and-a) - [Learn Zig Series (#121) - Topological Sort](https://hive.blog/hive-196387/@scipio/learn-zig-series-121-topological-sort) - [Learn Zig Series (#122) - Union-Find](https://hive.blog/hive-196387/@scipio/learn-zig-series-122-union-find) - [Learn Zig Series (#123) - LRU Cache](https://hive.blog/hive-196387/@scipio/learn-zig-series-123-lru-cache) - [Learn Zig Series (#124) - Consistent Hashing](https://hive.blog/hive-196387/@scipio/learn-zig-series-124-consistent-hashing) - [Learn Zig Series (#125) - Mini Project: Search Engine - Inverted Index](https://hive.blog/hive-196387/@scipio/learn-zig-series-125-mini-project-search-engine-inverted-index) - [Learn Zig Series (#126) - Mini Project: Search Engine - TF-IDF](https://hive.blog/hive-196387/@scipio/learn-zig-series-126-mini-project-search-engine-tf-idf) - [Learn Zig Series (#127) - Mini Project: Search Engine - Query Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-127-mini-project-search-engine-query-parser) - [Learn Zig Series (#128) - Mini Project: Database Engine - Page Storage](https://hive.blog/hive-196387/@scipio/learn-zig-series-128-mini-project-database-engine-page-storage) - [Learn Zig Series (#129) - Mini Project: Database Engine - B-Tree Index](https://hive.blog/hive-196387/@scipio/learn-zig-series-129-mini-project-database-engine-b-tree-index) - [Learn Zig Series (#130) - Mini Project: Database Engine - SQL Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-130-mini-project-database-engine-sql-parser) - [Learn Zig Series (#131) - Lexing a Simple Language](https://hive.blog/hive-196387/@scipio/learn-zig-series-131-lexing-a-simple-language) - [Learn Zig Series (#132) - Recursive Descent Parsing](https://hive.blog/hive-196387/@scipio/learn-zig-series-132-recursive-descent-parsing) - [Learn Zig Series (#133) - AST Design and Traversal](https://hive.blog/hive-196387/@scipio/learn-zig-series-133-ast-design-and-traversal) - [Learn Zig Series (#134) - Type Checking](https://hive.blog/hive-196387/@scipio/learn-zig-series-134-type-checking) - [Learn Zig Series (#135) - Bytecode Design](https://hive.blog/hive-196387/@scipio/learn-zig-series-135-bytecode-design) - [Learn Zig Series (#136) - Stack-Based Virtual Machine](https://hive.blog/hive-196387/@scipio/learn-zig-series-136-stack-based-virtual-machine) - [Learn Zig Series (#137) - Closures and Upvalues](https://hive.blog/hive-196387/@scipio/learn-zig-series-137-closures-and-upvalues) - [Learn Zig Series (#138) - Garbage Collection: Mark and Sweep](https://hive.blog/hive-196387/@scipio/learn-zig-series-138-garbage-collection-mark-and-sweep) - [Learn Zig Series (#139) - Garbage Collection: Generational](https://hive.blog/hive-196387/@scipio/learn-zig-series-139-garbage-collection-generational) - [Learn Zig Series (#140) - JIT Compilation Basics](https://hive.blog/hive-196387/@scipio/learn-zig-series-140-jit-compilation-basics) - [Learn Zig Series (#141) - Regex: Thompson NFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-141-regex-thompson-nfa) - [Learn Zig Series (#142) - Regex: NFA to DFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-142-regex-nfa-to-dfa) - [Learn Zig Series (#143) - Regex: Matching Engine](https://hive.blog/hive-196387/@scipio/learn-zig-series-143-regex-matching-engine) - [Learn Zig Series (#144) - Code Generation: AST to Machine Code](https://hive.blog/hive-196387/@scipio/learn-zig-series-144-code-generation-ast-to-machine-code) - [Learn Zig Series (#145) - Register Allocation](https://hive.blog/hive-196387/@scipio/learn-zig-series-145-register-allocation) - [Learn Zig Series (#146) - Mini Project: Calculator - Lexer/Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-146-mini-project-calculator-lexerparser) - [Learn Zig Series (#147) - Mini Project: Calculator - Interpreter](https://hive.blog/hive-196387/@scipio/learn-zig-series-147-mini-project-calculator-interpreter) - [Learn Zig Series (#148) - Mini Project: Calculator - Bytecode Compiler](https://hive.blog/hive-196387/@scipio/learn-zig-series-148-mini-project-calculator-bytecode-compiler) - [Learn Zig Series (#149) - Mini Project: Calculator - VM with Debugger](https://hive.blog/hive-196387/@scipio/learn-zig-series-149-mini-project-calculator-vm-with-debugger) - [Learn Zig Series (#150) - Mini Project: Lisp - Reader](https://hive.blog/hive-196387/@scipio/learn-zig-series-150-mini-project-lisp-reader) - [Learn Zig Series (#151) - Mini Project: Lisp - Evaluator](https://hive.blog/hive-196387/@scipio/learn-zig-series-151-mini-project-lisp-evaluator) - [Learn Zig Series (#152) - Mini Project: Lisp - Special Forms and Macros](https://hive.blog/hive-196387/@scipio/learn-zig-series-152-mini-project-lisp-special-forms-and-macros) - [Learn Zig Series (#153) - Mini Project: Lisp - Standard Library](https://hive.blog/hive-196387/@scipio/learn-zig-series-153-mini-project-lisp-standard-library) - [Learn Zig Series (#154) - Mini Project: Regex Engine - NFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-154-mini-project-regex-engine-nfa) - [Learn Zig Series (#155) - Mini Project: Regex Engine - Matching](https://hive.blog/hive-196387/@scipio/learn-zig-series-155-mini-project-regex-engine-matching) - [Learn Zig Series (#156) - Framebuffer Basics](https://hive.blog/hive-196387/@scipio/learn-zig-series-156-framebuffer-basics) - [Learn Zig Series (#157) - Line Drawing: Bresenham](https://hive.blog/hive-196387/@scipio/learn-zig-series-157-line-drawing-bresenham) - [Learn Zig Series (#158) - Circle and Ellipse Rasterization](https://hive.blog/hive-196387/@scipio/learn-zig-series-158-circle-and-ellipse-rasterization) - [Learn Zig Series (#159) - Polygon Filling: Scanline](https://hive.blog/hive-196387/@scipio/learn-zig-series-159-polygon-filling-scanline) - [Learn Zig Series (#160) - 2D Transform Matrices](https://hive.blog/hive-196387/@scipio/learn-zig-series-160-2d-transform-matrices) - [Learn Zig Series (#161) - Double Buffering and Vsync](https://hive.blog/hive-196387/@scipio/learn-zig-series-161-double-buffering-and-vsync) - [Learn Zig Series (#162) - Sprite Rendering and Tile Maps](https://hive.blog/hive-196387/@scipio/learn-zig-series-162-sprite-rendering-and-tile-maps) (this post)
Learn Zig Series (#162) - Sprite Rendering and Tile Maps
Last episode closed on a promise. We can now animate without flicker and without tearing, paced to the screen's heartbeat -- but every frame is still assembled from scratch, one pixel and one polygon at a time. That is fine for a single spinning triangle and completely hopeless for an actual game, where a single screen might hold a player, a dozen enemies, a scattering of coins, and a whole landscape stretching behind all of them. Nobody rebuilds a brick wall out of individual line-draws sixty times a second. Instead you draw each thing once into a small image, and from then on you stamp that image wherever it needs to go. Those small pre-drawn images are sprites, and when you lay them out in a grid to build a world, that grid is a tile map. Today we build both, from scratch, on top of everything we have. Here we go!
Solutions to Episode 161 Exercises
Three exercises last time, all extending the DoubleBuffer and Pacer. Here are my solutions.
Exercise 1 -- a swapCopy alternative. The task was to present by copying the back buffer over the front, instead of flipping the index, and then to say why the flip wins. The copy version is a one-line @memcpy, and it produces exactly the same visible frame. The difference is entirely in cost: swap moves one bit, swapCopy moves the whole screen every present:
zig const std = @import("std");
pub const Rgba = packed struct { r: u8, g: u8, b: u8, a: u8 = 255 };
pub const Framebuffer = struct { pixels: []Rgba, width: usize, height: usize, pub fn clear(self: Framebuffer, color: Rgba) void { @memset(self.pixels, color); } pub fn getPixel(self: Framebuffer, x: i64, y: i64) ?Rgba { if (x = self.width or uy >= self.height) return null; return self.pixels[uy self.width + ux]; } };
pub const DoubleBuffer = struct { allocator: std.mem.Allocator, buffers: [2][]Rgba, width: usize, height: usize, backindex: u1,
pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) !DoubleBuffer { const a = try allocator.alloc(Rgba, width height); errdefer allocator.free(a); const b = try allocator.alloc(Rgba, width height); errdefer allocator.free(b); @memset(a, .{ .r = 0, .g = 0, .b = 0 }); @memset(b, .{ .r = 0, .g = 0, .b = 0 }); return .{ .allocator = allocator, .buffers = .{ a, b }, .width = width, .height = height, .backindex = 0 }; } pub fn deinit(self: DoubleBuffer) void { self.allocator.free(self.buffers[0]); self.allocator.free(self.buffers[1]); self. = undefined; } pub fn back(self: DoubleBuffer) Framebuffer { return .{ .pixels = self.buffers[self.backindex], .width = self.width, .height = self.height }; } pub fn front(self: DoubleBuffer) Framebuffer { return .{ .pixels = self.buffers[self.backindex ^ 1], .width = self.width, .height = self.height }; } // O(1) present: flip which buffer is front. pub fn swap(self: DoubleBuffer) void { self.backindex ^= 1; } // Same visible result, but it moves widthheight4 bytes every frame. pub fn swapCopy(self: DoubleBuffer) void { const src = self.buffers[self.backindex]; const dst = self.buffers[self.backindex ^ 1]; @memcpy(dst, src); } };
test "swapCopy shows the same frame as swap, at the cost of a full-screen memcpy" { var db = try DoubleBuffer.init(std.testing.allocator, 4, 4); defer db.deinit();
var b = db.back(); b.clear(.{ .r = 7, .g = 0, .b = 0 }); db.swapCopy(); // present by copying rather than flipping try std.testing.expectEqual(@as(u8, 7), db.front().getPixel(0, 0).?.r); }
For a 1080p screen that copy is about 8 MB moved per present, sixty times a second, doing nothing but shuffling finished pixels. The index flip does the same job for the price of a single XOR -- which is precisely why every real renderer flips.
Exercise 2 -- frame statistics. Add a running count of presents and a mean-frame-time helper. The count belongs inside markPresented (the one place a present is recorded), and the average is a pure division with a guard against dividing by zero on the very first frame:
zig const std = @import("std");
pub const Pacer = struct { framens: u64, nextdeadlinens: u64, armed: bool, presented: u64 = 0,
pub fn init(targetfps: u32) Pacer { std.debug.assert(targetfps > 0); return .{ .framens = std.time.nspers / targetfps, .nextdeadlinens = 0, .armed = false }; } pub fn markPresented(self: Pacer, nowns: u64) void { if (!self.armed) { self.nextdeadlinens = nowns + self.framens; self.armed = true; } else { self.nextdeadlinens += self.framens; } self.presented += 1; } pub fn averageFrameNs(self: Pacer, totalelapsedns: u64) u64 { if (self.presented == 0) return 0; return totalelapsedns / self.presented; } };
test "averageFrameNs divides elapsed time by the number of frames presented" { var pacer = Pacer.init(60); const framens = std.time.nspers / 60;
pacer.markPresented(0); pacer.markPresented(framens); pacer.markPresented(2 framens); pacer.markPresented(3 framens);
// four frames over four frame-intervals of runtime -> the mean is one frame time try std.testing.expectEqual(framens, pacer.averageFrameNs(4 framens)); }
Exercise 3 -- detect a dropped frame. A frame is dropped when rendering overran its interval, so at present time you are already past the deadline. A small wrapper reads that condition before advancing the schedule, then reports it. Note the guard on armed: the very first present has no prior deadline, so it can never be "late":
zig const std = @import("std");
pub const Pacer = struct { framens: u64, nextdeadlinens: u64, armed: bool,
pub fn init(targetfps: u32) Pacer { std.debug.assert(targetfps > 0); return .{ .framens = std.time.nspers / targetfps, .nextdeadlinens = 0, .armed = false }; } pub fn markPresented(self: Pacer, nowns: u64) void { if (!self.armed) { self.nextdeadlinens = nowns + self.framens; self.armed = true; } else { self.nextdeadlinens += self.framens; } } // Present, and report whether we blew past the deadline (a dropped frame). pub fn presentAndReport(self: Pacer, nowns: u64) bool { const missed = self.armed and nowns > self.nextdeadlinens; self.markPresented(nowns); return missed; } };
test "presentAndReport flags only the frame that overran its deadline" { var pacer = Pacer.init(60); const framens = std.time.nspers / 60;
try std.testing.expect(!pacer.presentAndReport(1000)); // first present arms, never "late" try std.testing.expect(!pacer.presentAndReport(1000 + framens)); // exactly on time try std.testing.expect(pacer.presentAndReport(1000 + 2 framens + 5000)); // ran long: dropped }
Right, exercises done. Now to the sprites.
What a sprite really is
Strip away the mystique and a sprite is the most boring thing imaginable: a small rectangle of pixels, sitting in memory, that you copy onto a bigger rectangle of pixels (the framebuffer) at some position. The word comes from the 1970s arcade hardware that literally had dedicated circuitry to overlay these little images, but the software idea is the same -- draw once, stamp forever. A player character, a coin, a bush, a single letter of text: all sprites.
The operation of stamping one image onto another has a name that goes back to the same era: a blit (from BLIT, "block transfer"). Here is the whole thing -- a Sprite type and a first, naive blit -- built on exactly the Framebuffer and Rgba we have carried since episode 156:
zig const std = @import("std");
pub const Rgba = packed struct { r: u8, g: u8, b: u8, a: u8 = 255 };
pub const Framebuffer = struct { pixels: []Rgba, width: usize, height: usize, pub fn setPixel(self: Framebuffer, x: i64, y: i64, color: Rgba) void { if (x = self.width or uy >= self.height) return; self.pixels[uy self.width + ux] = color; } pub fn getPixel(self: Framebuffer, x: i64, y: i64) ?Rgba { if (x = self.width or uy >= self.height) return null; return self.pixels[uy self.width + ux]; } };
// A sprite is just a small, read-only image. Note the []const Rgba: you can draw // FROM a sprite, but the type forbids you from ever accidentally drawing INTO one. pub const Sprite = struct { pixels: []const Rgba, width: usize, height: usize, pub fn at(self: Sprite, x: usize, y: usize) Rgba { return self.pixels[y self.width + x]; } };
// Stamp a sprite at (dstx, dsty). Fully transparent pixels (a == 0) are skipped, // and setPixel clips anything that lands off the edge of the framebuffer. pub fn blit(fb: Framebuffer, sprite: Sprite, dstx: i64, dsty: i64) void { var sy: usize = 0; while (sy = self.width or uy >= self.height) return; self.pixels[uy self.width + ux] = color; } pub fn getPixel(self: Framebuffer, x: i64, y: i64) ?Rgba { if (x = self.width or uy >= self.height) return null; return self.pixels[uy self.width + ux]; } };
pub const Sprite = struct { pixels: []const Rgba, width: usize, height: usize, pub fn at(self: Sprite, x: usize, y: usize) Rgba { return self.pixels[y self.width + x]; } };
fn blit(fb: Framebuffer, sprite: Sprite, dstx: i64, dsty: i64) void { var sy: usize = 0; while (sy = self.width or uy >= self.height) return; self.pixels[uy self.width + ux] = color; } };
pub const Sprite = struct { pixels: []const Rgba, width: usize, height: usize, pub fn at(self: Sprite, x: usize, y: usize) Rgba { return self.pixels[y self.width + x]; } };
// The naive version, kept here so we can prove the fast one agrees with it. fn blit(fb: Framebuffer, sprite: Sprite, dstx: i64, dsty: i64) void { var sy: usize = 0; while (sy = sprite.width or startsy >= sprite.height) return; // fully off the left/top
// where do we stop (right/bottom clip)? var endsx: usize = sprite.width; var endsy: usize = sprite.height; if (dstx + @as(i64, @intCast(sprite.width)) > fbw) { const visible = fbw - dstx; if (visible fbh) { const visible = fbh - dsty; if (visible = self.width or uy >= self.height) return; self.pixels[uy self.width + ux] = color; } };
pub const Sprite = struct { pixels: []const Rgba, width: usize, height: usize, pub fn at(self: Sprite, x: usize, y: usize) Rgba { return self.pixels[y self.width + x]; } };
fn blit(fb: Framebuffer, sprite: Sprite, dstx: i64, dsty: i64) void { var sy: usize = 0; while (sy = fb->w || y >= fb->h) continue; // YOU must remember this fb->pixels[y fb->w + x] = px; } } }
Rust carries lengths on its slices and bounds-checks indexing for you, and the borrow checker keeps the const source and mutable destination cleanly apart:
rust // Rust: slices know their own length; indexing is bounds-checked by the language struct Rgba { r: u8, g: u8, b: u8, a: u8 } struct Framebuffer { pixels: &'a mut [Rgba], w: i64, h: i64 } struct Sprite { pixels: &'a [Rgba], w: i64, h: i64 }
fn blit(fb: &mut Framebuffer, s: &Sprite, dx: i64, dy: i64) { for sy in 0..s.h { for sx in 0..s.w { let px = &s.pixels[(sy s.w + sx) as usize]; if px.a == 0 { continue; } let (x, y) = (dx + sx, dy + sy); if x = fb.w || y >= fb.h { continue; } fb.pixels[(y fb.w + x) as usize] = Rgba { r: px.r, g: px.g, b: px.b, a: px.a }; } } }
Tags: #stem#stemsocial#steemstem#zig#programming