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/script/— GoScript VM host, input tracking, script execution.src/plugin.rs— plugin trait and manager (re-exports fromgroot-plugin-api).
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# Scaffold a new project folder
cargo run --bin groot-cli -- new my-game
# Run the current Groot game
cargo run --bin groot-cli -- run
# Build a release bundle
cargo run --bin groot-cli -- build
# Manage plugins
cargo run --bin groot-cli -- plugin list
cargo run --bin groot-cli -- plugin add audio
cargo run --bin groot-cli -- 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: