An Onchain Game Engine implemented in Cairo 1.0

Overview

Dōjō

Dojo is a full stack toolchain for developing onchain games in Cairo. Dojo leverages the afforadances provided by the Cairo language to offer an best-in-class developer experience for easily integration blockchain properties into their 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: An 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).

Development

Prerequisites

  • Install Rust
  • Setup Rust:
rustup override set stable && rustup update && cargo test

Overview

Entity Component System

Dojo implements the ECS pattern which is subsequently compiled to Starknet contracts for deployment. The syntax and semantics are heavily inspired by Bevy.

Worlds

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 {
    // Register a component or system. The returned
    // hash is used to uniquely identify the component or
    // system in the world. All components and systems
    // within a world are deteriministically addressed
    // relative to the world.
    // @TODO: Figure out how to propagate calldata with Cairo 1.0.
    fn register(id: felt, class_hash: felt) -> felt;

    // Called when a component in the world updates the value
    // for an entity. When called for the first time for an 
    // entity, the entity:component mapping is registered.
    // Additionally, a `ComponentValueSet` event is emitted.
    fn on_component_set(entity_id: felt, data: Array::<felt>);

    // Lookup entities that have a component by id.
    fn lookup(id: felt) -> Array::<felt>;
}

Components

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 method.

#[component]
mod PositionComponent {
    struct Position {
        x: felt,
        y: felt
    }

    #[view]
    fn is_zero(self: Position) -> bool {
        match self.x - self.y {
            0 => bool::True(()),
            _ => bool::False(()),
        }
    }

    #[view]
    fn is_equal(self: Position, b: Position) -> bool {
        self.x == b.x & self.y == b.y
    }
}

Components are then expanded to Starknet contract:

#[contract]
mod PositionComponent {
    struct Position {
        x: felt,
        y: felt
    }

    struct Storage {
        world_address: felt,
        state: Map::<felt, Position>,
    }

    // Initialize PositionComponent.
    #[external]
    fn initialize(world_addr: felt) {
        let world = world_address::read();
        assert(world == 0, 'PositionComponent: Already initialized.');
        world_address::write(world_addr);
    }

    // Set the state of an entity.
    #[external]
    fn set(entity_id: felt, value: Position) {
        state::write(entity_id, value);
    }

    // Get the state of an entity.
    #[view]
    fn get(entity_id: felt) -> Position {
        return state::read(entity_id);
    }

    #[view]
    fn is_zero(entity_id: felt) -> bool {
        let self = state::read(entity_id);
        match self.x - self.y {
            0 => bool::True(()),
            _ => bool::False(()),
        }
    }

    #[view]
    fn is_equal(entity_id: felt, b: Position) -> bool {
        let self = state::read(entity_id);
        self.x == b.x & self.y == b.y
    }
}

In the expanded form, entrypoints take entity_id as the first parameter.

Systems

A system is a free function that takes as input a set of entities to operate on. Systems define a Query which describes a set of Components to query a worlds entities by. At compile time, the Query is compiled, leveraging deterministic addresssing to inline efficient entity lookups.

#[system]
mod MoveSystem {
    fn execute(query: Query<(Position, Health)>) {
        // @NOTE: Loops are not available in Cairo 1.0 yet.
        for (position, health) in query {
            let is_zero = position.is_zero();
        }
        return ();
    }
}

Expansion:

#[contract]
mod MoveSystem {
    struct Storage {
        world_address: felt,
    }

    #[external]
    fn initialize(world_addr: felt) {
        let world = world_address::read();
        assert(world == 0, 'MoveSystem: Already initialized.');
        world_address::write(world_addr);
    }

    #[external]
    fn execute() {
        let world = world_address::read();
        assert(world != 0, 'MoveSystem: Not initialized.');

        let position_id = pedersen("PositionComponent");
        // We can compute the component addresses statically
        // during compilation.
        let position_address = compute_address(position_id);
        let position_entities = IWorld.lookup(world, position_id);

        let health_id = pedersen("HealthComponent");
        let health_address = compute_address(health_id);
        let health_entities = IWorld.lookup(world, health_id);

        let entities = intersect(position_entities, health_entities);

        for entity_id in entities {
            let is_zero = IPosition.is_zero(position_address, entity_id);
        }
    }
}

Entities

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.

