Skip to content

Models

Models = Data

TL;DR
  • 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