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. You need to define a key for each model, as this is how you query the model. However, you can create composite keys by defining multiple fields as keys.

#[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
        },
    )
);

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
    #[external(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