Enter the Dojo — An introduction to your first game onchain

Extropy.IO
10 min readOct 5, 2023

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:

  1. A good storytelling
  2. 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…
  3. Enigmatic Puzzles
  4. 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?…
  5. Timer
  6. 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:

  1. 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,
}
  1. 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,
}
  1. 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,
}
  1. 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 level
  • interact - a system that handles interactions with objects
  • escape - a system that attempts to complete the challenge.

To accomplish this, go to your systems.cairo file and include the following code:

  1. 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. interactsystem

#[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. escapesystem

#[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 the interact system.
  • 0 is the game_id
  • 0x5061696e74696e67 is the hex conversion of the string Painting
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/

--

--

Extropy.IO

Oxford-based blockchain and zero knowledge consultancy and auditing firm