boardgame.io新手教程

boardgame.io新手教程 

这个新手教程介绍了如何实现井字棋游戏。

通过新手教程,对boardgame.io及其依赖环境有个初步认识。

第一阶段:搭个架子

教程的完整代码可以通过在Edit on CodeSandBox那个按钮那里点击进入CodeSandBox里看到。

第一阶段把教程Game Improvements之前的全部跑通并基本理解。

1 准备与安装

准备一个代码编辑器,用vsCode、sublime、Eclipse等都可以。sublime非常轻量,本文用这个。

教程说用的脚本标准是ES2015。什么是ES2015? ECMAScript 6(简称es6)是ECMA在2015年发布的JavaScript语言标准,又称ECMAScript 2015。也就是说,es6就是es2015。

首先安装node.js。在windows中,打开dos终端,以node -v和npm -v检查安装是否完全成功。

2 Setup

在某个位置建立一个目录bgio-tutorial,然后在dos终端进入该目录,执行如下命令:

npm init --yes

这个命令将创建一个名为package.json的文件。

添加 boardgame.io:

npm install boardgame.io

这个命令将创建node_modules目录和package-lock.json文件。执行起来有点慢,似乎死机了,需要耐心等待完成,完成后会回到DOS命令符。安装完成后,库文件存在于\node_modules\boardgame.io子目录。可以进去浏览一下,看看有些什么东西。

添加Parcel 来帮助我们构建我们的应用程序:

npm install --save-dev parcel-bundler

据说,Parcel 是一个强大而灵活的打包工具,它可以让你专注于编写代码,而不必花费大量时间配置构建过程。

执行起来也比较慢,耐心等待完成。执行后,根目录中不产生新文件,原来的两个.json文件被更新。

创建项目所需的基本结构:

1)Web 应用程序的 JavaScript 文件位于src/App.js.

2)游戏定义的 JavaScript 文件位于src/Game.js.

3)一个基本的 HTML 页面index.html

创建这3个文件之后,项目目录的文件组织应该如下所示:

bgio-tutorial/

├── index.html

├── node_modules/

├── package-lock.json

├── package.json

└── src/

├── App.js

└── Game.js

教程在Setup这一步给了一个CodeSandbox,其中有教程的完整代码:

3 Defining a Game

按教程为src/Game.js文件编写代码。

4 Creating a Client

按教程为src/App.js文件编写代码。

修改package.json。注意修改这个json文件时有没有引入特殊符号,有没有加够逗号,在sublimeText中编写若有异常,会以红色提示。若package.json存在异常,则在后面执行npm start会报类似下面的错误:

npm start运行没有看到教程所说的效果 → 仔细检查发现Chrome还是在执行原来的代码。原来是index.html中对index.js的引用还没有改成app.js,于是改正过来。

控制台和Chrome都提示发生了错误 → 仔细检查是app.js的代码在拷贝时没复制全,改正后,控制台已经自动重新编译了。

5 在CodeSandbox中跑通第一阶段代码

CodeSandbox既然能跑通教程的全部代码(上图中的按钮直接点进去就是全部代码),那么部分代码也是能够跑通的。现在我们在CodeSandbox中试试。

首先研究了一会儿,发现修改任何代码后,应该点击下图中的刷新按钮:

否则可能存在各种不正常。

用第一阶段的代码替换原来的app.js和game.js之后,点刷新,没有任何异常。

6 在本机跑通第一阶段代码

先离开教程测试一下。把根目录的index.html文件对app.js的引用改成index.js,即:

<!DOCTYPE html><html><head><title>Parcel 实践</title></head><body><script src="./src/index.js"></script></body></html>

然后在src子目录添加index.js 文件。在 src/index.js 文件中添加一行代码:

console.log('Hello Parcel!');

接下来,使用 Parcel 运行项目:npx parcel index.html。其实可以不用重新运行,修改任何源文件后,parcel会自动在后台监测并重新编译,直接在浏览器刷新就可以了。这点与在CodeSandbox中是一样的。

打开Chrome并访问 http://localhost:1234,网页一片空白。右键弹出菜单,选择检查,或者按快捷键CTRL+SHIFT+I,打开右边栏,选择console选项卡:

可以看到控制台输出 “Hello Parcel!”。

