155 lines
3.8 KiB
Markdown
155 lines
3.8 KiB
Markdown
# boardgame-phaser
|
||
|
||
基于Phaser3/boardgame-core的游戏框架
|
||
|
||
## 概述
|
||
|
||
项目使用pnpm monorepo管理,包含以下包:
|
||
- `framework`:通用框架
|
||
- `boop-game`:boop样例
|
||
- `sample-game`:tic tac toe样例
|
||
|
||
游戏应当使用vite构建,基于`preact/signals`进行状态管理,使用`phaser3`实现游戏功能。
|
||
|
||
## boardgame-core
|
||
|
||
项目使用`boardgame-core`进行游戏定义。
|
||
`boardgame-core`的内容可以在`framework/node_modules/boardgame-core`找到。
|
||
这个文件夹被.gitignore忽略,查看时需要绕开这一限制。
|
||
|
||
## 编写GameModule
|
||
|
||
游戏逻辑以GameModule的形式定义:
|
||
|
||
```typescript
|
||
import {createGameCommandRegistry, IGameContext} from "boardgame-core";
|
||
|
||
// 创建mutative游戏初始状态
|
||
export function createInitialState(): GameState {
|
||
//...
|
||
}
|
||
|
||
// 运行游戏
|
||
export async function start(game: IGameContext<GameState>) {
|
||
// ...
|
||
}
|
||
|
||
// 可选
|
||
export const registry = createGameCommandRegistry();
|
||
```
|
||
|
||
使用以下步骤创建GameModule:
|
||
|
||
### 1. 定义状态
|
||
|
||
通常使用一个`regions: Record<string, Region>`和一个`parts: Record<string, Part<TMeta>>`来记录桌游物件的摆放。
|
||
|
||
```typescript
|
||
import {Region} from "boardgame-core";
|
||
|
||
type GameState = {
|
||
regions: {
|
||
white: Region,
|
||
black: Region,
|
||
board: Region,
|
||
},
|
||
pieces: Record<string, Part<PartsTable['0']>>,
|
||
currentPlayer: PlayerType,
|
||
winner: WinnerType,
|
||
};
|
||
```
|
||
|
||
游戏的部件可以从`csv`加载。详情见`boop-game/node_modules/inline-schema/`。
|
||
```
|
||
/// parts.csv
|
||
type, player, count
|
||
string, string, number
|
||
cat, white, 8
|
||
cat, black, 8
|
||
|
||
/// parts.csv.d.ts
|
||
type PartsTable = {
|
||
type: string;
|
||
player: string;
|
||
count: number;
|
||
}[];
|
||
|
||
declare const data: PartsTable;
|
||
export default data;
|
||
```
|
||
|
||
### 2. 定义流程
|
||
|
||
使用`async function start(game: IGameContext<GameState>)`作为入口。
|
||
|
||
使用`game.value`读取游戏当前状态
|
||
```typescript
|
||
async function gameEnd(game: IGameContext<GameState>) {
|
||
return game.value.winner;
|
||
}
|
||
```
|
||
|
||
需要修改状态时,使用`game.produce`或`game.produceAsync`。
|
||
|
||
```typescript
|
||
async function start(game: IGameContext<GameState>) {
|
||
await game.produceAsync(state => {
|
||
state.currentPlayer = 'white';
|
||
});
|
||
}
|
||
```
|
||
|
||
需要等待玩家交互时,使用`await game.prompt(schema, validator, player)`。
|
||
|
||
```typescript
|
||
async function turn(game: IGameContext<GameState>, currentPlayer: PlayerType) {
|
||
const {player, row, col} = await game.prompt(
|
||
'play <player:string> <row:number> <col:number>',
|
||
(player, row, col) => {
|
||
if(player !== currentPlayer)
|
||
throw `Wrong player!`
|
||
return {player, row, col};
|
||
}
|
||
)
|
||
}
|
||
```
|
||
|
||
### 3. 编写测试
|
||
|
||
使用`vitest`编写测试,测试应当使用`GameHost`来模拟游戏环境。
|
||
|
||
## 编写Phaser App
|
||
|
||
使用`framework/src/ui/PhaserBridge`来创建Phaser App。
|
||
|
||
使用`framework/src/scenes/GameHostScene`来创建游戏场景。
|
||
|
||
使用`GameHost`来控制游戏状态。
|
||
|
||
```typescript
|
||
export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
|
||
// 获取游戏状态的只读快照
|
||
get state(): TState{}
|
||
// 运行状态
|
||
readonly status: ReadonlySignal<GameHostStatus>;
|
||
|
||
// 运行中途需要玩家输入时使用
|
||
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
||
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
||
|
||
// 玩家响应activePrompt的输入,若报错则返回string,否则返回null
|
||
onInput(input: string): string | null {}
|
||
|
||
// 添加中断,context.produceAsync会等待所有中断结束之后再继续
|
||
addInterruption(promise: Promise<void>): void {}
|
||
|
||
// 开始或者重新开始游戏
|
||
start(): Promise<TResult>{}
|
||
|
||
// 销毁
|
||
dispose(): void {}
|
||
|
||
// 事件侦听
|
||
on(event: 'start' | 'dispose', listener: () => void): () => void {}
|
||
}
|
||
``` |