Addressing

Everything inside a Dojo World is deterministically addressed relative to the world, from the address of a system to the storage slot of an entities component value. This is accomplished by enforcing module name uniqueness, i.e. PositionComponent and MoveSystem, wrapping all components and systems using the proxy pattern, and standardizing the storage layout of component modules.

This property allows for:

  1. Statically planning deployment and migration strategies for updates to the world
  2. Trustlessly recreating world state on clients using a light client with storage proofs
  3. Optimistically updating client state using client computed state transitions
  4. Efficiently querying a subset of the world state without replaying event history
use starknet::{deploy, pedersen};

impl World {
    struct Storage {
        registry: Map::<felt, felt>,
    }

    fn register(class_hash: felt) -> felt {
        let module_id = pedersen("PositionComponent");
        let address = deploy(
            class_hash=proxy_class_hash,
            contract_address_salt=module_id,
            constructor_calldata_size=0,
            constructor_calldata=[],
            deploy_from_zero=FALSE,
        );
        IProxy.set_implementation(class_hash);
        IPositionComponent.initialize(address, ...);
        registry.write(module_id, address);
    }
}

Events

Events are emitted anytime a components state is updated a ComponentValueSet event is emitted from the world, enabling clients to easily track changes to world state.

Migrate

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

Bind is a cli for generating typed interfaces for integration with various client libraries / languages.