再把index.htm对index.js的引用改成app.js。在浏览器中刷新。若有错误,可在Chrome的console中看到正确的异常信息。我当时的情况是 根据提示检查,发现是代码copy错了。

更正,再刷新,终于出现了预期结果,即在网页右半边出现了下面的调试面板:

7 初步理解第一阶段代码

app.js的代码:

import { Client } from 'boardgame.io/client'; // 导入boardgame.io客户端库
import { TicTacToe } from './Game'; // 导入game.js中定义的TicTacToe对象
// 下面定义一个class,在构造函数中调用了boardgame.io 中的Client类的构造及start函数,这个Client类用到了game.js中定义的TicTacToe对象
class TicTacToeClient {
  constructor() {
    this.client = Client({ game: TicTacToe });
    this.client.start();
  }
}
// 创建这个class的实例
const app = new TicTacToeClient();

game.js的代码:

export const TicTacToe = { // 导出TicTacToe对象,这是es5语法
  setup: () => ({ cells: Array(9).fill(null) }), // setup成员指向一个函数
  moves: {
    clickCell: ({ G, playerID }, id) => {
      G.cells[id] = playerID;
    },
  },
};
//这个对象以es5对象定义语法将setup和moves两个成员分别指向各自对应的函数。它们应该是将会被boardgame.io 中的Client类调用。

这个对象以es5对象定义语法将setup和moves两个成员分别指向各自对应的函数。它们应该是将会被boardgame.io 中的Client类调用。

要完全理解这个代码,需要深入学习node.js和boardgame.io。

8 回头看Concepts(官方文档第一章)

boardgame.io 捕获游戏状态,放在在两个对象中:G和ctx。G是The game state (managed by you)。ctx是Read-only metadata (managed by the framework). (framework,框架,应该就是指boardgame.io库,下面统一叫框架吧)

这些状态对象在各处传递,并在客户端和服务器上无缝维护。ctx中的状态是立即采用的,这意味着如果您愿意,您可以手动管理G中的所有状态。这句话写得真难懂

因为状态可以在客户端和服务器之间发送,所以G必须是 JSON 可序列化的对象;特别是,它不能包含类或函数。所以G不是game.js,因为教程全部代码中的game.js包含了函数。正解!我们来看game.js中的代码就明白了:

moves: {
    clickCell: ({ G, playerID }, id) => {
      G.cells[id] = playerID;
    },
  },

game.js中可以包括很多东西,TicTacToe只是其中一个类,所以G是不是game.js这句话本身就存在问题。

TicTacToe这个类其中定义了moves,它引用到了G!G只是我们可以管理的游戏状态,即,可读写。ctx是boardgame.io框架自动管理的,我们仅能访问,不能修改。

其实教程运行出现的调试面板中已经给出了G和ctx的概貌:

          

教程中G的成员cells应该是TicTacToe.setup函数建立的。

接下来有几个重要的概念:moves, events, phases, Turn Order, stages。

move告诉框架如何更改G的状态。后面的话没看懂。如教程定义的clickCell,就是一个move,它修改了G的状态cells。这在教程运行出现的调试面板中有对应: 。官方文档继续说可以显示调用move,例如:client.moves.clickCell();为什么在client.moves下存在clickCell,应该是在创建client时指定了TicTacToe类,Client类访问了TicTacTole.moves。所以moves不是函数,而是数组/集合之类的东西,其成员是函数指针/回调/委托。

Event是框架提供的功能,类似于moves,只是它们在ctx上工作。它们常通过结束一个turn、改变游戏phase等操作来推进游戏状态。event以类似于move的方式从client调用。官方文档说可以这样显示触发event:client.events.endTurn();在教程运行出现的调试面板中,给了一个默认的event:

phase是游戏中的一个时期,当它处于活动状态时,它会覆盖游戏配置。在一个phase内,你可以使用不同的move序列或者不同的turn order序列。turn发生在phase内,意思应该是一个phase中包含多个turn。游戏在不同的phase之间转换,这意思应该是一个游戏由多个phase衔接而成(当然可能存在循环链)。

一个turn是与玩家个体相关的周期,通常由一到多个move组成。玩家A完成turn内的move之后,turn转到下一个玩家。

