Dojo is a full stack toolchain for developing onchain games in Cairo. Dojo leverages the affordances provided by Cairo language plugins to offer a best-in-class developer experience for easily integrating blockchain properties into games.
- Simple composition through the Entity Component System pattern
- Concise implementations leveraging language plugins and macros
- Expressive query system with efficiently compiled strategies
- Typed interface generation for client libraries
The toolchain includes the following:
dojo-ecs
: A concise and efficient implementation of the Entity Component System pattern.dojo-migrate
: Deploy, migrate, and manage the entities, components, and systems in the world.dojo-bind
: Generate bindings for various languages / frameworks (typescript, phaser / rust, bevy).
git submodule update --init --recursive
It is recommended to use the dev container when building on DoJo as it contains everything needed to begin developing.
Make sure you update your Docker to the latest stable version, sometimes the Dev containers do not play nicely with old Docker versions.
Command pallete: ctrl + shift + p
Then: Remote-Containers: Rebuild Container Without Cache
cd cairo/vscode-cairo
npm install --global @vscode/vsce
npm install
vsce package
code --install-extension cairo1*.vsix
cd /workspaces/dojo
cargo build --bin dojo-language-server --release
- Install Rust
- Setup Rust:
rustup override set stable && rustup update && cargo test
Then install the language like described above.
Dojo implements the ECS pattern which is subsequently compiled to Starknet contracts for deployment. The syntax and semantics are heavily inspired by Bevy.
A world
is the top-level concept in an onchain game, serving as a centralized registry, namespace, and event bus for all entities, components, systems, and resources.
The worlds interface is as follows:
trait World {
#[event]
fn ValueSet(component: felt252, key: StorageKey, offset: u8, value: Span<felt252>) {}
#[event]
fn ComponentRegistered(name: felt252, class_hash: ClassHash) {}
#[event]
fn SystemRegistered(name: felt252, class_hash: ClassHash) {}
// Returns a globally unique identifier.
#[view]
fn uuid() -> felt252;
// Returns a globally unique identifier.
#[view]
fn get(component: felt252, key: StorageKey, offset: u8, length: usize) -> Span<felt252>;
// Returns all entities that contain the component.
#[view]
fn all(component: felt252, partition: felt252) -> Array<StorageKey>;
// Sets a components value.
#[external]
fn set(component: felt252, key: StorageKey, offset: u8, value: Span<felt252>);
// Returns all entities that contain the component.
#[external]
fn register_component(name: felt252, class_hash: ClassHash);
// Returns all entities that contain the component.
#[external]
fn register_system(name: felt252, class_hash: ClassHash);
}
Components in dojo-ecs
are modules with a single struct describing its state, for example, the following implements a Position
component which exposes a is_zero
and is_equal
method.
#[derive(Component)]
struct Position {
x: u32,
y: u32
}
trait PositionTrait {
fn is_zero(self: Position) -> bool;
fn is_equal(self: Position, b: Position) -> bool;
}
impl PositionImpl of PositionTrait {
fn is_zero(self: Position) -> bool {
match self.x - self.y {
0 => bool::True(()),
_ => bool::False(()),
}
}
fn is_equal(self: Position, b: Position) -> bool {
self.x == b.x & self.y == b.y
}
}
A system is a pure function that takes as input a set of entities to operate on. Systems define a Query
which describes a set of criteria to query entities with.
#[system]
mod SpawnSystem {
#[execute]
fn spawn(name: String) {
let player_id = commands::create((
Health::new(100_u8),
Name::new(name)
));
return ();
}
}
#[system]
mod MoveSystem {
#[execute]
fn move(player_id: usize) {
let player = commands<(Health, Name)>::get(player_id);
let positions = commands<(Position, Health)>::all();
// @NOTE: Loops are not available in Cairo 1.0 yet.
for (position, health) in positions {
let is_zero = position.is_zero();
}
return ();
}
}
An entity is addressed by a felt
. An entity represents a collection of component state. A component can set state for an arbitrary entity, registering itself with the world as a side effect.
Events are emitted anytime a components state is updated a ValueSet
event is emitted from the world, enabling clients to easily track changes to world state.
Given addresses of every component / system in the world is deterministically addressable, the dojo-migrate
cli takes a world address as entrypoint and diffs the onchain state with the compiled state, generating a deploying + migration plan for declaring and registering new components and / or updating existing components.
Bind is a cli for generating typed interfaces for integration with various client libraries / languages.