Install and first run
rocco is a 3D game engine for games written in Roc. You write your game as a few pure functions, and the engine runs the window, the clock, the input and the GPU for you.
You only need Roc. Each rocco release comes with the engine prebuilt for macOS arm64, Linux x64 and Windows x64, so you don’t need Odin, sokol or a C compiler.
If you read this page on GitHub, the Roc nightly and the bundle URLs show as markers in double braces. You can find the values in the notes of the latest release. The notes give the Roc nightly and the URL of each bundle.
Is rocco right for you?
rocco is young, and it has some clear limits today. Check them before you start a game with it:
- Desktop only. rocco runs on macOS arm64, Linux x64 and Windows x64. On Linux it needs an X11 desktop and an OpenGL driver. You build on each OS that you ship to, because there is no cross-compiling.
- No mouse buttons. The Input has the keyboard, the mouse movement and up to 4 gamepads, but no mouse buttons. Windows reads Xbox-compatible pads only. See Gamepads.
- No physics. The engine answers questions about shapes, such as “what does this ray hit?”, but it moves nothing and pushes nothing apart. See Collisions and Queries.
- Roc changes quickly. Roc changes from one nightly to the next. Each rocco release pins one nightly, and you use that exact nightly.
If those limits are fine for your game, read on.
Install Roc
Use the nightly that the release names. A different nightly may not build the game.
- Download the Roc nightly
nightly-2026-10-11-7d256b9for your OS from the Roc nightlies. - Extract the archive.
- Add the folder that holds
rocto yourPATH. - Run
roc version. It should printnightly-2026-10-11-7d256b9.
On Linux, the game needs an X11 desktop and an OpenGL driver. On Windows,
run roc in a shell where LIB is not set. A Visual Studio developer
prompt sets LIB, and then the link fails.
Make the game folder
- Make a folder called
my-game. - Make the file
my-game/main.roc. - Put this header at the top of
my-game/main.roc:
app [init, launch, loaded, step, view] { cam: "https://github.com/Pixelsprout/rocco-engine/releases/download/v0.6.0/camera-QiJqL4WFPQGUbBCMZ5bzjdLHhgRz7BiUw1ngwxWwHtP.tar.zst", mesh: "https://github.com/Pixelsprout/rocco-engine/releases/download/v0.6.0/mesh-5Fmg6uFHADmSBmZxNWDg25VStK6mU7YqhkWEXHjMEGNP.tar.zst", pf: platform "https://github.com/Pixelsprout/rocco-engine/releases/download/v0.6.0/platform-2gvPTPztQ7D8yQsMvRTD4rBKS8VmSwMCPZ432eWzNNtJ.tar.zst", roc: "nightly-2026-10-11-7d256b9" }- Below the header, add this code. It is the cards game from
examples/cards/main.roc.
import cam.Camera as Cam
import mesh.Mesh as Mesh
import pf.Vocabulary exposing [Command, Manifest]
import pf.Config
import pf.Draw
import pf.Input
import pf.Launch
import pf.Scene
import pf.Key
import pf.Light
import pf.QueryResults
# A game with no entities and no positions in the Model.
Card : [Ace, King, Number(U8)]
Model : { hand : List(Card), turns : U64, name : Str, card_mesh : U32 }
launch : Vocabulary.Config -> Vocabulary.Launch
launch = |_config| Launch.default
# A plane until a card mesh is on disk. Asking only for names in Config keeps a missing card out of the log.
init : Vocabulary.Config -> { model : Model, commands : List(Command) }
init = |config| {
assets = if config.assets.contains("card") {
["card"]
} else {
[]
}
model = { hand: [Ace, Number(7)], turns: 0, name: "solitaire", card_mesh: Mesh.primitive(Plane) }
{ model, commands: [Load({ pack: "cards", assets })] }
}
loaded : Model, Str, Manifest -> Model
loaded = |m, _pack, manifest| match List.find_first(manifest, |entry| entry.name == "card") {
Ok(entry) => { ..m, card_mesh: entry.id }
Err(NotFound) => m
}
update : Model, Vocabulary.Input, Vocabulary.QueryResults, F32 -> Model
update = |m, input, _results, _dt|
if Key.pressed(input, Space) {
{ ..m, hand: List.append(m.hand, King), turns: m.turns + 1 }
} else {
m
}
step : Model, Vocabulary.Input, Vocabulary.QueryResults, F32 -> { model : Model, commands : List(Command) }
step = |m, input, results, dt| { model: update(m, input, results, dt), commands: [] }
ambient : Vocabulary.Light
ambient = Light.ambient({ r: 0.2, g: 0.2, b: 0.2 })
# The BRDF divides diffuse light by pi, so 5.03 (1.6 pi) lights a white surface facing it to 1.6.
sun : Vocabulary.Light
sun = Light.directional({ x: -2.0 / 7.0, y: -3.0 / 7.0, z: -6.0 / 7.0 }, { r: 5.03, g: 5.03, b: 5.03 })
# The Lights, then one Draw for each card.
view : Model -> Vocabulary.Scene
view = |m| {
cards = List.map_with_index(m.hand, |card, i| Draw(card_draw(m, card, i)))
nodes = [Light(ambient), Light(sun)].concat(cards)
{ ..Scene.empty, camera: Cam.look_at({ x: 0.0, y: 8.0, z: 8.0 }, { x: 0.0, y: 0.0, z: 0.0 }), nodes }
}
card_draw : Model, Card, U64 -> Vocabulary.Draw
card_draw = |m, card, i| {
tint = match card {
Ace => { r: 1.0, g: 0.787, b: 0.033 }
King => { r: 0.604, g: 0.033, b: 0.033 }
Number(_) => { r: 0.787, g: 0.787, b: 0.787 }
}
# The slot index is the draw id: a card slides to its slot when dealt.
{ ..Draw.default, id: i, mesh: m.card_mesh, pos: { x: i.to_f32() * 1.2, y: 0.0, z: 0.0 }, scale: { x: 1.0, y: 1.0, z: 1.4 }, tint }
}Run the game
In my-game/, run:
roc main.rocThe first run downloads the bundles and builds the game. Then a window opens with two cards. Press Space to add a card, and close the window to stop the game.
What the code does
The header names the platform and each package that the game uses. pf is
the rocco platform, mesh gives the primitive meshes, and cam points the
camera. If your game uses vectors, add linalg: "https://github.com/Pixelsprout/rocco-engine/releases/download/v0.6.0/linalg-4N62Pw77Sky6ba8RiBfvULjHuAQeQiqHbUor7qSeor7F.tar.zst". If it
uses the UI layout, add ui: "https://github.com/Pixelsprout/rocco-engine/releases/download/v0.6.0/ui-5msrKWjNRwuEBomX2R1CBFRycCCjrQCPjhEvJXtwWbU6.tar.zst".
The game gives the engine five calls:
launchsets up the window and the step rate. The cards game keeps the defaults.initgives the first Model and the first Commands.loadedputs the meshes of a Pack into the Model. The cards game has noassets/folder, so it draws each card as a plane.stepgives the next Model from the Input, once for each fixed step.viewgives the Scene that the engine draws: the camera, and the Lights and the Draws inScene.nodes.
Next, read Important defaults to avoid the common surprises, then The five calls.