一个stage类似于一个phase,只是它发生在一个turn内,适用于玩家个体,而不是整个游戏。一个turn可以细分为许多stage,每个stage都允许不同的move集,并在该stage活动时覆盖其他游戏配置选项。此外,不同的玩家在一个turn中可能处于不同的stage。

小结一下:

move和event是用来修改游戏状态的函数。move由开发者定义,event内置于框架中。move改变G的状态,event改变ctx的状态!

一个game由很多phase衔接而成。一个phase由多个turn组成。一个turn由多个move组成。一个复杂的turn可以细分成多个stage,一个stage由多个move组成。

move是单个玩家不可分的最小行动。一个turn是单个玩家的一系列move。玩家A做完了这些move,turn order才转换到下一个玩家B。

9 基本理解第一阶段代码

app.js关键的话就两句:

this.client = Client({ game: TicTacToe }); // 实例化框架中的Client类,初始化参数是game.js中的TicTacToe类

this.client.start(); // 执行框架中的Client类的start函数,把游戏运行起来。

game.js就只定义了TicTacToe对象,它包含setup函数和moves,这两个东西将要被Client类访问,moves将成为Client中的moves。

setup应该是返回一个G,其中包含cells。

moves中包含一个move,即clickCell,它修改了G的状态。

目前没有调用任何move啊什么的,所以游戏状态没有变化,clickCell也不会被执行。

10 理解运行结果

运行结果只有一个调试面板。

在教程的网页上与自己在本机发布的结果是一样的。在MOVES一栏点击clickCell函数,输入0-8的数字(注意没有输入光标,直接输入数字可以看到输进去了,在CodeSandbox中运行则能看到输入光标,可能Chrome版本低的原因),回车,看到G的状态变了。

当前的player可以在ctx中看到,在PLAYERS这一栏以灰黑色背景表示。

可以在player这一栏手工选择某个player,以红框框出,但是再点击clickCell函数修改会在console中提示这是disallowed move。

要切换player,必须点击EVENTS一栏的endTurn()函数,然后回车,会看到当前player切换成了下一个正确的player的。

在教程第一阶段中:两个player,没有代码设置这个参数,所以它应该是默认值;一个turn只包含一个player的一个move:clickCell;endTurn是内生的event,用于结束当前player的turn切换到下一个player。

clickCell的输入参数是2个:({ G, playerID }, id);第一个应该是一个数组/集合,是标准的参数(不一定!在完整版教程中它的输入参数是(G, ctx, id)),因为调试面板不需要输入它;第二个是要在调试面板中输入的值。在game.js中看到代码据此修改了G.cells,调试面板中可以看到运行结果与预期一致。

在调试面板中选中某个move或event输入参数后按enter,相当于调用了这个move/event。

看起来框架没做什么工作。但是我们在调试面板做各种非法动作就可以看到,框架提示不被允许。

到这里基本完全理解了教程第一阶段。有一点需要注意的是,看起来前台和后台完全是在一起的。难道仅仅是编写了一些前台的东西,boardgame.io仅仅是运行在前台?事实基本如此,相当于在服务器上实现了一堆带js的静态的东西,浏览器下载了这些静态的东西之后,就完全可以独立运行了,运行期间不需要与服务器交互。

第二阶段:单机游戏

本阶段完成单人单机部分的后续任务。先记录水一点,有时间再返回来修改整理。

1  Game Improvements

Validating Moves:

验证move的有效性。这里发现move可以return INVALID_MOVE。注意:

turn: {

    minMoves: 1,

    maxMoves: 1,

  },

这个意思就是一个move作出后,一个turn就会自动结束,切换到下一个玩家。去掉这个代码就会在完成UI之后发现一个player可以一直落子直到游戏结束,而另一个player什么move都还没有作出。

Managing Turns:

这段没看懂。

Victory Condition:

这段代码比较复杂。应该是构造了两个函数:IsVictory和IsDraw(注意不要望文生义以为这是是否画图,而应该是IsGameOver);一个TicTacToe成员:endIf。

比较难懂的是endIf,为什么IsVictory在IsDraw的前面。是不是说因为可能存在平局,所以要先调用IsVictory检查是否产生了胜利者?应该是这样,因为如果把IsDraw放前面的话,就先返回了draw变量(注意等价于isGameOver)而非winner变量。

