Enter the Dojo — An introduction to your first game onchain
Welcome to the world of game development with Dojo! In this tutorial, we will embark on an exciting journey to create a Room Escape game from scratch.
Before diving into the nitty-gritty of game development, we’ll guide you through the installation of these tools and setting up your project environment. With everything in place, we’ll explore the game’s architecture and design, ensuring we have a solid foundation before we begin coding.
A Room Escape game involves storytelling, enigmatic puzzles, and a ticking timer, creating an immersive and challenging experience for players. As we progress, we’ll define the game’s components, systems, and events that drive the gameplay.
Once our game structure is solidified, we’ll deploy it on the local machine using Katana, a powerful Dojo tool. With the game up and running, we’ll demonstrate how to interact with it, from initialising the game to attempting the escape.
Are you ready to embark on this adventure of game development with Dojo? Let’s get started!
Getting started:
In this tutorial you will need two things namely Scarb and Dojo.
Scarb installation
Follow the installation steps from here
It is recommended to install it via `asdf` as this will help us switch seamlessly to a upgrade/downgrade `scarb` whenever we need.
For now, please install `scarb v0.6.0` in order to declare and deploy contracts on Starknet.
asdf plugin add scarb https://github.com/software-mansion/asdf-scarb.git
asdf install scarb latest
asdf global scarb latest
asdf reshim scarb
Once you have completed the installation, we can check if it was successful. Run the following command:
scarb — version
>>
scarb 0.7.0 (58cc88efb 2023–08–23)
cairo: 2.2.0 (https://crates.io/crates/cairo-lang-compiler/2.2.0)
sierra: 1.3.0
You should receive the version of Scarb along with the version of Cairo.
Note: A good place to check which Cairo version is currently supported on Starknet is the [Version notes](https://docs.starknet.io/documentation/starknet_versions/version_notes/).
Dojo
Go to the Dojo Book — Quick Start page and follow the guide to install dojo. In this tutorial we will be working with dojo v0.2.2
❯ sozo --version
sozo 0.2.1
Initialising the project
Let create our folder as dojo_examples
and then initialise the folder with scarb init
.
mkdir dojo_examples
cd dojo_examples
scarb init
Now that we have initialised our project, the folder looks like this:
.
├── Scarb.toml
└── src
└── lib.cairo
In the src
folder create the following files:
└── src
└── systems.cairo
└── components.cairo
└── events.cairo
Now, in the lib.cairo
file, remove the boilerplate code and import the Cairo files.
mod systems;
mod components;
mod events;
Now that our project structure is setup, let’s update the Scarb.toml
with the necessary dependencies.
[package]
cairo-version = "2.2.0"
name = "dojo_examples"
version = "0.1.0"
[cairo]
sierra-replace-ids = true
[dependencies]
dojo = { git = "https://github.com/dojoengine/dojo", tag="v0.2.2"}
[[target.dojo]]
[tool.dojo]
initializer_class_hash = "0xbeef"
[tool.dojo.env]
rpc_url = "http://localhost:5050/"
# Default account for katana with seed = 0
account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
private_key = "0x1800000000300000180000000000030000000000003006001800006600"
Note: You can also initialize your project with sozo init
which will create a project template with an example based on the Dojo Starter Template.
Building the game
Game architecture
Before we start getting into the coding part, let’s analyse our game design. As a game designer, you need to understand what designs and concepts you need to implement.
For a good Room Escape game there are three things that are important:
- A good storytelling
- Everything begins with a captivating story. Let’s envision ourselves in a dream within an oddly shaped, dimly lit room. Inside this room, various objects are scattered about, and there is an enigmatic door with an unusual appearance. You approach the door and attempt to open it, but it’s securely locked. You notice beside it a keypad with both numbers and letters…
- Enigmatic Puzzles
- Mounted on the room’s wall is a painting. As you draw nearer to the artwork, you discern an oddly shaped banana adorned with peculiar symbols. Is this what they refer to as modern art?…
- Timer
- Every action you take consumes precious minutes. You see, this room has limited oxygen. You likely have only around 60 minutes left to find a way out.
Now that we get an idea what the game looks like, it’s time for implementation!
Implementation
Components
First, we need to define components within your game world. A component identifies an entity as having a specific characteristic and stores the necessary data to represent that characteristic. For instance, a component could represent an object, such as a Chair
or Table
.
To accomplish this, go to your components.cairo
file and include the following code:
- Game Component
First we need to add the Game component. This struct will store information on the current state of the game such as: — game_id
- tells us the current id of the game - start_time
- store the time when the game started - turns_remaining
- the time limit for the player to escape - is_finished
- if the player finished the game or not - creator
- the player address which created the game
use starknet::ContractAddress;
#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Game {
#[key]
game_id: u32,
start_time: u64,
turns_remaining: usize,
is_finished: bool,
creator: ContractAddress,
}
- Object Component
This is the component we will use to create various objects in the room. The variables represent the following: — game_id
- similar to the game_id
variable from the Game
component - player_id
- similar to the creator
variable from the Game
component - object_id
- the name of the object that we will create such as Painting
, Book
, etc… - description
- a small description about the object, or who knows maybe a secret
#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Object{
#[key]
game_id: u32,
#[key]
player_id: ContractAddress,
#[key]
object_id: felt252,
description: felt252,
}
- Door Component
This component will represent the door which we need to open. This one is special as it has a secret attached to it. — secret
- represents the secret phrase that the player must discover
#[derive(Component, Copy, Drop, Serde, SerdeLen)]
struct Door{
#[key]
game_id: u32,
#[key]
player_id: ContractAddress,
secret: felt252,
}
- Ticking Function
Last element to add to our file is the ticking function. This will help our game tick for every time we interact with the game.
#[generate_trait]
impl GameImpl of GameTrait {
#[inline(always)]
fn tick(self: Game) -> bool {
let info = starknet::get_block_info().unbox();
if info.block_timestamp < self.start_time {
return false;
}
if self.is_finished {
return false;
}
true
}
}
Events
Events can be added in the game when a certain action needs to be emitted on the block explorer. Such cases can be when the game has been successfully initialized, when the player has successfully escaped the room or even when interacting with certain objects in the room. To do this, go to the event.cairo
file and add the following code:
ObjectData
- will be used to emit information when the player interacts with a certain object
GameState
- will be used to update the player about the state of the game, such as the game has been initialized or if the player has successfully escaped.
#[derive(Drop, Clone, Serde, PartialEq, starknet::Event)]
struct ObjectData {
object_id: felt252,
description: felt252,
}
#[derive(Drop, Clone, Serde, PartialEq, starknet::Event)]
struct GameState {
game_state: felt252,
}
#[derive(Drop, Clone, Serde, PartialEq, starknet::Event)]
enum Event{
ObjectData: ObjectData,
GameState: GameState,
}
Systems
Now that you have defined the components, it’s time to define the systems within your game world. A system is a process that operates on all entities with the specified components. For example, in our game, you need to define the following:
create
- a system that generates the game levelinteract
- a system that handles interactions with objectsescape
- a system that attempts to complete the challenge.
To accomplish this, go to your systems.cairo
file and include the following code:
create
system
#[system]
mod create {
use array::ArrayTrait;
use box::BoxTrait;
use traits::Into;
use dojo::world::Context;
use starknet::{ContractAddress};
use dojo_examples::components::{Door};
use dojo_examples::components::{Object, ObjectTrait};
use dojo_examples::components::{Game, GameTrait};
#[event]
use dojo_examples::events::{Event, ObjectData, GameState};
fn execute(ctx: Context, start_time: u64, turns_remaining: usize,) {
let game_id = ctx.world.uuid();
let game = Game {
game_id,
start_time,
turns_remaining,
is_finished: false,
creator: ctx.origin,
};
let door = Door {
game_id,
player_id: ctx.origin,
secret: '1984',
};
set !(ctx.world, (game, door));
set !(
ctx.world,
(
Object {
game_id:game_id, player_id: ctx.origin, object_id: 'Painting', description: 'An intriguing painting.',
},
Object {
game_id:game_id, player_id: ctx.origin, object_id: 'Foto', description: 'An egyptian cat.',
},
Object {
game_id:game_id, player_id: ctx.origin, object_id: 'Strange Amulet', description: 'Until tomorrow',
},
Object {
game_id:game_id, player_id: ctx.origin, object_id: 'Book', description: '1984',
},
)
);
emit!(ctx.world, GameState { game_state: 'Game Initialized'});
return ();
}
}
2. interact
system
#[system]
mod interact {
use array::ArrayTrait;
use box::BoxTrait;
use traits::Into;
use dojo::world::Context;
use dojo_examples::components::{Object, ObjectTrait};
use dojo_examples::components::{Game, GameTrait};
#[event]
use dojo_examples::events::{Event, ObjectData, GameState};
fn execute(ctx: Context, game_id: u32, object_id: felt252) {
let player_id = ctx.origin;
let mut game = get!(ctx.world, game_id, (Game));
assert(game.tick(), 'Cannot Progress');
if game.turns_remaining == 0 {
emit!(ctx.world, GameState {game_state: 'Game Over'});
return ();
} else {
game.turns_remaining -= 1;
}
let object = get! (ctx.world, (game_id, player_id, object_id).into(), Object);
set!(ctx.world, (game, ));
// emit item data
emit!(ctx.world, GameState { game_state: 'Checking Item'});
emit!(ctx.world, ObjectData { object_id: object.object_id, description: object.description });
}
}
3. escape
system
#[system]
mod escape {
use array::ArrayTrait;
use box::BoxTrait;
use traits::Into;
use dojo::world::Context;
use dojo_examples::components::{Door};
use dojo_examples::components::{Object, ObjectTrait};
use dojo_examples::components::{Game, GameTrait};
#[event]
use dojo_examples::events::{Event, ObjectData, GameState};
fn execute(ctx: Context, game_id: u32, secret: felt252) {
let player_id = ctx.origin;
let mut game = get!(ctx.world, game_id, Game);
assert(game.tick(), 'Cannot Progress');
if game.turns_remaining == 0 {
emit!(ctx.world, GameState {game_state: 'Game Over'});
return ();
} else {
game.turns_remaining -= 1;
}
let door = get! (ctx.world, (game_id, player_id).into(), Door);
if door.secret == secret {
game.is_finished = true;
set !(ctx.world, (game, ));
emit!(ctx.world, GameState {game_state: 'Escaped'});
}
set !(ctx.world, (game, ));
emit!(ctx.world, GameState {game_state: 'Wrong Secret'});
}
}
Deploying the game
Now that we have everything ready, it is time to build and deploy our game on the local machine.
Open a terminal and start running Katana with the following command:
katana --disable-fee
Open another terminal and build your project with the following command:
sozo build
Once everything has been compiled, it’s time to deploy the game on Katana. Execute the following command, where room_escape
is the name of the world.
sozo migrate --name room_escape
>...
>🎉 Successfully migrated World on block #4 at address 0x84486b8e9ffe38978b33c9d7685d9d2d487d0e9f096a1d2669edefc8506c35
Afterwards, you should receive the address of the deployed world in your terminal. To simplify interactions with our world, copy this world address and append it to your Scarb.toml
file at the end.
world_address = "0x84486b8e9ffe38978b33c9d7685d9d2d487d0e9f096a1d2669edefc8506c35"
Interacting with the game
Now that we have successfully deployed the world, it is time to create the game. To do, we have the create
system which will initialize and create all the necessary components for the game.
To do this we need to use the execute
command from sozo
which looks like this:
sozo execute <SYSTEM> [OPTIONS]
In our case, we have:
sozo execute create --calldata 16565656, 10
Because we already added the world address in the Scarb.toml
, sozo will automatically pick that up.
To check if everything has been created you can use Torri which is an automatic indexer for dojo worlds.
Yet in another terminal, run Torri with the following command:
torri
Once done, you can go to http://0.0.0.0:8080/
and interact with your world with GraphQL.
To get all the entities in our room_escape
world run the following query in the browser:
query getEntities {
entities(keys: ["%"]) {
edges {
node {
keys
components {
__typename
}
}
}
}
}
The output should be all your entities within the created dojo world.
Now we can interact with our game by running the following command:
- we call the
interact
system that we created - we pass the –calldata argument and followed by the necessary values that the
execute()
functions takes within theinteract
system. 0
is thegame_id
0x5061696e74696e67
is the hex conversion of the stringPainting
sozo execute interact --calldata 0, 0x5061696e74696e67
After that we should be able to see the description of the Painting
object by checking our emitted events within the world:
sozo events --chunk-size 30
This will output the emitted events and we can see our object within the data
field that we have emitted:
{
"from_address": "0x444f65a9a7f32ef32b38af2acdc68de8f5dfd100cc2c4dfac8af436c55afc7b",
"keys": [
"0xc9e23d20b5094255ea48711b9bd2dbfc51876481a613661c6763305ac830a0",
"0x696e746572616374"
],
"data": [
"0x426f6f6b",
"0x31393834"
],
"block_hash": "0x28a267cdc5369a40356d88960c0bd0f77c77179e531fa8b15f3bb410471d190",
"block_number": 17,
"transaction_hash": "0x755e06c79026142a9bce86dfd3638bbb5c24d374b72aae40b7b6f76e0abb0f9"
}
Now all we need to do is figure out what could the secret to the door be by interacting with the items within the room. Once we have figured it out, we can head to the door and execute the following command to escape from the room:
sozo execute escape --calldata 0, 0x5061696e74696e67
In the --calldata
we have provide the game_id
and the secret
to escape. If the secret was correct, then we should notice that Escaped
has been emitted, otherwise Wrong Secret
.
And if we haven’t managed to solve the puzzle within 10
moves, which was predefined when we executed the create
system, then the emitted event will be Game Over
.
Wrapping up
In this tutorial, we have covered the process of utilizing the Dojo engine alongside tools like Katana, Torii, and Sozo.
We’ve taken you through the creation of a Room Escape game from the ground up, incorporating the ECS design architecture.
We’ve developed essential components such as Objects
and Doors
, while also crafting systems like create
, interact
, and escape
.Furthermore, we’ve introduced event handling and the seamless emission of events as the user engages with the game or successfully escapes the room.
Lastly, we’ve demonstrated how to deploy the game locally using Katana and interacted with it.
In the next article, we will explore how to setup a game UI with Bevy and connect it with our contract. Stay tuned!
Further reading and resources
Interested in other game examples?
Roll Your Own — https://twitter.com/tarrenceva/status/1704671871461969986
Beer Baron — https://github.com/ponderingdemocritus/beer-baron
zknight — https://github.com/z-korp/zknight
Other Resources
Dojo website: https://dojoengine.org/
Dojo Book: https://book.dojoengine.org/framework/sozo/overview.html
Dojo Notion: https://dojoengine.notion.site/Dojo-Engine-Community-Hub-d316194b998941c48ddf771a4dd5ff08
Dojo Github: https://github.com/dojoengine/