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.