Models
TL;DRModels = Data
- Models store structured data in your world.
- Models are Cairo structs with additional features.
- Models can implement traits.
- Use the
#[derive(Model)]
decorator to define them. - Custom enums and types are supported.
- Define the primary key using the
#[key]
attribute.
Models are Structs
Models are structs annotated with the #[derive(Model)]
attribute. Consider these models as a key-value store, where the #[key]
attribute is utilized to define the primary key. While models can contain any number of fields, adhering to best practices in Entity-Component-System (ECS) design involves maintaining small, isolated models.
This approach fosters modularity and composability, enabling you to reuse models across various entity types.
#[derive(Model, Copy, Drop, Serde)]
struct Moves {
#[key]
player: ContractAddress,
remaining: u8,
}
The #[key] attribute
The #[key]
attribute indicates to Dojo that this model is indexed by the player
field. A field that is identified as a #[key]
is not stored. It is used by the dojo database system to uniquely identify the storage location that contains your model.
You need to define at least one key for each model, as this is how you query the model. However, you can create composite keys by defining multiple fields as keys. If you define multiple keys, they must all be provided to query the model.
#[derive(Model, Copy, Drop, Serde)]
struct Resource {
#[key]
player: ContractAddress,
#[key]
location: ContractAddress,
balance: u8,
}
In this case you then would set the model with both the player and location fields:
set!(
world,
(
Resource {
player: caller,
location: 12,
balance: 10
},
)
);
To retrieve a model with a composite key using the get! command, you must provide a value for each key as follow:
let player = get_caller_address();
let location = 0x1234;
let resource = get!(world, (player, location), (Resource));
Implementing Traits
Models can implement traits. This is useful for defining common functionality across models. For example, you may want to define a Position
model that implements a PositionTrait
trait. This trait could define functions such as is_zero
and is_equal
which could be used when accessing the model.
trait PositionTrait {
fn is_zero(self: Position) -> bool;
fn is_equal(self: Position, b: Position) -> bool;
}
impl PositionImpl of PositionTrait {
fn is_zero(self: Position) -> bool {
if self.x - self.y == 0 {
return true;
}
false
}
fn is_equal(self: Position, b: Position) -> bool {
self.x == b.x && self.y == b.y
}
}
Custom Setting models
Suppose we need a place to keep a global value with the flexibility to modify it in the future. Take, for instance, a global combat_cool_down
parameter that defines the duration required for an entity to be primed for another attack. To achieve this, we can craft a model dedicated to storing this value, while also allowing for its modification via a decentralized governance model.
To establish these models, you'd follow the usual creation method. However, when initializing them, employ a constant identifier, such as GAME_SETTINGS_ID.
const GAME_SETTINGS_ID: u32 = 9999999999999;
#[derive(model, Copy, Drop, Serde)]
struct GameSettings {
#[key]
game_settings_id: u32,
combat_cool_down: u32,
}
Types
Support model types:
u8
u16
u32
u64
u128
u256
ContractAddress
- Enums
- Custom Types
It is currently not possible to use Arrays.
Custom Types + Enums
For models containing complex types, it's crucial to implement the SchemaIntrospection
trait.
Consider the model below:
struct Card {
#[key]
token_id: u256,
/// The card's designated role.
role: Roles,
}
For complex types, like Roles
in the above example, you need to implement SchemaIntrospection
. Here's how:
impl RolesSchemaIntrospectionImpl for SchemaIntrospection<Roles> {
#[inline(always)]
fn size() -> usize {
1 // Represents the byte size of the enum.
}
#[inline(always)]
fn layout(ref layout: Array<u8>) {
layout.append(8); // Specifies the layout byte size;
}
#[inline(always)]
fn ty() -> Ty {
Ty::Enum(
Enum {
name: 'Roles',
attrs: array![].span(),
children: array![
('Goalkeeper', serialize_member_type(@Ty::Tuple(array![].span()))),
('Defender', serialize_member_type(@Ty::Tuple(array![].span()))),
('Midfielder', serialize_member_type(@Ty::Tuple(array![].span()))),
('Attacker', serialize_member_type(@Ty::Tuple(array![].span()))),
]
.span()
}
)
}
}
In practice with modularity in mind
Consider a tangible analogy: Humans and Goblins. While they possess intrinsic differences, they share common traits, such as having a position and health. However, humans possess an additional model. Furthermore, we introduce a Counter model, a distinct feature that tallies the numbers of humans and goblins.
#[derive(Model, Copy, Drop, Serde)]
struct Potions {
#[key]
entity_id: u32,
quantity: u8,
}
#[derive(Model, Copy, Drop, Serde)]
struct Health {
#[key]
entity_id: u32,
health: u8,
}
#[derive(Model, Copy, Drop, Serde)]
struct Position {
#[key]
entity_id: u32,
x: u32,
y: u32
}
// Special counter model
#[derive(Model, Copy, Drop, Serde)]
struct Counter {
#[key]
counter: u32,
goblin_count: u32,
human_count: u32,
}
So the Human will have a Potions
, Health
and Position
model, and the Goblin will have a Health
and Position
model. By doing we save having to create Health and Position models for each entity type.
So then a contract would look like this:
#[dojo::contract]
mod spawnHuman {
use array::ArrayTrait;
use box::BoxTrait;
use traits::Into;
use dojo::world::Context;
use dojo_examples::models::Position;
use dojo_examples::models::Health;
use dojo_examples::models::Potions;
use dojo_examples::models::Counter;
// we can set the counter value as a const, then query it easily! This pattern is useful for settings.
const COUNTER_ID: u32 = 9999999999999;
// impl: implement functions specified in trait
#[abi(embed_v0)]
impl GoblinActionsImpl of IGoblinActions<ContractState> {
fn goblin_actions(self: @ContractState, entity_id: u32) {
let world = self.world_dispatcher.read();
let counter = get!(world, COUNTER_ID, (Counter));
let human_count = counter.human_count + 1;
let goblin_count = counter.goblin_count + 1;
// spawn a human
set!(
world,
(
Health {
entity_id: human_count, health: 100
},
Position {
entity_id: human_count, x: position.x + 10, y: position.y + 10,
},
Potions {
entity_id: human_count, quantity: 10
},
)
);
// spawn a goblin
set!(
world,
(
Health {
entity_id: goblin_count, health: 100
},
Position {
entity_id: goblin_count, x: position.x + 10, y: position.y + 10,
},
)
);
// increment the counter
set!(
world,
(
Counter {
counter: COUNTER_ID, human_count: human_count, goblin_count: goblin_count
},
)
);
}
}
}
A complete example can be found in the Dojo Starter