上一篇我们讲了MUD store
如何编写data model数据模型
还是比较简单的
接下来我们来讲MUD world
我们可以理解成是一个MUD的世界状态
比如我们现在创建并部署了一个world合约
那么world合约本质上是来记录我们现在所有的游戏状态的
那么存储的结构也就是我们通过data model生成的表结构
每一个用户都是将一个namespace命名空间作为存储索引
举个例子
比如我们现在有3个表,cat表,dog表,balance表
用户a的地址为0xaaa
用户b的地址为0xbbb
那么0xaaa和0xbbb就是这2个用户的命名空间
然后他们在每张表上存储了自己的数据
那么MUD store和MUD world就是2个最基本的概念了
接下来我们正式来看MUD框架
MUD就是集成了
1.前端React
2.后端Next
3.合约框架foundry
我们来创建个项目
pnpm create mud@next my-project
创建完之后就来运行一下
pnpm run dev
我们可以看到现在的模板
实现了一个非常简单的功能
按一下按钮就调用了increment()方法
然后合约上的number就增加了1
那我们来看看这一切是如何运作的
首先我们来看看data model数据模型
export default mudConfig({
tables: {
Counter: {
keySchema: {},
schema: "uint32",
},
},
});
我们已经学习过data model了
这里很简单吧
那么我们看一下通过data model生成的table合约
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/* Autogenerated file. Do not edit manually. */
// Import schema type
import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol";
// Import store internals
import { IStore } from "@latticexyz/store/src/IStore.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { StoreCore } from "@latticexyz/store/src/StoreCore.sol";
import { Bytes } from "@latticexyz/store/src/Bytes.sol";
import { Memory } from "@latticexyz/store/src/Memory.sol";
import { SliceLib } from "@latticexyz/store/src/Slice.sol";
import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol";
import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol";
import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol";
bytes32 constant _tableId = bytes32(abi.encodePacked(bytes16(""), bytes16("Counter")));
bytes32 constant CounterTableId = _tableId;
library Counter {
/** Get the table's key schema */
function getKeySchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](0);
return SchemaLib.encode(_schema);
}
/** Get the table's value schema */
function getValueSchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](1);
_schema[0] = SchemaType.UINT32;
return SchemaLib.encode(_schema);
}
/** Get the table's key names */
function getKeyNames() internal pure returns (string[] memory keyNames) {
keyNames = new string[](0);
}
/** Get the table's field names */
function getFieldNames() internal pure returns (string[] memory fieldNames) {
fieldNames = new string[](1);
fieldNames[0] = "value";
}
/** Register the table's key schema, value schema, key names and value names */
function register() internal {
StoreSwitch.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
}
/** Register the table's key schema, value schema, key names and value names (using the specified store) */
function register(IStore _store) internal {
_store.registerTable(_tableId, getKeySchema(), getValueSchema(), getKeyNames(), getFieldNames());
}
/** Get value */
function get() internal view returns (uint32 value) {
bytes32[] memory _keyTuple = new bytes32[](0);
bytes memory _blob = StoreSwitch.getField(_tableId, _keyTuple, 0, getValueSchema());
return (uint32(Bytes.slice4(_blob, 0)));
}
/** Get value (using the specified store) */
function get(IStore _store) internal view returns (uint32 value) {
bytes32[] memory _keyTuple = new bytes32[](0);
bytes memory _blob = _store.getField(_tableId, _keyTuple, 0, getValueSchema());
return (uint32(Bytes.slice4(_blob, 0)));
}
/** Set value */
function set(uint32 value) internal {
bytes32[] memory _keyTuple = new bytes32[](0);
StoreSwitch.setField(_tableId, _keyTuple, 0, abi.encodePacked((value)), getValueSchema());
}
/** Set value (using the specified store) */
function set(IStore _store, uint32 value) internal {
bytes32[] memory _keyTuple = new bytes32[](0);
_store.setField(_tableId, _keyTuple, 0, abi.encodePacked((value)), getValueSchema());
}
/** Tightly pack full data using this table's schema */
function encode(uint32 value) internal pure returns (bytes memory) {
return abi.encodePacked(value);
}
/** Encode keys as a bytes32 array using this table's schema */
function encodeKeyTuple() internal pure returns (bytes32[] memory) {
bytes32[] memory _keyTuple = new bytes32[](0);
return _keyTuple;
}
/* Delete all data for given keys */
function deleteRecord() internal {
bytes32[] memory _keyTuple = new bytes32[](0);
StoreSwitch.deleteRecord(_tableId, _keyTuple, getValueSchema());
}
/* Delete all data for given keys (using the specified store) */
function deleteRecord(IStore _store) internal {
bytes32[] memory _keyTuple = new bytes32[](0);
_store.deleteRecord(_tableId, _keyTuple, getValueSchema());
}
}
这个Counter.sol就是一个table合约
是自动生成的,我们不需要去修改
然后我们看一下client这边
看看App.tsx
import { useComponentValue } from "@latticexyz/react";
import { useMUD } from "./MUDContext";
import { singletonEntity } from "@latticexyz/store-sync/recs";
export const App = () => {
const {
components: { Counter },
systemCalls: { increment },
} = useMUD();
const counter = useComponentValue(Counter, singletonEntity);
return (
<>
<div>
Counter: <span>{counter?.value ?? "??"}</span>
</div>
<button
type="button"
onClick={async (event) => {
event.preventDefault();
console.log("new counter value:", await increment());
}}
>
Increment
</button>
</>
);
};
这里我们看到
点一下Button就调用了increment
这里的increment是来自useMUD()的systemcalls
const {
components: { Counter },
systemCalls: { increment },
} = useMUD();
那么我们来看一下systemCalls
import { getComponentValue } from "@latticexyz/recs";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
import { singletonEntity } from "@latticexyz/store-sync/recs";
export type SystemCalls = ReturnType<typeof createSystemCalls>;
export function createSystemCalls(
{ worldContract, waitForTransaction }: SetupNetworkResult,
{ Counter }: ClientComponents
) {
const increment = async () => {
const tx = await worldContract.write.increment();
await waitForTransaction(tx);
return getComponentValue(Counter, singletonEntity);
};
return {
increment,
};
}
createSystemCalls.ts
那么这里就看到了increment方法
const tx = await worldContract.write.increment();
那么我们实际上调用了worldContract的increment方法
那么我们再返回去看一下worldContract中的increment
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Counter } from "../codegen/Tables.sol";
contract IncrementSystem is System {
function increment() public returns (uint32) {
uint32 counter = Counter.get();
uint32 newValue = counter + 1;
Counter.set(newValue);
return newValue;
}
}
这是我们的IncrementSystem.sol合约
那么这里的increment方法实际上就是在Counter表中记录的数值
我们再看一下读取counter的地方
const counter = useComponentValue(Counter, singletonEntity);
这里使用了useComponentValue
是因为使用ecs
import { singletonEntity } from "@latticexyz/store-sync/recs";
ecs指的是游戏开发中的ecs结构
即 Entity-Component-System(实体-组件-系统) 的缩写,其模式遵循组合优于继承原则,游戏内的每一个基本单元都是一个实体,每个实体又由一个或多个组件构成,每个组件仅仅包含代表其特性的数据(即在组件中没有任何方法),例如:移动相关的组件MoveComponent包含速度、位置、朝向等属性,一旦一个实体拥有了MoveComponent组件便可以认为它拥有了移动的能力,系统便是来处理拥有一个或多个相同组件的实体集合的工具,其只拥有行为(即在系统中没有任何数据),在这个例子中,处理移动的系统仅仅关心拥有移动能力的实体,它会遍历所有拥有MoveComponent组件的实体,并根据相关的数据(速度、位置、朝向等),更新实体的位置。
我们现在用另一种方法来读取counter
用一下useRow
import { useComponentValue } from "@latticexyz/react";
import { useMUD } from "./MUDContext";
import { singletonEntity } from "@latticexyz/store-sync/recs";
export const App = () => {
const {
components: { Counter },
systemCalls: { increment },
network: { singletonEntity, storeCache },
} = useMUD();
// const counter = useComponentValue(Counter, singletonEntity);
const counter= useRow(storeCache, {table: "Counter", key:{} })
return (
<>
<div>
{/*Counter: <span>{counter?.value ?? "??"}</span>*/}
Counter: <span>{counter?.value.value ?? "??"}</span>
</div>
这里我们用了useRow
然后从storeCache里面查询
填入table和key
下面读取的地方改成
counter?.value.value
import { useComponentValue ,useRow} from "@latticexyz/react";
ok
ok
ok
ok