Engine
Docs
2. Map and terrain

2. Map and terrain

In this section, we will accomplish the following:

  1. Configure the map as a singleton schema and initialize it in the client.
  2. Add terrain (tall grass and boulders) to the map.
  3. Prevent movement with boulders.

2.1. Use singleton schemas for the map config

At this point we have the concept of a 2D grid but there is no official "map" and there is no terrain. In order to do so in the ECS model we will now implement the schemas as a singleton schema and initialize it in the client. Put simply, singleton schemas are schemas with a single record. They don’t have keys and are quite useful to store top-level state.

Go ahead and add the map as a singleton schema in the Obelisk config (obelisk.config.ts).

obelisk.config.ts
import { ObeliskConfig } from '@0xobelisk/common';
 
export default obeliskConfig = {
    name: 'constantinople',
    description: 'constantinople',
    systems: ['map_system', 'encounter_system'],
    schemas: {
        map: {
            valueType: {
                width: 'u64',
                height: 'u64',
                terrain: 'vector<vector<u8>>',
            },
            defaultValue: {
                width: 32,
                height: 27,
                terrain: [
                    [0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80,80],
                    [0, 0, 0, 0, 0, 0, 81, 81, 81, 81, 81, 81, 81, 0, 0, 0, 135, 136, 137, 138, 139, 0, 0, 81, 81, 81, 81, 81,81, 81, 81, 81],
                    [0, 0, 0, 0, 0, 0, 22, 22, 0, 20, 20, 20, 20, 20, 20, 0, 140, 141, 142, 143, 144, 0, 0, 0, 0, 0, 0, 0, 0, 080, 80],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 145, 146, 147, 148, 149, 0, 0, 0, 0, 83, 83, 22, 0, 0,81, 81],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 150, 151, 152, 153, 154, 0, 0, 0, 0, 83, 83, 22, 0, 0,80, 80,],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 155, 156, 157, 158, 159, 161, 0, 0, 0, 0, 0, 0, 0, 0,81, 81],
                    [83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [80, 80, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 22, 0, 0, 0, 0, 100, 101, 102, 103, 104, 105, 106,0, 0, 81, 81],
                    [81, 81, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 107, 108, 109, 110, 111, 112, 113, 0, 0, 80, 80],
                    [80, 80, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 115, 116, 117, 118, 119, 120, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 30, 31, 32, 83, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 121, 122, 123, 124, 125, 126, 127, 0, 0, 80, 80],
                    [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 128, 129, 130, 131, 132, 133, 134, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 22, 72, 22, 0, 0, 0, 80, 0, 22, 22, 22, 161, 23, 0, 0, 0, 0, 80, 80 ],
                    [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 22, 74, 73, 75, 22, 0, 0, 81, 0, 22, 22, 22, 0, 0, 0, 0, 0, 0, 81, 81],
                    [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 83, 20, 30, 31, 32, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 83 ],
                    [80, 80, 83, 0, 0, 0, 83, 33, 34, 34, 31, 31, 31, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,31, 31, 32, 83],
                    [81, 81, 83, 0, 0, 0, 83, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 43],
                    [83, 83, 83, 0, 0, 0, 83, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 34, 34, 34, 37, 37, 37, 37, 37, 37, 38, 83],
                    [22, 83, 0, 0, 0, 0, 83, 83, 83, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 33, 34, 35, 20, 0, 0, 0, 0, 0, 83, 83],
                    [22, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 20, 36, 37, 38, 20, 80, 0, 0, 0, 0, 80, 80],
                    [22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 60, 61, 62, 0, 81, 0, 0, 0, 0, 81, 81],
                    [80, 80, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 0, 63, 64, 65, 0, 80, 0, 0, 22, 0, 80, 80],
                    [81, 81, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 66, 67, 68, 0, 81, 0, 0, 22, 0, 81, 81],
                    [80, 80, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [81, 81, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 80, 0, 80, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81, 81],
                    [80, 80, 22, 0, 0, 0, 0, 0, 83, 83, 0, 0, 81, 0, 81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                    [81, 81, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81,81],
                ],
            },
        },
        movable: "bool",
        player: "bool",
        position: {
            valueType: {
                x: "u64",
                y: "u64",
            },
        },
}
} as ObeliskConfig;

2.2. Add terrain

There are two features we have yet to implement—boulders to obstruct movement and tall grass to generate encounters. Before we start setting up the components and systems necessary to do so we must first add the terrain itself and render them in the client.

First, we’ll use the script/deploy_hook.move to initialize the client with the terrain.

