Blog

Pico Engine: a minimal C game engine behind a fishing game

A small 2D/3D game engine in C with a Vulkan renderer and SDL3 platform layer, plus Kalastaja, the fishing game I am building on top of it.

28 June 2026 · C, Game Engine, Vulkan, SDL3, Graphics

Overview #

Pico Engine is a minimal 2D and 3D game engine written in C, with a Vulkan renderer sitting on top of an SDL3 platform layer. It started as a way to understand the full path from a key press to pixels on screen without hiding behind a large framework, and over time it has turned into something solid enough to build a real game on.

I have not shipped a game yet. What I have is one game in active development, Kalastaja, a 2D fishing game also written in C. It is the reason the engine exists in its current form, and it is the thing that keeps pushing the engine forward. The game lives in its own repository and vendors the engine under third_party/pico-engine/, so the two evolve together but stay cleanly separated.

Kalastaja title screen with a reflected logo and a Swedish main menu
The Kalastaja title screen, rendered by the engine's sprite and text path.

Here is a short reel of the sample games and the fishing game running on the engine:

Why build another engine #

The goal was never to compete with Godot or Unity. It was to keep the whole stack legible. I wanted to understand Vulkan device, swapchain, and pipeline setup by writing it rather than configuring it, to keep the public API small enough that a new game can live in a single readable file, and to produce self-contained binaries with assets baked in at build time. Everything is C17, the dependency list is short, and the sample games are kept independent on purpose so each one reads as a clean example rather than a piece of a framework.

Different from my C++ engine, on purpose #

I already have another engine: a C++23 one built around SDL3 and Vulkan 1.3. That is the ambitious project. It has a runtime host that loads the gameplay as a hot-reloadable shared library, a service-and-callback engine API, and a renderer with lighting and a whole post-processing stack of vignette, fog, god rays and the rest. In that codebase the engine is the point, and the architecture itself is the thing I am building.

Pico Engine is a deliberate counterweight to that. It is written in C rather than C++, which keeps the public surface small and the ABI boring, and it links straight into the game as a static library instead of being loaded as a plugin. There is no hot reload, no host process, and no module boundary to keep honest. A game includes pico/pico.h, calls a handful of functions, and that is the whole contract. The scope is intentionally smaller: get sprites, text, audio, and input on screen with as little ceremony as possible, then stay out of the way. The C API is the feature. It is meant to be easy to pick up and easy to reason about, not to be the most capable engine I can write.

So the two projects pull in opposite directions, and that is the point. In the C++ engine the engine is the project. Here the game is the project, and the engine exists to disappear while I build Kalastaja. To be clear, I have not abandoned the C++ game. I am simply focused on Kalastaja for now, and a small, decoupled C engine is the faster path to actually finishing a game rather than perfecting an engine.

Architecture #

The repository is organized around one static engine library and a handful of small game executables. The engine/ directory builds the pico static library and holds the 2D and 3D rendering, audio, ECS, physics, and collision code. The games/ directory contains the sample games and demos, shaders/ holds the GLSL sources that get compiled to SPIR-V during the build, vendor/ keeps the checked-in third-party headers, and cmake/ holds the shared build helpers and toolchains. Each game links the pico static library directly, so a build is self-contained apart from platform runtime dependencies such as Vulkan, plus the MinGW DLLs when cross-compiling for Windows.

Internally the engine is split into a few layers. The public API in engine/include/pico/ is a deliberately small surface covering initialization and the frame lifecycle, input polling, texture, sprite, tilemap and text rendering, TTF fonts, and audio. The umbrella header pico/pico.h pulls in everything a game needs. The core uses a single global PicoEngine instance that owns the lifecycle order, from SDL setup through Vulkan setup, pipeline creation, renderer init, the per-frame input, event, update and render loop, and finally a reverse-order shutdown. The runtime is single-context and effectively single-threaded, which keeps the mental model simple while I am still learning the hard parts.

The platform layer in engine/src/platform/ handles the SDL3 responsibilities: window creation, event polling, keyboard, mouse and gamepad state, audio mixing, and resolving asset paths relative to the executable. Edge-triggered helpers like pico_key_pressed() are derived from the difference between the previous and current frame rather than tracked separately. The renderer in engine/src/render/ is a Vulkan backend tuned for simple 2D batching. Quads are batched and flushed on a texture change or at the end of the frame, with two frames in flight, nearest-neighbor sampling, and a descriptor-pool-backed texture model. The higher-level primitives for sprites, tilemaps, text, and fonts all build on top of that single path.

What it can do today #