Comments
  • fix: mirror starknet contract syntax

    fix: mirror starknet contract syntax

    updates component syntax to match that of contracts with the cairo-plugin-starknet crate

    #[component]
    mod PositionComponent {
        struct Position {
            x: felt,
            y: felt
        }
    
        #[view]
        fn is_zero(self: Position) -> bool {
            match self.x - self.y {
                0 => bool::True(()),
                _ => bool::False(()),
            }
        }
    
        #[view]
        fn is_equal(self: Position, b: Position) -> bool {
            self.x == b.x & self.y == b.y
        }
    }
    
    opened by tarrencev 0
  • Compute component contracts address

    Compute component contracts address

    Given all components in a world are deployed by the world, we can compute a components address given it's component id:

    let module_id = pedersen("<module_name>");
    let address = deploy(
        class_hash=proxy_class_hash,
        contract_address_salt=module_id,
        constructor_calldata_size=0,
        constructor_calldata=[],
        deploy_from_zero=FALSE,
    );
    IProxy.set_implementation(class_hash);
    IPositionComponent.initialize(address, ...);
    registry.write(module_id, address);
    

    Currently we don't know the proxy class hash so we can stub that.

    starknet-rs provides a helper for computing a contract address: https://github.com/xJonathanLEI/starknet-rs/blob/ab4752f2b26bbb43dec3450e4358d88fa7a496e2/starknet-core/src/utils.rs#L130

    The method should be used here: https://github.com/dojoengine/dojo/blob/c9dea00d631ec691a770b21ed00aeb1eb47e84f2/crates/cairo-lang-dojo/src/query.rs#L45

    good first issue help wanted Difficulty: easy Duration: 0.5 day 
    opened by tarrencev 0
  • Dojo CLI

    Dojo CLI

    Create a dojo-cli crate with a rough (eventual) api generated by chat gpt below

    Usage: dojo [command] [options]
    
    Commands:
      build        Build the project's ECS, outputting smart contracts for deployment
      migrate      Run a migration, declaring and deploying contracts as necessary to update the world
      bind         Generate rust contract bindings
      inspect      Retrieve an entity's state by entity ID
    
    Options:
      -h, --help   Show help information
    
    Command "build":
      Usage: dojo build [options]
    
      Options:
        -h, --help  Show help information
    
    Command "migrate":
      Usage: dojo migrate [options]
    
      Options:
        -h, --help     Show help information
        --plan         Perform a dry run and outputs the plan to be executed
        --world_address  World address to run migration on
    
    Command "bind":
      Usage: dojo bind [options]
    
      Options:
        -h, --help  Show help information
    
    Command "inspect":
      Usage: dojo inspect [options]
      Options:
        -h, --help  Show help information
        --id        Entity ID to retrieve state for
        --world_address  World address to retrieve entity state from
    
    enhancement help wanted 
    opened by tarrencev 0
  • World indexer

    World indexer

    Implement an indexer to index world events in an efficient and easily queryable manner. The database should expose both historical and the current state of the world.

    Objects like:

    • components
    • entities
    • systems
    • resources

    Historical events like:

    • component / system / entity registration
    • entity state updates
    • system calls

    The indexer could use the https://github.com/apibara/apibara engine to stream events from starknet and write them to a sqlite db that can be easily replicated and shared with others.

    enhancement good first issue Duration: 5 days Difficulty: intermediate 
    opened by tarrencev 2
  • Build cairo language server with Dojo plugin

    Build cairo language server with Dojo plugin

    Currently the cairo language server doesn't support dynamic plugin configuration. In the short term, we should create a crate that wraps it and incorporates the dojo plugin.

    Basically, we need to copy this file and include the dojo plugin before the starknet plugin:

    https://github.com/starkware-libs/cairo/blob/main/crates/cairo-lang-language-server/src/bin/language_server.rs#L18

    good first issue help wanted Difficulty: intermediate Duration: 0.5 day 
    opened by tarrencev 1
  • Tracking world state

    Tracking world state

    So far we've planned to use a similar pattern to mud for tracking world state externally, emitting events from the world contract for indexers. I'm wondering if we could instead use the state diff directly, since we can constrain the storage address of the component state in the generated code, for example, the storage location for an entity's position state below would be:

    pedersen(keccak('state'), entity_id)

    #[contract]
    mod PositionComponent {
        struct Position { x: felt, y: felt }
    
        struct Storage {
            world_address: felt,
            state: Map::<felt, Position>,
        }
    }
    

    Given the indexer knows about the components / systems that exist in the world, it could compute the entity the state update corresponds to. The tricky part is the storage address is not reversible, so the indexer would need to track entity id's that have been provisioned and compute/store the component state address.

    The upside is the state diff is part of consensus state, so if we do this, clients can verifiably pull world state off rpc nodes using storage proofs, which would allow for light clients ect that don't need to replay all events to reconstruct the world state.

    Tagging @fracek @ponderingdemocritus @milancermak for feedback.

    question 
    opened by tarrencev 7
  • Implement entity query api

    Implement entity query api

    Systems operate on entities in the world. In order to define the set of entities to operate on, dojo should expose a query api similar to the bevy engine.

    The api should be expressive, allowing developers to easily construct complex queries, which are interpreted, optimized and inlined at compile time to reduce onchain compute.

    The interface should follow the bevy interface:

    fn move(query: Query<(Position, Health)>) {
        // @NOTE: Loops are not available in Cairo 1.0 yet.
        for (position, health) in query {
            let is_zero = position.is_zero();
        }
        return ();
    }
    

    Expansion:

    #[contract]
    mod MoveSystem {
        struct Storage {
            world_address: felt,
        }
    
        #[external]
        fn initialize(world_addr: felt) {
            let world = world_address::read();
            assert(world == 0, 'MoveSystem: Already initialized.');
            world_address::write(world_addr);
        }
    
        #[external]
        fn execute() {
            let world = world_address::read();
            assert(world != 0, 'MoveSystem: Not initialized.');
    
            let position_id = pedersen("PositionComponent");
            // We can compute the component addresses statically
            // during compilation.
            let position_address = compute_address(position_id);
            let position_entities = IWorld.lookup(world, position_id);
    
            let health_id = pedersen("HealthComponent");
            let health_address = compute_address(health_id);
            let health_entities = IWorld.lookup(world, health_id);
    
            let entities = intersect(position_entities, health_entities);
    
            for entity_id in entities {
                let is_zero = IPosition.is_zero(position_address, entity_id);
            }
        }
    }
    
    Duration: 3 days Difficulty: intermediate 
    opened by tarrencev 2
Owner
Dojo
On-Chain Gaming Toolchain for StarkNet
Dojo
Minecraft-esque voxel engine prototype made with the bevy game engine. Pending bevy 0.6 release to undergo a full rewrite.

vx_bevy A voxel engine prototype made using the Bevy game engine. Goals and features Very basic worldgen Animated chunk loading (ala cube world) Optim

Lucas Arriesse 125 Dec 31, 2022
2-player game made with Rust and "ggez" engine, based on "Conway's Game of Life"

fight-for-your-life A 2-player game based on the "Conway's Game of Life", made with Rust and the game engine "ggez". Create shapes on the grid that wi

