Get started

A camera, a few entities and an update function are a running game. Here is the shortest path from an empty crate to a scene on the desktop and in a browser tab.

1. Add the engine

Physics dimension is chosen by a feature: 2d (the default, and the smaller WebAssembly) or 3d. Pick one and keep it the same in every crate of a multiplayer project, or the simulations will not agree.

# Cargo.toml
[dependencies]
oniq = { path = "../oniq/engine", features = ["rendering"] }

2. A first scene

Spawn a camera and a sprite, step a physics world in the update function, and hand the batcher what to draw. AppHost calls the update at a fixed 60 Hz.

use oniq::ecs::{Camera, Sprite, Transform, World};
use oniq::math::Vec3;
use oniq::runtime::{AppError, AppHost, RenderContext};

fn main() -> Result<(), AppError> {
    let mut world = World::new();
    world.spawn((
        Transform::from_position(Vec3::new(0.0, 0.0, 10.0)),
        Camera::orthographic_2d(40.0, 22.5),
    ));
    world.spawn((Transform::default(), Sprite::new(0).with_color([0.3, 0.66, 1.0, 1.0])));

    AppHost::new("First scene")
        .with_world(world)
        .run(|ctx: &mut RenderContext| {
            ctx.batcher.batch(
                (&mut ctx.world.query::<(&Sprite, &Transform)>())
                    .into_iter()
                    .map(|(_, (sprite, transform))| (sprite, transform)),
            );
            ctx.batcher.upload_to_gpu();
            Ok(())
        })
}

Run it with cargo run. The demos/ directory in the engine has small, runnable scenes for most subsystems: sprites, physics, fluids, text, audio, bones, tilemaps, brushes, scripting, networking and Spine.

3. The same scene in a browser

Build the game into a cdylib and start it with run_web, which takes a canvas on the page. The page's CSS sizes the canvas, the browser paces the frames, and GPU setup finishes asynchronously before the first update.

# Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Document", "HtmlCanvasElement", "Window"] }
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

#[wasm_bindgen]
pub fn start(canvas_id: &str) -> Result<(), JsValue> {
    let canvas = web_sys::window()
        .and_then(|w| w.document())
        .and_then(|d| d.get_element_by_id(canvas_id))
        .ok_or("no canvas")?
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    let (host, update) = build_game().map_err(|e| e.to_string())?;
    host.run_web(canvas, update).map_err(|e| e.to_string().into())
}
# Build, generate the JavaScript glue, and serve.
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir pkg \
  target/wasm32-unknown-unknown/release/my_game.wasm
<canvas id="game"></canvas>
<script type="module">
  import init, { start } from "./pkg/my_game.js";
  await init();
  start("game");
</script>

The wasm-bindgen command-line tool must match the version of the wasm-bindgen crate in your lockfile. Browsers need WebGPU: current Chrome and Edge, Safari 26 and later, and Firefox 141 and later on Windows.

4. Android

On Android the same host runs through run_android, which takes the activity handle the android-activity glue passes to android_main. The engine's Android guide covers packaging with cargo-ndk and Gradle, the JNI helpers, lifecycle, input and power.