在这一步修改代码时,发现function不能放在class里。原来node.js是这样的。

2  Building a Board

这段代码就更不容易懂了,还好他只用到了基本的HTML DOM。他这里的board就相当于棋盘或者说UI。反正是用户交互界面。

不说UI的部分,只说与框架有关的地方:

在TicTacToeClient这个类的构造函数中:

this.client = Client({ game: TicTacToe });

    this.client.start();

    this.rootElement = rootElement;

    this.createBoard();

    this.attachListeners();

    this.client.subscribe(state => this.update(state));

有个subscribe函数用于订阅状态变化,并指定回调update函数。这里的参数state是这个什么东西?是不是就是Client实例?反正在update函数中访问了state.G和state.ctx

在单元格的click事件中调用了:

this.client.moves.clickCell(id);

注意在这一步需要修改index.html,否则接下来运行不起来。

3  运行代码

之前只能在调试面板上操作,现在可以在UI上通过点击单元格来进行move了。每走一步,就点击空白单元格一次,当前player做出一个move,并且turn转换到下一个player。相当于一个人两边下。所以要想人机对战,必须加入Bots。

4  Bots

这就是所谓的机器AI了。

它的代码非常简单,就是看哪个单元格是空的,就落一子。但是调试面板中的AI却不是这样的,有random(随机)和MCTS(一种AI方法)两种AI。所以调试面板上是框架提供的功能。

5  运行代码

一番试用后,认为应该是我们的代码提供可能的move集合,而调试面板中的AI是框架自动产生的,它将在我们返回的move集合中挑一个move。

调试面板给出了AI的两种动作方式

play:使机器人计算并做出单个动作(快捷方式2:)

simulate:使机器人自己玩整个游戏(快捷键3:)

play就相当于走一步。simulate就相当于机器走两边一个人把棋局走到结束。

第三阶段:多人网络

1 Game Master

多人模式下必须有一个GameMaster来协调各个Client的同步运行。客户端发出移动/事件,但游戏逻辑在主服务器上运行,主服务器在将其广播到其他客户端之前计算下一个游戏状态。

然而,由于客户端了解游戏规则,他们也会并行运行游戏(这称为乐观更新optimistic update,是一种提供无延迟体验的优化)。如果特定客户端错误地计算新的游戏状态,它最终会被主机覆盖,因此整个设置仍然具有单一的权威来源。

Game Master有Local Master和Remote Master两种。

2 Local Master

GameMaster可以完全在浏览器上运行。这对于pass-and-play multiplayer或对多人游戏体验进行原型设计非常有用,而无需设置服务器来测试它。这时,GameMaster部署于浏览器上而非服务器,所以称为Local Master。

为做到这一点,请import { Local } from 'boardgame.io/multiplayer',然后在创建Client实例时指定multiplayer参数为Local()。

接下来教程给出了修改TicTacToe的代码,并给出了其在CodeSandbox中的源代码(这个源代码比教程中说的,多了些功能)。

实践中遇到了两个问题。

1)教程说“当没有轮到玩家时,点击特定的棋盘是没有效果的”。实际上,在CodeSandbox会立即报错:

这个界面挡住了棋盘界面。注意到右上角有个×号,可以点击它来关闭这个界面。

在本机上,Chrome中的效果是按教程说的,但是console中会报错:

这个错误在CodeSandbox的console中也是一样。

所以实际上是框架抛出了异常。正常写代码应该要处理这种异常,或者避免出现disallowed move出现。

2)教程说Storing state in the browser这一段,CodeSanbox会报错

按教程的意思,应该是在创建Client实例时设置参数multiplayer: Local({persist: true,storageKey: 'bgio',}), 但是CodeSanbox会报错。后来在本机Chrome中运行不会报错。那么有没有起到效果呢?我们在浏览器上点击刷新,就会发现刷新后棋盘仍然不变!如果去掉{persist: true,storageKey: 'bgio',}这句话,那么在浏览器上点击刷新,棋盘就会清空。所以是起到了效果的。

再看看代码是如何完成Player切换的?

