7.1. Singleton component for map config
Before we start making the map more interesting with terrain, let's move the map configuration into ECS. We'll use this map config in our move system in future steps to check boundaries for movement and add behavior to different terrain types (e.g. tall grass).
We'll use the singleton pattern to create one value globally. Using ECS isn't strictly necessary here, but doing it now opens up the option of changing the map on the fly.
Map config component
Because we have a very specific shape of data, we'll use a custom component for this. The main difference between this and the built-in component types is that we'll need to manually define the component schema. This is usually done for you when inheriting from e.g. BoolComponent
. Defining the schema in our components allows the MUD networking stack to decode component data stored on chain.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { BareComponent } from "solecs/BareComponent.sol";
import { LibTypes } from "solecs/LibTypes.sol";
import { SingletonID } from "solecs/SingletonID.sol";
uint256 constant ID = uint256(keccak256("component.MapConfig"));
struct MapConfig {
uint32 width;
uint32 height;
}
contract MapConfigComponent is BareComponent {
constructor(address world) BareComponent(world, ID) {}
function getSchema() public pure override returns (string[] memory keys, LibTypes.SchemaValue[] memory values) {
keys = new string[](2);
values = new LibTypes.SchemaValue[](2);
keys[0] = "width";
values[0] = LibTypes.SchemaValue.UINT32;
keys[1] = "height";
values[1] = LibTypes.SchemaValue.UINT32;
}
function set(MapConfig memory mapConfig) public {
set(SingletonID, abi.encode(mapConfig.width, mapConfig.height));
}
function getValue() public view returns (MapConfig memory) {
(uint32 width, uint32 height) = abi.decode(getRawValue(SingletonID), (uint32, uint32));
return MapConfig(width, height);
}
}
Initialize map config
We can use the initializer pattern in MUD to set our map config on deploy. They exist as libraries with an internal init
function that receives the world
contract address.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { IWorld } from "solecs/interfaces/IWorld.sol";
import { MapConfigComponent, ID as MapConfigComponentID, MapConfig } from "components/MapConfigComponent.sol";
library MapConfigInitializer {
function init(IWorld world) internal {
MapConfigComponent(world.getComponent(MapConfigComponentID)).set(MapConfig({ width: 20, height: 20 }));
}
}
Now we can add our new map config component and initializer to the deploy.json
config. You may need to restart your dev:contracts
service to redeploy contracts with the initializer.
{
"components": ["MapConfigComponent", "MovableComponent", "PlayerComponent", "PositionComponent"],
"initializers": ["MapConfigInitializer"],
"systems": [
Map config on the client
Again, because this is a custom component, we need to define its schema on the client as part of its component definition.
import { overridableComponent, defineComponent, Type } from "@latticexyz/recs";
import {
defineBoolComponent,
defineCoordComponent,
} from "@latticexyz/std-client";
import { world } from "./world";
export const contractComponents = {
MapConfig: defineComponent(
world,
{
width: Type.Number,
height: Type.Number,
},
{
id: "MapConfig",
metadata: { contractId: "component.MapConfig" },
}
),
Movable: defineBoolComponent(world, {
metadata: {
contractId: "component.Movable",
We'll use a new React hook for the map config, so we can ensure its value is present before using it. This makes types easier to work with downstream. Without the loading screen we added, using this hook would immediately throw an error.
import { useComponentValue } from "@latticexyz/react";
import { useMUD } from "./MUDContext";
export const useMapConfig = () => {
const {
components: { MapConfig },
singletonEntity,
} = useMUD();
const mapConfig = useComponentValue(MapConfig, singletonEntity);
if (mapConfig == null) {
throw new Error("game config not set or not ready, only use this hook after loading state === LIVE");
}
return mapConfig;
};
And now we can replace the hardcoded map dimensions in the game board with the new map config.
import { useComponentValue } from "@latticexyz/react";
import { useMUD } from "./MUDContext";
import { useKeyboardMovement } from "./useKeyboardMovement";
import { useMapConfig } from "./useMapConfig";
export const GameBoard = () => {
const { width, height } = useMapConfig();
const rows = new Array(height).fill(0).map((_, i) => i);
const columns = new Array(width).fill(0).map((_, i) => i);
const {
components: { Position },
playerEntity,
} = useMUD();