deploy_hook.move
module constantinople::deploy_hook {
    use constantinople::world::{World, AdminCap, get_admin};
    use sui::object;
    use constantinople::map_schema;
    use std::vector;
    use constantinople::entity_key;
    use constantinople::encounter_trigger_schema;
    use constantinople::obstruction_schema;
    use constantinople::position_schema;
 
    /// Not the right admin for this world
    const ENotAdmin: u64 = 0;
 
    public entry fun run(world: &mut World, admin_cap: &AdminCap) {
        assert!( get_admin(world) == object::id(admin_cap), ENotAdmin);
 
        // Logic that needs to be automated once the contract is deployed
        let (width, height, terrain) = map_schema::get(world);
 
        let y = 0;
        while (y < height) {
            let x = 0;
            while (x < width) {
                let value = *vector::borrow(vector::borrow(&terrain, y), x);
                let entity = entity_key::from_position(x, y);
                // TODO:
 
                x = x + 1;
            };
            y = y + 1;
        };
    }
 
    #[test_only]
    public fun deploy_hook_for_testing(world: &mut World, admin_cap: &AdminCap){
        run(world, admin_cap)
    }
}

2.3. Turn boulders into obstructions

Although boulders are now rendering on the map at this point, they do not yet prevent movement in the way we want them to. To accomplish this we will add an obstruction schema and query for entities with that schema in our move method.

Let's start by adding the schema to the Obelisk config:

obelisk.config.ts
import { ObeliskConfig } from '@0xobelisk/common';
 
export default obeliskConfig = {
    name: 'constantinople',
    description: 'constantinople',
    systems: ['map_system', 'encounter_system'],
    schemas: {
        map: {
            valueType: {
                width: 'u64',
                height: 'u64',
                terrain: 'vector<vector<u8>>',
            },
            defaultValue: {
                width: 32,
                height: 27,
                terrain: [
                [0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80,80],
                [0, 0, 0, 0, 0, 0, 81, 81, 81, 81, 81, 81, 81, 0, 0, 0, 135, 136, 137, 138, 139, 0, 0, 81, 81, 81, 81, 81,81, 81, 81, 81],
                [0, 0, 0, 0, 0, 0, 22, 22, 0, 20, 20, 20, 20, 20, 20, 0, 140, 141, 142, 143, 144, 0, 0, 0, 0, 0, 0, 0, 0, 080, 80],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 145, 146, 147, 148, 149, 0, 0, 0, 0, 83, 83, 22, 0, 0,81, 81],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 150, 151, 152, 153, 154, 0, 0, 0, 0, 83, 83, 22, 0, 0,80, 80,],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 20, 22, 22, 20, 20, 0, 155, 156, 157, 158, 159, 161, 0, 0, 0, 0, 0, 0, 0, 0,81, 81],
                [83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [80, 80, 83, 0, 0, 0, 0, 0, 0, 20, 20, 20, 20, 20, 20, 0, 22, 0, 0, 0, 0, 100, 101, 102, 103, 104, 105, 106,0, 0, 81, 81],
                [81, 81, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 107, 108, 109, 110, 111, 112, 113, 0, 0, 80, 80],
                [80, 80, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 115, 116, 117, 118, 119, 120, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 30, 31, 32, 83, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0, 121, 122, 123, 124, 125, 126, 127, 0, 0, 80, 80],
                [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 0, 71, 0, 0, 0, 0, 0, 0, 128, 129, 130, 131, 132, 133, 134, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 0, 22, 72, 22, 0, 0, 0, 80, 0, 22, 22, 22, 161, 23, 0, 0, 0, 0, 80, 80 ],
                [80, 80, 22, 0, 0, 0, 83, 33, 34, 35, 83, 0, 22, 74, 73, 75, 22, 0, 0, 81, 0, 22, 22, 22, 0, 0, 0, 0, 0, 0, 81, 81],
                [81, 81, 22, 0, 0, 0, 83, 33, 34, 35, 83, 83, 20, 30, 31, 32, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83, 83 ],
                [80, 80, 83, 0, 0, 0, 83, 33, 34, 34, 31, 31, 31, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,31, 31, 32, 83],
                [81, 81, 83, 0, 0, 0, 83, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 43],
                [83, 83, 83, 0, 0, 0, 83, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 34, 34, 34, 37, 37, 37, 37, 37, 37, 38, 83],
                [22, 83, 0, 0, 0, 0, 83, 83, 83, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 20, 33, 34, 35, 20, 0, 0, 0, 0, 0, 83, 83],
                [22, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 20, 36, 37, 38, 20, 80, 0, 0, 0, 0, 80, 80],
                [22, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 60, 61, 62, 0, 81, 0, 0, 0, 0, 81, 81],
                [80, 80, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 0, 63, 64, 65, 0, 80, 0, 0, 22, 0, 80, 80],
                [81, 81, 22, 0, 20, 20, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 81, 0, 66, 67, 68, 0, 81, 0, 0, 22, 0, 81, 81],
                [80, 80, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [81, 81, 22, 0, 20, 20, 20, 0, 83, 83, 0, 0, 80, 0, 80, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81, 81],
                [80, 80, 22, 0, 0, 0, 0, 0, 83, 83, 0, 0, 81, 0, 81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80],
                [81, 81, 83, 83, 83, 83, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22, 22, 22, 22, 0, 0, 0, 0, 0, 81,81],
                ],
            },
        },
        obstruction: 'bool',
        movable: "bool",
        player: "bool",
        position: {
            valueType: {
                x: "u64",
                y: "u64",
            },
        },
}
} as ObeliskConfig;