在Game Improvements分析过了,一个player做出一个move,turn自动切换到下一个player。此处仍然一样,代码其实什么都没有做。创建棋盘时,棋盘与Client关联,即player对应各自的棋盘,各自的Client,点击棋盘,则调用自己的client的moves.clickCell(playerid)。所以如果当前player不是自己,则框架抛出异常。

比较神奇的是这句话:const { currentPlayer } = state.ctx,居然不是currentPlayer= state.ctx. currentPlayer。难道这是node.js的功能?后面学习node.js的时候注意。

3 RemoteMaster

RemoteMaster是运行于服务器上的GameMaster。

为了将Client连接到RemoteMaster,创建Client实例时应该将multiplayer 参数应该设置SocketIO而不是Local,并指定服务器的位置:

multiplayer: SocketIO({ server: 'localhost:8000' }),

当使用RemoteMaster时,Client在第一次运行时不会知道游戏状态,因此state参数在update函数第一次被调用时将为null,只有在连接到服务器后state才有完整的游戏状态。所以update函数应该改成这样:

update(state) {
  if (state === null) return; // 还没有连接到服务器,什么都不做
  ...}

现在,每当您进行移动时,客户端都会在幕后通过 WebSocket 向RemoteMaster发送更新。

那么怎么建立RemoteMaster呢?

我们把教程反过来,从结果往回倒着走。结果就是:通过npm run serve在一个终端中运行来启动服务器程序,通过npm start在另一个终端中运行来为我们的 Web 应用程序提供服务。把项目部署于不同的机器上就可以达到目标,服务器启动服务器程序,另外的机器(多台)作为客户端运行浏览器。当然我们也可以把项目部署于一台服务器上,浏览器则从其它计算机运行发起访问。这样的话,教程中的TicTacToe项目就变成了“webSocket服务器+WEB服务器+浏览器”的方案,相当于做成了网游的形式。

总之,之前的代码就是WEB服务器+浏览器,浏览器访问WEB服务器取得全部文件后就用不着与WEB服务器通信了相当于静态网页,而现在webSocket服务器这一块是个单独的配置项了,浏览器在运行游戏的过程中需要不断与webSocket服务器通信。但教程中,这3块的代码是合在一起的。

教程给出在这个阶段的末尾给出了全部源代码,在这句话里存在链接:Complete code from this section is available on CodeSandbox for both React and Plain JS versions.

教程在文中没有叙述全部源代码,而是说明了实现方法,所以对于新手来说,应该从上面的链接中的CodeSandbox中复制源代码下来实践一下看看。

为了在本机运行教程,需要把app.js的代码和server.js的代码原样弄到本机。然后修改package.json,在scripts这一栏下添加一句话(注意教程说新建一个文件,其含义是把webSocket服务器这一块当作新的项目来处理的):"serve": "node -r esm src/server.js"

还要在dependencies这一栏下添加一句话:"esm": "3.2.25"

另外,还必须安装esm:npm install esm。耐心等待它安装完毕。

然后用两个windows控制台,一个npm run serve运行webSocket服务器,另一个npm start运行web服务器;开两个浏览器,都采用npm start所给URL:http://localhost:1234,很快就看到了教程所说的效果。

如果断开webSocket服务器,则浏览器中的游戏都出现了异常,Chrome console面板会提示具体的异常。

如果一开始就没有运行webSocket服务器,则在Client连接webSocket服务器时就会报错。

也就是说,教程只是以最简单的方式实现游戏,还没有完整地控制各种异常。

不明白的是,为什么同为scripts的命令,运行webSocket服务器用的语句是npm run serve,运行web服务器用的是npm start而不是npm run start实测npm run start是可以的。

我们现在回头看webSocket服务器部分代码,从源码文件看,应该就只有game.js和server.js两个文件,其中game.js与web服务器部分的代码是一样的(web服务器部分是不是可以做到不需要game.js?从Client初始化看是不行的),server.js的代码竟出奇简单,只有4行:

const { Server } = require('boardgame.io/server');
const { TicTacToe } = require('./Game');
const server = Server({ games: [TicTacToe] });
server.run(8000);

正如Client用game: TicTacToe作为参数初始化,Server则用games:[TicTacToe]作为参数初始化。这句代码还说明Server可以同时运行多个游戏实例,见第五阶段。

boardgame.io为网络通信做了完善的封装工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值