Lena
Lena is a compact, handmade framework for making tiny games with palette graphics. It's software-rendered, cross-platform and provides artistic constraints that challenge your creativity without limiting your game's size and scope.
It comes with batteries-included palette graphics, palette-blending and other drawing effects, loaders and decoders for image and audio assets, a simple audio interface, and built-in text rendering. It also compiles and runs on:
- Windows (Native)
- macOS (Native)
- Linux (via SDL3)
- WebAssembly (Native)
The core functionality of Lena is implemented from scratch and the whole code base is designed to be extremely legible and hackable. Lena only relies on the libraries that ship with the Odin compiler.
Example
Here's the WASM version of the little introductory demo.odin that comes with the library:
While it's very basic, it shows off the palette blending and rendering features you can get from it, with both the visible and secret text being revealed by the cursor, and then the image shining through when the cursor is used to paint it in.
This is all achieved with the following code:
Click to expand!
package demo
import "lena"
WHITE :: 1
CYAN :: 9
BLUE :: 10
NAVY :: 11
YELLOW :: 15
ORANGE :: 16
CLOVER :: #load("demo.png")
main :: proc() {
lena.init("Hello, Lena!", 128, 128, flags = {.HIDE_WINDOW, .HIDE_CURSOR})
defer lena.destroy()
lena.set_window_background(BLUE)
lena.set_text_color(WHITE)
lena.set_bold_color(YELLOW)
// if yellow is drawn over white, substitute orange
// and don't let yellow on yellow disappear
lena.set_blend_palette_pair(WHITE, YELLOW, ORANGE)
lena.set_blend_palette_pair(YELLOW, YELLOW, ORANGE)
// if the cyan text is drawn over the navy background
// or the yellow cursor, flip it to white and orange
lena.set_blend_palette_pair(CYAN, NAVY, WHITE)
lena.set_blend_palette_pair(CYAN, YELLOW, ORANGE)
// make a blank image to capture drawing
canvas := lena.create_image(128, 128)
defer lena.destroy_image(canvas)
// convert our clover PNG into a Lena image
clover := lena.create_image_from_png(CLOVER)
defer lena.destroy_image(clover)
lena.show_window()
for _ in lena.step() {
if lena.key_pressed(.F11) {
lena.toggle_fullscreen()
}
if lena.key_pressed(.ESCAPE) {
lena.quit()
}
mx, my := lena.get_cursor()
lena.clear_screen(CYAN)
if lena.mouse_held(.LEFT) {
// draw paint to canvas
lena.draw_circle_to_image(canvas, mx, my, 14, YELLOW, true)
// copy clovers to canvas but clipped to canvas alpha
lena.set_draw_state({.LOCK_ALPHA})
lena.draw_image_to_image(clover, canvas, 0, 0)
}
// draw composited canvas to screen
lena.set_draw_state({.BLEND})
lena.draw_image(canvas, 0, 0)
// draw yellow cursor
lena.draw_circle(mx, my, 14, YELLOW, true)
// draw both sets of text
lena.set_text_color(WHITE)
lena.draw_text("Welcome to |*Lena*!", 10, 54 - 47)
lena.set_text_color(CYAN)
lena.draw_text("Hold down |left click...", 10, 66 - 47)
}
}Design & Philosophy
Lena is designed with simplicity in mind. The entire boilerplate required for a 'Hello, world!' program looks like the following —
package main
import "lena"
main :: proc() {
lena.init("Title", 128, 128)
defer lena.destroy()
for _ in lena.step() {
lena.clear_screen()
lena.draw_text("Hello, |world|!", 10, 10)
}
}I wrote Lena because I wanted a dead-simple library to make fun little games with, especially for game jams:
- I wanted something I could work with directly in code, as a library rather than a fantasy console.
- I wanted something with artistic constraints, like palette-based graphics and screen size, but without arbitrary limitations like size of code or number of sprites.
- I wanted something that could produce interesting visual results despite its constraints, like the ZX Spectrum.
- I wanted something truly cross-platform and easy to ship: Lena leverages the tools of the Odin compiler to output your game, assets and all, in a single executable.
It's as much of a fun challenge for me to maintain such a small, legible footprint in a framework as it will hopefully be to make little games in it. I certainly think so.
Education
Lena serves an ulterior motive as well. Many small engines of this kind are still either exceedingly complex in terms of feature set or written in a code-golf style to be as compressed and impressively small as possible, and therefore illegible to beginners.
There are thousands of basic tutorials and toy codebases and playgrounds and sandboxes for the beginner programmer to get started in, some with more value than others. There are also many, many massively complex codebases out there that you're free to peruse, but that even ten-thousand-hour veterans would struggle to understand without putting in months of work doing what amounts to archaeology to wrap their heads around the implementation and architecture.
The problem is the lack of in-between codebases — intermediate-level programs that are doing complex and complete tasks, the kinds of things an enterprising beginner really wants to be doing, but that are implemented in such a way that a single mind can still understand easily.
Lena is just this: a feature-complete toolchain for making fun little games, implemented in a way that can be read and understood in a day. A library doing what I was told, for years, were complicated things — like being natively cross-platform or rendering graphics — but that demonstrates the actual, simple reality of doing so if you don't bury code under layers and layers of abstraction and misdirection and teach beginners that this is 'just how you do it in current year'.
Lena is ~2000 lines of Odin. The hot path for each platform is ~1000 lines. Each platform layer is just ~200 lines of code and a few lookup tables for things like keyboard codes.
Lena is the codebase I would have gifted myself ten years ago if I could have.