The 2D side is the mature part of the engine and the part the fishing game leans on. It does sprite batching with texture switching, tilemap rendering, TTF font rendering with text layout, music and sound playback through SDL3_mixer, and input across keyboard, mouse, and gamepad. There is also an emerging 3D subsystem with OBJ mesh loading, directional and ambient lighting with shadow mapping, a first-person camera, AABB collision backed by a spatial grid, a basic ECS, and simple physics. The 3D work is a proof of concept that I poke at when I am curious, not the focus, so I would not build anything serious on it yet.

Kalastaja, the game in progress #

Kalastaja is where the engine earns its keep. It is well past the tech-demo stage even though it is not finished. There is a town and an overworld to move through, along with dedicated fishing locations. A day and night cycle runs on a tuned timeline, and a weather system layers in rain with angled, variable-length streaks that follow the wind direction. There is an economy with shops, gear, and inventories you can buy into and discard from, and a fish index that drives progression with loot rarity that varies per area.

Stylised overworld map with a legend for sea, archipelago, lake, and the town
The overworld map. Sea, archipelago, lake, and the town are picked out in the legend, and each region holds its own fishing spots.

Most of the time goes into the fishing scenes themselves, drawn as parallax layers with reflections on the water. The same spot looks different as the time of day and weather change, so a calm morning and a rainy one read as two distinct moods of the same place.

Calm fishing scene with a rowing boat, sun, and forested islands
A calm morning, rowing out.
The same fishing scene under rain with angled streaks and a muted palette
The same scene under rain, with wind-angled streaks.
Fishing scene in a blue palette with the line cast out and a bobber on the water
Line out, waiting for a bite.

Catching something opens a short reveal, and anything you keep lands in the inventory overlay, which is split across your catch, the boat, and what you are holding.

Loot reveal screen showing a small crab with the caption it is a small crab in Swedish
A catch reveal: it is a small crab.
Inventory overlay with tabs for catch, boat, and hand, holding a crab
The inventory overlay, split into catch, boat, and hand.

Away from the water there is a town to walk around, with a boatyard that sells rods and bait and a merchant who buys whatever you have hauled in. Shop prices are colour-coded by what you can currently afford.

Town hub drawn in a two-colour scheme with the player character among buildings
The town hub. Press F for the fishdex, ESC to leave.
Boatyard shop listing rods and bait with prices, green when affordable
The boatyard, with rods and bait priced in markka.
Fish merchant screen prompting you to click a fish to sell it
Selling the day's catch to the fish merchant.

My favorite part to build was the level editor, which lives right inside the game. Pressing F1 in a fishing location freezes gameplay and drops you into edit mode, where you can select and drag elements, assign them to parallax layers, draw collider rectangles, and save the level straight back into the source tree. Because the editor writes into the project, the levels it produces survive a clean build and get picked up automatically the next time the game is packaged.

The in-game level editor with a selected island element framed in cyan and a side panel for layers, scale, colliders, and an asset palette
The in-game level editor: a selected element framed in cyan, with the side panel for parallax layer, scale, colliders, shader toggles, and the asset palette.

Packaging is the other detail I am happy with. Running make dist produces single-file, self-contained binaries for Linux and Windows, with every asset, the sprites, fonts, and levels, embedded at compile time through build-time codegen, so there is no assets/ directory to ship next to the executable. Pushing a version tag triggers CI that builds both platforms and attaches them to a GitHub release, which means the day Kalastaja is ready to ship, the release path is already in place.

Building it #

Both projects use CMake presets and Ninja. For the engine the flow is to configure the debug preset, build it in parallel, and run one of the sample games:

cmake --preset debug
cmake --build --preset debug --parallel "$(./scripts/build-jobs.sh)"
./build/debug/games/tetris/tetris

Windows binaries are produced from Linux through a MinGW-w64 cross toolchain with cmake --preset windows-release, and both repositories ship a devcontainer so the same presets build inside Docker with Vulkan forwarded to the host.

Closing #

Pico Engine is small on purpose. The constraint of keeping the engine readable, keeping the API tiny, and shipping self-contained binaries is exactly what makes it possible to build a game like Kalastaja on top of it without the engine quietly turning into a second project of its own. Nothing here is shipped yet, but the fishing game is getting closer, and the best way to understand the engine is still to read the sample games rather than the documentation.

I wanted to create my own 2D engine, where I control everything relevant (not the platform layer: handled by SDL3). Thank you Ramon Santamaria for creating and maintaining Raylib, that taught me so much! For those of you who don’t know Raylib. It is a cross platform C-based development library that makes writing games very easy and is a good introduction to game development. Raylib exposes an API similar in my opinion to LÖVE2D which is a LUA game development libaray that wraps SDL3.

If you want to create your own game. Do it! In whatever engine you choose. If you want to learn graphics programming: my humble suggestion would be to start with raylib in your language of your choosing (I started with C, because I like how explicit and close to the metal it is).