Groot is a fast-iteration 2D/3D game engine powered by wgpu (modern GPU rendering)
with GoScript — an embeddable Go-syntax scripting VM written in pure Rust.
Game logic lives in .gos files under assets/scripts/ and hot-reloads on save;
visuals and prefab hierarchies live in RON asset files under assets/prefabs/ and assets/scenes/.
Groot is data-driven: scripts own behavior and data; the host engine owns representation and rendering. Visuals are declared as RON prefab data.
assets/scenes/*.scene.ron ──► hecs ECS (3D Meshes, Lights, Sprites, UI)
assets/prefabs/*.prefab.ron ──► RonAssetWatcher (Visual Hot Reloading)
assets/scripts/*.gos ──► GoScript VM (Logic Hot Reloading) ──► Entity Data
▲ │
└── groot.* host bindings ──────────┘
assets/config.ron— project config & render settings.assets/prefabs/*.prefab.ron— 2D/3D prefabs (sprites, text, 3D PBR meshes, lights, colliders, parent-child hierarchies).assets/scenes/*.scene.ron— scene layout, environment settings, cameras, entity initializers.src/main.rs— initializes winit window and wgpu render context, runs main event loop.src/platform/— platform abstraction layer (desktop event loop, Android entry).src/render/— pure wgpu rendering engine (3D meshes, 2D sprites, text, gizmos).src/ecs/— hecs-based ECS components and queries.src/assets/— asset loading, encryption, archive packing (see Asset Security).src/script/— GoScript VM host, input tracking, script execution.src/plugin.rs— plugin trait and manager (re-exports fromgroot-plugin-api).
Groot ships a built-in asset protection pipeline that prevents plaintext inspection of scripts, prefabs, and scenes in release builds.
All assets are encrypted with a ChaCha20-inspired XOR stream cipher keyed
by a compile-time secret and an asset-path nonce. A CRC32 integrity header
detects tampering. The default key is baked in at compile time; production
builds can override it at compile time by setting the GROOT_ENCRYPTION_KEY
environment variable before building:
GROOT_ENCRYPTION_KEY="MyStrongKey32BytesExactly______!" cargo build --release| Function | Description |
|---|---|
encrypt_asset(path, data) |
Encrypt bytes with a path-derived nonce |
decrypt_asset(path, data) |
Decrypt bytes; falls back to raw if not encrypted (debug) |
minify_goscript(source) |
Strip // and /* */ comments + blank lines from .gos |
Assets can be packed into a single encrypted .gpak binary archive,
similar to Unreal .pak or Unity asset bundles.
Archive format:
[GPAK] 4 bytes magic
[u16] 2 bytes version
[u32] 4 bytes entry count
per entry:
[u16] path length
[N] UTF-8 relative path
[u32] data length
[M] encrypted payload
Pack via the CLI (see CLI):
groot pack assets/ assets.gpakOpen a .gpak from Rust:
let archive = GpakArchive::open("assets.gpak")?;
let bytes = archive.entries.get("scripts/player.gos");| Build mode | Strategy |
|---|---|
cargo run (debug, desktop) |
Raw .gos read from disk — hot-reload preserved |
cargo build --release |
Decrypt + minify in memory via HotReloadEngine::from_str — no temp file |
| Android | Same as release — fully in-memory |
When a physical path is required (debug hot-reload watcher), prepare_script_path
writes a minified, hashed .goc file to /tmp/.groot_cache/ with 0600
permissions and registers a panic hook that deletes the cache on process exit.
Groot supports native Rust plugins via the GrootPlugin trait. Plugins are
managed through the CLI and compiled as separate crates that depend on the
shared groot-plugin-api crate.
groot-plugin-api ──► Defines GrootPlugin trait + PluginManager
groot-plugin-audio ──► Sample audio synthesizer plugin
groot-plugin-gizmos ──► Sample debug shape drawer plugin
groot-plugins/ ──► Plugin registry (index.ron)
use groot_plugin_api::{GrootPlugin, VirtualMachine};
use goscript::value::Value;
pub struct MyPlugin;
impl GrootPlugin for MyPlugin {
fn name(&self) -> &'static str {
"my-plugin"
}
fn register_script_bindings(&self, vm: &mut VirtualMachine) {
vm.register_fn("my_plugin.DoThing", |args| {
let x = args.first().and_then(|v| v.as_number()).unwrap_or(0.0);
log::info!("[MY PLUGIN] Doing thing at {x}");
Value::Nil
});
}
}# List available plugins
groot plugin list
# Install a plugin (adds to Cargo.toml)
groot plugin add audio
# Remove a plugin
groot plugin remove audio# Install the CLI (binary is now `groot`)
cargo install --path . # installs `groot` to ~/.cargo/bin
# Scaffold a new project folder
groot new my-game # or: cargo run --bin groot -- new my-game
# Run the current Groot game
groot run # or: cargo run --bin groot -- run
# Build a release bundle
groot build # or: cargo run --bin groot -- build
# Pack and encrypt the assets directory into a .gpak archive
groot pack assets/ assets.gpak
# Scaffold prefabs, scripts, and scenes
groot generate prefab enemy
groot generate script player
groot generate scene arena # alias: groot g <type> <name>
# Diagnose toolchain (Rust, cargo-apk, Android SDK/NDK, Vulkan)
groot doctor
# Shell completions
groot completions bash # bash|zsh|fish
# or: groot completions zsh > ~/.zsh/completions/_groot
# Manage plugins
groot plugin list
groot plugin add audio
groot plugin remove audioDesktop and Android builds share a single --target flag on run and build (defaults to desktop):
# Desktop (current host)
groot run
groot build --target desktop
# Android APK (requires rustup target aarch64-linux-android + cargo-apk)
groot run --target android # cargo apk run (deploys to a connected device via adb)
groot build --target android # cargo apk build --releaseOn Android, groot run --target android auto-detects connected devices with
adb; if several are present it lists them and lets you pick one, or you can
pass --device <serial> (or --device <index>) to select directly.
Assets (assets/) are compiled into the APK using rust-embed with the
debug-embed feature, so games work on-device in both debug and release
builds. On desktop debug builds, asset files are still read from disk first
so .gos and .prefab.ron edits hot-reload while developing.
rustup target add aarch64-linux-androidcargo install cargo-apk- Android SDK with platform
android-34(or settarget_sdk_version/min_sdk_versionunder[package.metadata.android.sdk]inCargo.toml)
cargo runThe Flappy Groot demo starts up: a neon 2D side-scroller where you flap a bird through pipe gaps, avoiding solid ground and ceiling. Save any .gos or .prefab.ron file to see live hot reloading!
2D sprites are drawn back-to-front by their layer field (background → pipes → ground/ceiling → bird) and scaled to their world-space size from the prefab's size. Sprite textures (bird, pipes, ground, ceiling, grid) are generated procedurally at startup and cached as PNG bind groups.
type Player struct { Speed float64 }
var self = Player{Speed: 5.0}
func OnUpdate(dt float64) {
var pos = groot.GetSelfPosition()
var moveX = groot.GetAxis("Horizontal")
var moveZ = -groot.GetAxis("Vertical")
groot.SetSelfPosition(pos[0] + moveX*self.Speed*dt, pos[1], pos[2] + moveZ*self.Speed*dt)
groot.SetSelfCollider(1.0, 1.0, 1.0)
groot.Log("Hello from Groot 3D GoScript!")
}winit0.29 - Cross-platform windowing and inputwgpu0.19 - Modern GPU rendering (Vulkan/Metal/DX12)glam0.27 - Fast 3D/2D math libraryhecs0.10 - Minimalist archetype ECSbytemuck1.14 - Safe casting for GPU bufferspollster0.3 - Block on async operationsron0.8 - Rusty Object Notation for assetsserde1 - Serialization frameworkgoscript(git:github.com/johnesleyer/goscript) - GoScript VMgroot-plugin-api- Shared plugin trait and manager
Engineering deep-dives from the engine's development, including the Android bring-up: