Skip to content

Dojo Framework Overview

Understanding ECS

Entity-Component-System (ECS) is a design pattern for building games and interactive applications. It was introduced by Looking Glass Studios with 1998's Thief: The Dark Project and quickly became a de-facto standard in game development.

ECS was developed to address some of the shortcomings of the then-dominant Object-Oriented Programming (OOP) paradigm, such as unweildly inheritance hierarchies and the difficulty of scaling interactions to large numbers of objects.

The core idea is to break down your application into three main components:

  • Entities: The objects in your game — characters, items, etc.
  • Components: The properties of your entities — position, durability, etc.
  • Systems: The rules that govern your game — movement, combat, etc.

With ECS, you assign components to entities, and operate on them using systems. This approach allows for a more modular and scalable design, as well as better performance and memory usage. Entities are typically represented as a unique identifier, to which components are assigned. Systems can then operate over large numbers of entities at once, depending on the components they have.

As an example, a combat system might reduce the durability of all weapons used in a battle, while a rest system might increase the health of all members of a party.

Dojo Core Concepts

The Dojo framework revolves around three core concepts:

  • Models: Models act like structured database entries, managing and organizing your onchain data — much like a regular ORM.
  • Contracts: Implement business logic and state changes, which are just like regular Cairo contracts, but with some syntax sugar.
  • World: The central hub that connects all models and systems. The World contract ensures consistency, manages authorization, and orchestrates interactions between different parts of your application.

Models

Dojo simplifies onchain application development by providing a simple and standardized approach to state management, similar to a regular database. It achieves this through the use of procedural macros that extend the Cairo compiler.

Under-the-hood, Cairo uses these macros to generate standardized events and functionality for each model. Once deployed, these events will be automatically picked up by the Torii indexer, letting you query them with gRPC or GraphQL. If you've never developed onchain applications before, this is A Big Deal™️.

Here is an example of a Dojo model:

#[derive(Copy, Drop, Serde)]
// Defines a Dojo model, used to generate standardized events
#[dojo::model]
pub struct Moves {
    // Defines a model's key, analagous to a primary key in an ORM
    #[key]
    pub player: ContractAddress,
    // The rest of the model's attributes
    pub remaining: u8,
    pub last_direction: Direction,
    pub can_move: bool,
}

Think of the model here as an entry into your onchain database, with #[key] being the primary key to reference it. You use this key to query your state onchain within contracts and off-chain in your clients. This allows you to use the same data model for your onchain and offchain applications.

Contracts

Once you have your state design, you want it to be effectively mutated via contracts. Dojo simplifies this by exposing an interface to interact with the world state, allowing fast queries and mutations.

// Defines a Dojo contract, used to generate standardized functionality
#[dojo::contract]
mod actions {
    use starknet::{ContractAddress, get_caller_address};
 
    // Import the model attributes like a normal Cairo struct
    use dojo_starter::models::{Moves, Position};
 
    #[abi(embed_v0)]
    impl ActionsImpl of super::IActions<ContractState> {
 
        // Passing in contract storage lets you mutate the world state
        fn spawn(ref self: ContractState) {
            // Get the world and current player key
            let mut world = self.world(@"greyhawk");
            let player = get_caller_address();
 
            // Cairo's typing tells us we are querying the player's position
            let mut position: Position = world.read_model(player);
 
            // Move the player 10 units up and to the right
            let new_position = Position {
                player, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 }
            };
 
            // Define the player's remaining moves
            let moves = Moves {
                player, remaining: 100, last_direction: Direction::None(()), can_move: true
            };
 
            // Write the new state to the world
            world.write_model(@new_position);
            world.write_model(@moves);
        }
    }
}

By having it's own Cairo compiler plugin, Dojo provides an api that transform into comprehensive queries at compile time.

World

Dojo models and contracts are the foundation of your application. We encapsulate them within the World contract, which serves as a container for all models, systems, and authorization management.

overview

Summary

  • Models: Utilize the #[dojo::model] macro to define structured data representing your application's state. Models function similarly to database entries, organizing and managing onchain data effectively.

  • Contracts: Implement business logic using the #[dojo::contract] macro. Contracts handle operations that modify models, ensuring seamless and reliable updates to your application's state.

  • World: The central orchestrator for all models and systems. The World contract maintains consistency, manages authorization, and coordinates interactions between different components, serving as the source of truth for your application.