Hello Dojo
This section assumes that you have already installed the Dojo toolchain and are familiar with Cairo. If not, please refer to the Getting Started section.
Dojo as an ECS in 15 Minutes
Although Dojo isn't exclusively an Entity Component System (ECS) framework, we recommend adopting this robust design pattern. In this context, systems shape the environment's logic, while components (models) mirror the state of the world. By taking this route, you'll benefit from a structured and modular framework that promises both flexibility and scalability in a continuously evolving world. If this seems a bit intricate at first, hang tight; we'll delve into the details shortly.
To start, let's set up a project to run locally on your machine. From an empty directory, execute:
sozo init
Congratulations! You now have a local Dojo project. This command creates a dojo-starter
project in your current directory. It's the ideal starting point for a new project and equips you with everything you need to begin.
Anatomy of a Dojo Project
Inspect the contents of the dojo-starter
project, and you'll notice the following structure (excluding the non-Cairo files):
src
- lib.cairo
- systems
- actions.cairo
- models
- position.cairo
- moves.cairo
- tests
- test_world.cairo
Scarb.toml
Dojo projects bear a strong resemblance to typical Cairo projects. The primary difference is the inclusion of a special attribute tag used to define your data models. In this context, we'll refer to these models as components.
As we're crafting an ECS, we'll adhere to the specific terminology associated with Entity Component Systems.
Open the src/models/moves.cairo
file to continue.
#[derive(Model, Drop, Serde)]
struct Moves {
#[key]
player: ContractAddress,
remaining: u8,
last_direction: Direction
}
...rest of code
Notice the #[derive(Model, Drop, Serde)]
attributes. For a model to be recognized, we must include Model
. This signals to the Dojo compiler that this struct should be treated as a model.
Our Moves
model houses a player
field. At the same time, we have the #[key]
attribute, it informs Dojo that this model is indexed by the player
field. If this is unfamiliar to you, we'll clarify its importance later in the chapter. Essentially, it implies that you can query this model using the player
field. Our Moves
model also contains the remaining
and last_direction
fields
Open the src/models/position.cairo
file to continue.
#[derive(Model, Copy, Drop, Serde)]
struct Position {
#[key]
player: ContractAddress,
vec: Vec2,
}
#[derive(Copy, Drop, Serde, Introspect)]
struct Vec2 {
x: u32,
y: u32
}
...rest of code
In a similar vein, we have a Position
model that have a Vec2 data structure. Vec holds x
and y
values. Once again, this model is indexed by the player
field.
Now, let's examine the src/systems/actions.cairo
file:
// define the interface
#[starknet::interface]
trait IActions<TContractState> {
fn spawn(self: @TContractState);
fn move(self: @TContractState, direction: dojo_starter::models::moves::Direction);
}
// dojo decorator
#[dojo::contract]
mod actions {
use super::IActions;
use starknet::{ContractAddress, get_caller_address};
use dojo_starter::models::{position::{Position, Vec2}, moves::{Moves, Direction}};
// declaring custom event struct
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Moved: Moved,
}
// declaring custom event struct
#[derive(Drop, starknet::Event)]
struct Moved {
player: ContractAddress,
direction: Direction
}
// define functions in your contracts like this:
fn next_position(mut position: Position, direction: Direction) -> Position {
match direction {
Direction::None => { return position; },
Direction::Left => { position.vec.x -= 1; },
Direction::Right => { position.vec.x += 1; },
Direction::Up => { position.vec.y -= 1; },
Direction::Down => { position.vec.y += 1; },
};
position
}
// impl: implement functions specified in trait
#[abi(embed_v0)]
impl ActionsImpl of IActions<ContractState> {
// ContractState is defined by system decorator expansion
fn spawn(self: @ContractState) {
// Access the world dispatcher for reading.
let world = self.world_dispatcher.read();
// Get the address of the current caller, possibly the player's address.
let player = get_caller_address();
// Retrieve the player's current position from the world.
let position = get!(world, player, (Position));
// Retrieve the player's move data, e.g., how many moves they have left.
let moves = get!(world, player, (Moves));
// Update the world state with the new data.
// 1. Set players moves to 10
// 2. Move the player's position 100 units in both the x and y direction.
set!(
world,
(
Moves { player, remaining: 100, last_direction: Direction::None },
Position { player, vec: Vec2 { x: 10, y: 10 } },
)
);
}
// Implementation of the move function for the ContractState struct.
fn move(self: @ContractState, direction: Direction) {
// Access the world dispatcher for reading.
let world = self.world_dispatcher.read();
// Get the address of the current caller, possibly the player's address.
let player = get_caller_address();
// Retrieve the player's current position and moves data from the world.
let (mut position, mut moves) = get!(world, player, (Position, Moves));
// Deduct one from the player's remaining moves.
moves.remaining -= 1;
// Update the last direction the player moved in.
moves.last_direction = direction;
// Calculate the player's next position based on the provided direction.
let next = next_position(position, direction);
// Update the world state with the new moves data and position.
set!(world, (moves, next));
// Emit an event to the world to notify about the player's move.
emit!(world, Moved { player, direction });
}
}
}
Breaking it down
System is a function in a contract
As you can see a System
is like a regular function of a dojo(starknet) contract. It imports the Models we defined earlier and exposes two functions spawn
and move
. These functions are called when a player spawns into the world and when they move respectively.
// Retrieve the player's current position from the world.
let position = get!(world, player, (Position));
// Retrieve the player's move data, e.g., how many moves they have left.
let moves = get!(world, player, (Moves));
Here we use get!
command to retrieve the Position
and Moves
model for the player
entity, which is the address of the caller.
Now the next line:
// Update the world state with the new data.
// 1. Increase the player's remaining moves by 10.
// 2. Move the player's position 10 units in both the x and y direction.
set!(
world,
(
Moves {
player, remaining: moves.remaining + 10, last_direction: Direction::None
},
Position {
player, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10}
},
)
);
Here we use the set!
command to set the Moves
and Position
models for the player
entity.
We covered a lot here in a short time. Let's recap:
- Explained the anatomy of a Dojo project
- Explained the importance of the
#[derive(Model)]
attribute - Explained the
spawn
andmove
functions - Explained the
Moves
andPosition
struct - Touched on the
get!
andset!
commands
Run it locally!
Now that we've covered some theory, let's build the Dojo project! In your primary terminal:
sozo build
That compiled the models and system into an artifact that can be deployed! Simple as that!
Now, let's deploy it to Katana! First, we need to get Katana running. Open a second terminal and execute:
katana --disable-fee
Success! Katana should now be running locally on your machine. Now, let's deploy! In your primary terminal, execute:
sozo migrate
This will deploy the artifact to Katana. You should see terminal output similar to this:
Migration account: 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973
World name: dojo_examples
[1] 🌎 Building World state....
> No remote World found
[2] 🧰 Evaluating Worlds diff....
> Total diffs found: 5
[3] 📦 Preparing for migration....
> Total items to be migrated (5): New 5 Update 0
# Executor
> Contract address: 0x59f31686991d7cac25a7d4844225b9647c89e3e1e2d03460dbc61e3fbfafc59
# Base Contract
> Class Hash: 0x77638e9a645209ac1e32e143bfdbfe9caf723c4f7645fcf465c38967545ea2f
# World
> Contract address: 0x5010c31f127114c6198df8a5239e2b7a5151e1156fb43791e37e7385faa8138
# Models (2)
Moves
> Class hash: 0x509a65bd8cc5516176a694a3b3c809011f1f0680959c567b3189e60ddab7ce1
Position
> Class hash: 0x52a1da1853c194683ca5d6d154452d0654d23f2eacd4267c555ff2338e144d6
> Registered at: 0x82d996aab290f086314745685c6f05bd69730d46589339763202de5264b1b6
# Contracts (1)
actions
> Contract address: 0x31571485922572446df9e3198a891e10d3a48e544544317dbcbb667e15848cd
🎉 Successfully migrated World at address 0x5010c31f127114c6198df8a5239e2b7a5151e1156fb43791e37e7385faa8138
✨ Updating manifest.json...
✨ Done.
Your 🌎 is now deployed at 0x5010c31f127114c6198df8a5239e2b7a5151e1156fb43791e37e7385faa8138
!
This establishes the world address for your project.
Let's discuss the Scarb.toml
file in the project. This file contains environment variables that make running CLI commands in your project a breeze (read more about it here). Make sure your file specifies the version of Dojo you have installed! In this case version 0.5.0
.
[dependencies]
dojo = { git = "https://github.com/dojoengine/dojo", version = "0.5.0" }
Indexing
With your local world address established, let's delve into indexing. You can index the entire world. To accomplish this we have to copy your world address
from the output of sozo migrate
. Now Open a new terminal and input this simple command that includes your own world address:
torii --world 0x5010c31f127114c6198df8a5239e2b7a5151e1156fb43791e37e7385faa8138
Running the command mentioned above starts a Torii server on your local machine. This server uses SQLite as its database and is accessible at http://0.0.0.0:8080/graphql. Torii will automatically organize your data into tables, making it easy for you to perform queries using GraphQL. When you run the command, you'll see terminal output that looks something like this:
2023-10-18T06:49:48.184233Z INFO torii::server: 🚀 Torii listening at http://0.0.0.0:8080
2023-10-18T06:49:48.184244Z INFO torii::server: Graphql playground: http://0.0.0.0:8080/graphql
2023-10-18T06:49:48.185648Z INFO torii_core::engine: processed block: 0
2023-10-18T06:49:48.186129Z INFO torii_core::engine: processed block: 1
2023-10-18T06:49:48.186720Z INFO torii_core::engine: processed block: 2
2023-10-18T06:49:48.187202Z INFO torii_core::engine: processed block: 3
2023-10-18T06:49:48.187674Z INFO torii_core::engine: processed block: 4
2023-10-18T06:49:48.188215Z INFO torii_core::engine: processed block: 5
2023-10-18T06:49:48.188611Z INFO torii_core::engine: processed block: 6
2023-10-18T06:49:48.188985Z INFO torii_core::engine: processed block: 7
2023-10-18T06:49:48.199592Z INFO torii_core::processors::register_model: Registered model: Moves
2023-10-18T06:49:48.210032Z INFO torii_core::processors::register_model: Registered model: Position
2023-10-18T06:49:48.210571Z INFO torii_core::engine: processed block: 8
2023-10-18T06:49:48.211678Z INFO torii_core::engine: processed block: 9
2023-10-18T06:49:48.212335Z INFO torii_core::engine: processed block: 10
You can observe that our Moves
and Position
models have been successfully registered.
Next, let's use the GraphiQL IDE to retrieve data from the Moves
model. In your web browser, navigate to http://0.0.0.0:8080/graphql
, and enter the following query:
query {
model(id: "Moves") {
id
name
classHash
transactionHash
createdAt
}
}
After you run the query, you will receive an output like this:
{
"data": {
"model": {
"id": "Moves",
"name": "Moves",
"classHash": "0x64495ca6dc1dc328972697b30468cea364bcb7452bbb6e4aaad3e4b3f190147",
"transactionHash": "",
"createdAt": "2023-12-15 18:07:22"
}
}
}
Awesome, now let's work with subscriptions to get real-time updates. Let's clean up your workspace on the GraphiQL IDE and input the following subscription:
subscription {
entityUpdated {
id
keys
eventId
createdAt
updatedAt
}
}
Once you execute the subscription, you will receive notifications whenever new entities are updated or created. For now, don't make any changes to it and proceed to create a new entity.
To accomplish this, we have to go back to our primary terminal and check the contracts section.
# Contracts (1)
actions
> Contract address: 0x31571485922572446df9e3198a891e10d3a48e544544317dbcbb667e15848cd
We have to use actions
contract address to start to create entities. In your main local terminal, run the following command:
sozo execute 0x31571485922572446df9e3198a891e10d3a48e544544317dbcbb667e15848cd spawn
By running this command, you've activated the spawn system, resulting in the creation of a new entity. This action establishes a local world that you can interact with.
Now, go back to your GraphiQL IDE, and you will notice that you have received the subscription's results, which should look something like this:
{
"data": {
"entityUpdated": {
"id": "0x28cd7ee02d7f6ec9810e75b930e8e607793b302445abbdee0ac88143f18da20",
"keys": [
"0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
],
"eventId": "0x000000000000000000000000000000000000000000000000000000000000000e:0x0000:0x0000",
"createdAt": "2023-12-15 18:07:22",
"updatedAt": "2023-12-15 18:10:56"
}
}
}
--------------------------------------------------------------------------------------------------------
{
"data": {
"entityUpdated": {
"id": "0x28cd7ee02d7f6ec9810e75b930e8e607793b302445abbdee0ac88143f18da20",
"keys": [
"0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
],
"eventId": "0x000000000000000000000000000000000000000000000000000000000000000e:0x0000:0x0001",
"createdAt": "2023-12-15 18:07:22",
"updatedAt": "2023-12-15 18:10:56"
}
}
}
In the GraphiQL IDE, by clicking the DOCS
-button on the right, you can open the API documentation. This documentation is auto-generated based on our schema definition and displays all API operations and data types of our schema.. In order to know more about query and subscription, you can jump to GraphQL section.
We've covered quite a bit! Here's a recap:
- Built a Dojo world
- Deployed the project to Katana
- Indexed the world with Torii
- Ran the spawn system locally
- Interacted with GraphQL
Next Steps
This overview provides a rapid end-to-end glimpse into Dojo. However, the potential of these worlds is vast! Designed to manage hundreds of systems and components, Dojo is equipped for expansive creativity. So, what will you craft next?