Petros 3 Oct 25, 2021
2d Endless Runner Game made with Bevy Game Engine

Cute-runner A 2d Endless Runner Game made with Bevy Game Engine. Table of contents Project Infos Usage Screenshots Disclaimer Project Infos Date: Sept

JoaoMarinho 2 Jul 15, 2022
A game of snake written in Rust using the Bevy game engine, targeting WebGL2

Snake using the Bevy Game Engine Prerequisites cargo install cargo-make Build and serve WASM version Set your local ip address in Makefile.toml (loca

Michael Dorst 0 Dec 26, 2021
A game made in one week for the Bevy engine's first game jam

¿Quien es el MechaBurro? An entry for the first Bevy game jam following the theme of "Unfair Advantage." It was made in one week using the wonderful B

mike 20 Dec 23, 2022
A Client/Server game networking plugin using QUIC, for the Bevy game engine.

Bevy Quinnet A Client/Server game networking plugin using QUIC, for the Bevy game engine. Bevy Quinnet QUIC as a game networking protocol Features Roa

Gilles Henaux 65 Feb 20, 2023
Solana Game Server is a decentralized game server running on Solana, designed for game developers

Solana Game Server* is the first decentralized Game Server (aka web3 game server) designed for game devs. (Think web3 SDK for game developers as a ser

Tardigrade Life Sciences, Inc 16 Dec 1, 2022
A sci-fi battle simulation implemented in the bevy engine.

Bevy Combat It's a bevy port of my ECS Combat Unity demo. Check out the web demo in your browser. You can use the - and = keys to speed up and slow do

ElliotB256 47 Dec 22, 2022
Game examples implemented in rust console applications primarily for educational purposes.

rust-console-games A collection of game examples implemented as rust console applications primarily for providing education and inspiration. :) Game *

Zachary Patten 2 Oct 11, 2022
Rust-implemented master mind game solver

master_mind_rust A Rust-implemented solver for the master mind game. You can play the master mind game online here → https://webgamesonline.com/master

Nariaki Tateiwa 4 Feb 25, 2024
A refreshingly simple data-driven game engine built in Rust

What is Bevy? Bevy is a refreshingly simple data-driven game engine built in Rust. It is free and open-source forever! WARNING Bevy is still in the ve

Bevy Engine 21.1k Jan 4, 2023
A modern 3D/2D game engine that uses wgpu.

Harmony A modern 3D/2D game engine that uses wgpu and is designed to work out of the box with minimal effort. It uses legion for handling game/renderi

John 152 Dec 24, 2022
RTS game/engine in Rust and WebGPU

What is this? A real time strategy game/engine written with Rust and WebGPU. Eventually it will be able to run in a web browser thanks to WebGPU. This

Thomas SIMON 258 Dec 25, 2022
unrust - A pure rust based (webgl 2.0 / native) game engine

unrust A pure rust based (webgl 2.0 / native) game engine Current Version : 0.1.1 This project is under heavily development, all api are very unstable

null 368 Jan 3, 2023
Basic first-person fly camera for the Bevy game engine

bevy_flycam A basic first-person fly camera for Bevy 0.4 Controls WASD to move horizontally SPACE to ascend LSHIFT to descend ESC to grab/release curs

Spencer Burris 85 Dec 23, 2022
A no-frills Tetris implementation written in Rust with the Piston game engine, and Rodio for music.

rustris A no-frills Tetris implementation written in Rust with the Piston game engine, and Rodio for music. (C) 2020 Ben Cantrick. This code is distri

Ben Cantrick 17 Aug 18, 2022
Inspector plugin for the bevy game engine

bevy-inspector-egui This crate provides the ability to annotate structs with a #[derive(Inspectable)], which opens a debug interface using egui where

Jakob Hellermann 517 Dec 31, 2022
Crossterm plugin for the bevy game engine

What is bevy_crossterm? bevy_crossterm is a Bevy plugin that uses crossterm as a renderer. It provides custom components and events which allow users

null 79 Nov 2, 2022
Concise Reference Book for the Bevy Game Engine

Unofficial Bevy Cheat Book Click here to read the book! Concise reference to programming in the Bevy game engine. Covers useful syntax, features, prog

null 947 Jan 8, 2023