We'll then make sure deploy_hook.move initializes the boulders properly (with the obstruction and position component) so we can query them later.

deploy_hook.move
module constantinople::deploy_hook {
    use constantinople::world::{World, AdminCap, get_admin};
    use sui::object;
    use constantinople::map_schema;
    use std::vector;
    use constantinople::entity_key;
    use constantinople::encounter_trigger_schema;
    use constantinople::obstruction_schema;
    use constantinople::position_schema;

    /// Not the right admin for this world
    const ENotAdmin: u64 = 0;

    public entry fun run(world: &mut World, admin_cap: &AdminCap) {
        assert!( get_admin(world) == object::id(admin_cap), ENotAdmin);

        // Logic that needs to be automated once the contract is deployed
        let (width, height, terrain) = map_schema::get(world);

        let y = 0;
        while (y < height) {
            let x = 0;
            while (x < width) {
                let value = *vector::borrow(vector::borrow(&terrain, y), x);
                let entity = entity_key::from_position(x, y);
                if (value >= 40) {
                    position_schema::set(world, entity, x, y);
                    obstruction_schema::set(world, entity, true);
                };
                x = x + 1;
            };
            y = y + 1;
        };
    }

    #[test_only]
    public fun deploy_hook_for_testing(world: &mut World, admin_cap: &AdminCap){
        run(world, admin_cap)
    }
}

Then let's use this function in the move_t method and register method in map_system.move.

map_system.move
module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}

Now if you try moving onto a tile with a boulder you’ll see that you can’t!

2.4. Constrain movement

You may notice two quirks of the way we've implemented movement so far—players can move off of the bounds of the map and they may move more than one space at a time by clicking. We're going to fix those with some changes to systems and methods.

We'll address this by updating the register and move_t methods in map_system.sol to wrap the player coordinate around the map size.

map_system.move
module constantinople::map_system {
    use sui::tx_context::TxContext;
    use sui::tx_context;
    use constantinople::world::World;
    use constantinople::player_schema;
    use constantinople::position_schema;
    use constantinople::movable_schema;

    /// error already register
    const EAlreadyRegister: u64 = 0;
    /// error can only move to adjacent spaces
    const EOnlyMoveToAdjacentSpaces: u64 = 6;

    public entry fun register(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);
        assert!(!player_schema::contains(world, player), EAlreadyRegister);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        // error constrain position to map size
        let (width, height, _) = map_schema::get(world);
        assert!(x >= 0 && x <= width, EExceedingMapLimits);
        assert!(y >= 0 && y <= height, EExceedingMapLimits);

        player_schema::set(world, player, true);
        position_schema::set(world, player, x, y);
        movable_schema::set(world, player, true);
    }

    public entry fun move_t(world: &mut World, x: u64, y: u64, ctx: &mut TxContext) {
        let player = tx_context::sender(ctx);

        let position = entity_key::from_position(x, y);
        // error this space is obstructed
        assert!(!obstruction_schema::contains(world, position), EObstaclesExist);

        // error constrain position to map size
        let (width, height, _) = map_schema::get(world);
        assert!(x >= 0 && x <= width, EExceedingMapLimits);
        assert!(y >= 0 && y <= height, EExceedingMapLimits);

        let (from_x, from_y) = position_schema::get(world, player);
        // error can only move to adjacent spaces
        assert!(distance(from_x, from_y, x, y) == 1, EOnlyMoveToAdjacentSpaces);

        position_schema::set(world, player, x, y);
    }

    fun distance(from_x: u64, from_y: u64, to_x: u64, to_y: u64) : u64 {
        let delta_x = if(from_x > to_x) {from_x - to_x } else { to_x - from_x };
        let delta_y = if(from_y > to_y) { from_y - to_y } else { to_y - from_y };
        delta_x + delta_y
    }
}