2. Map and terrain
In this section, we will accomplish the following:
- Configure the map as a singleton schema and initialize it in the client.
- Add terrain (tall grass and boulders) to the map.
- 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
).
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.
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:
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.
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
.
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.
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
}
}