共计 6201 个字符,预计需要花费 16 分钟才能阅读完成。
前言
本文作者赵杭天。他加入了“2022 RTE 编程挑战赛”——“赛道二 场景化白板插件利用开发”,并凭借作品“成语解谜”取得了该赛道大奖。“成语解谜”是一个基于互动白板 SDK 的互动小游戏利用。通过前端编码、调用白板 API 能力、定制化后端逻辑等,实现了一个老少咸宜、寓教于乐的成语解谜游戏。其中的流程、步骤与相干的技术栈在白板互动利用开发上具备肯定的通用性。本文将分享该项目标开发过程,包含一些要害性能的实现,心愿与各位同学一起交换,共同进步。大家能够拜访 game.willtian.cn/idiom2/,在线体验该作品。
01 选题
为什么要做这样一款小游戏?有几个起因。
零几年刚上小学的时候,第一次接触到电脑和教育软件,外面有一些小游戏,真的会被疏导去学习到一些货色,比方一些名词概念、迷信常识,对小孩子挺有帮忙。
“白板”两个字,给我的第一感觉是回到了校园。在学校里都能遇到很好的同学和老师,有很多美妙的回顾。小时候喜爱读成语字典,就像看故事书,而后在教室里也会玩一些相似成语解谜这样的字谜游戏。
20 年疫情在家会玩一些益智休闲游戏,能玩到本人做的游戏,感觉很开心。另外,这类游戏很适宜碎片化的工夫,并且能让用户学习到一些货色。尤其适宜小朋友和喜爱休闲游戏的大敌人;对于前辈,操作上比拟敌对,内容也容易引起共鸣。从市场和社会上看,都是有价值的。
02 什么是互动白板 SDK
互动白板的正式名称叫 声网 Flat(点击文末“浏览原文”,理解更多),官网的解释是:“集体老师可间接应用的在线授课软件,开箱即用,前后端齐全开源,疾速搭建简洁好看的在线教室”。它运行起来初始界面长这样子:
互动白板初始界面
左侧工具栏图标通知咱们,这是一个能够在下面写写画画的货色。它具备这些特点:
1. 互动性,每个房间对应一个互动白板,默认状况下,房间内所有人都能够操作白板,并且交互成果所有人可见的;
2. 扩展性,除根本的书写、涂鸦性能外,互动白板反对自定义利用(点击工具栏最上面的“田”字型图标查看所有利用);
集体认为反对各种 APPs 是 Flat 互动白板最弱小的性能,通过 Flat 提供的 SDK 能力,咱们能够实现许多简单的性能的白板利用。
每个房间对应一个白板
互动白板的内容,包含文字、涂鸦以及 App,可由 SDK 中的 Window Manager 对象来管制。能够通过 官网提供的 demo 来疾速相熟一个 App 开发流程。利用 Window Manager 的 API 接口,咱们能够实现利用实例通信等操作,具体例子请见后文。
03 架构布局
在开展具体例子前,先介绍“成语解谜“我的项目的整体框架。如下图,咱们将前后端拆散的形式,前端专一页面绘制与互动,后端专一题目生成与后果判断。用户拜访前端页面无需下载全量词库,大幅提高访问速度。前端利用 Window Manager 的 context API 接口,在声网服务器上进行 App 实例的同步与播送。
前端 App 实例与声网服务、游戏后端的通信
04 界面设计
咱们采纳“设计驱动”的开发模式,首先画出设计图,而后一步一步的把脑海里的画面通过代码变成事实 :
设计草图
游戏主界面设计图如上,交互设计如下:
1. 谜面随机呈现若干个成语,这些成语由公共字进行关联,作为生成的约束条件;
2. 成语间关联的公共字被挖走并随机排列,作为候选字;
3. 用户通过“触摸 -> 拖拽 -> 搁置”交互操作候选字的实现对谜面的补全;
4.“提交”失去对用户谜面的判断后果,别离对应通关与未通过的场景;
5.“重置”将谜面和候选字复原到游戏初始状态;
6.“答案”通过弹窗展现谜面蕴含成语的信息,包含字型、字音、释义、出处以及用例;
(对于比较复杂的场景,倡议把场景间接切换的逻辑都画进去,造成一个比拟实现的需要文档)
抓住主要矛盾,优先实现外围性能的开发,实现产品原型后,再持续打磨,解决次要矛盾。
05 前端开发
实现游戏根本界面设计后,咱们开始抉择前端框架并实现界面开发。
适宜游戏开发的前端框架很多,Three.js、Phaser、Cocos2d-js 等,针对具体需要抉择。个人感觉 Three.js 比拟底层,用来写游戏代码量可能比拟大。Cocos2d-js 封装程度较高,须要相熟 Cocos 的工具链,对于非专业做游戏的同学而言,上手难度不低而且技术可迁移性不高。
这里抉择的是 PixiJS,PixiJS 是一个基于 2D WebGL 的渲染引擎,兼容 HTML5 Canvas。它有一系列正当、整洁的 APIs,反对 Sprite,将对象形象为各种层级的 Container。相似 React/Vue 数据驱动的设计,在 PixiJS 中,通过批改 Container 的参数,即可产生用户界面的变动。Pixi 的 API 实际上是 Flash 率先应用的,通过重复改良,有 Flash 教训的同学极易上手。
入口
以“成语解谜”为例,咱们来介绍编码的一些细节。首先咱们找到本人代码的挂载点,依据 文档给出的 demo 或者 本文提供的例子,找到这个入口文件:
自定义利用的入口(src/index.js)
留神到 const box = context.getBox();
这一行,box 对应这个利用关上的窗口。咱们通过 box.mountContent
向窗口挂载了蕴含咱们的 App 实例的 div 容器 $content
。
App 类
接下来,咱们定义 App 类。要害代码如下。
App 类(src/app.js)App 类中持有一个 PIXI.Application
实例,此外 App 类还持有一些绝对 App 维度上的变量与办法,例如:从 setup
(见 src/index.js)里透传的过去的 context
(用于调用 Window Manager 的 API)、App 实例的 id(用于前端辨别 App 实例)、layers(图层)、resizeObserver
(用于监听界面变动并自适应布局)getRandomString
(生成每局游戏的 token,用于后端交互)、storage(用于在声网服务器上存取 App 的状态)等。
Scene 类
咱们为每个场景写一个 Scene 类,这里只有一个场景。App 类实例化了 Scene 类,并应用 addChild
将 scene
实例退出渲染。接下来咱们为主界面写一个 Scene。要害代码如下:
Scene 类(src/scene.js)
在 Scene 的构造函数里实例化了“提交”、“重置”和“答案”三个按键,并定义了对应事件。咱们在 Scene 里实例化了类 Idiom,一个 Idiom 实例对应一套字谜与候选字,Idiom 又有子对象 Piece,Piece 对应具体的每一个字块。因为 Scene 的按键事件函数的须要,咱们把 Piece 状态的保留 / 读取办法写在了 Scene 类里。
Idiom 类 & Piece 类
咱们在 Idiom 类里定义了谜面与候选字的(Piece)字块生成办法、重置办法、拖拽失效办法。在 Piece 类中实现拖拽时的外观行为。
Idiom 类(src/idiom.js)
Piece 类(src/piece.js)
整体成果
主界面运行成果
06 后端开发
实例关联与隔离
因为词库比拟大,用户每次加载残缺词库会耗费较多的带宽和工夫,对用户体验影响较大。咱们通过搭建后端将谜面的获取、提交后果的验证、答案的获取,进行服务化,晋升用户体验。
如上文“架构布局”所述,咱们和每个 App 实例均持有一个 token,用于与后端通信时,对应上后端的游戏实例对象。UserGames 的 key 即为 token,在承受到浏览器发来的申请后,后端会在 UserGames 中查找相应的游戏实例 BoardGame,并失去以后的游戏状态,包含谜面 table、答案 answers、答案解析 answerDetail 等。
应用 UserGames 的 key(token) 来隔离游戏实例,并与前端 App 实例关联
谜面生成
谜面是怎么生成的呢,根本的算法思路是:
1. 预处理成语库,建设所有成语的字索引 NthOfChar *[]map[rune][][]rune
,保留第 n 个字为 m 的信息;
2. 应用 DFS 递归搜寻谜面。在以后成语找一个字 k 作为下一个生成开始的节点,依据约束条件,选定新成语以及新成语摆放地位:
a. k 必须呈现在新成语中;
b. 新成语搁置后须保障以后谜面不被毁坏;
搜寻的过程中应用索引 NthOfChar
实现剪枝;
多解兼容
咱们通过生成算法造成的谜面同时会产生 1 个惟一的答案。但实际上可能答案并不惟一,尤其是在成语较多时,替换某几个字,亦可生成正当的答案。针对这种状况,我须要一一校验用户提交的成语。若成语库里总共有 N 个成语,对成语库的成语生成字典树 Trie,能够将查找时间复杂度从 O(N) 降落到 O(1),最多 4 次搜寻。
全局单例
负责游戏实例生成的构造体 GlobalBoard 贮存了全量成语以及两头数据信息,作为全局单例,缩小内存拷贝;对于每个问题(谜面)获取的申请,间接返回 GlobalBoard 生成后果的拷贝。
应用全局单例与状态拷贝的形式优化内存应用
07 App 实例通信
实例状态的同步
到目前为止,咱们根本实现单用户的游戏。然而当咱们关上两个浏览器 tab 模仿多用户操作时会发现,App 的交互仅对以后用户失效,其余用户是无感知的。体现为,A 用户关上 App,拖拽到 App 窗口适合的地位,开始游戏,将候选词与空字块替换,而后提交;同时,B 用户在同一房间,却只看到了 A 关上 App,拖拽 App,看到的 App 内容与 A 的 App 展现内容并不同步,也感知不到 A 对 App 做的操作(能看到 A 鼠标光标静止,这是 Flat 兜底的同步逻辑)。
针对以后问题,咱们能够天然想到必须有某种机制,使用户在本地对 App 实例操作后,同步状态到某个所有用户可拜访的远端服务里,而后告诉所有用户将远端服务贮存的状态同步到本地 App 实例中,从新渲染 App 画面 ,这样才能够实现 多用户的互动。
谈到这里,大家可能会想到,那咱们是否能够在本人写的后端服务中退出同步性能呢?让咱们构思一下做这样的同步性能须要做哪些事:
1. 设计一套通信机制,本地实例可能被动感知远端状态的更新;
2. 解决好超时、重连、弱网等问题;
3. 提早足够低,能承受业务稳定的负载;
4. 服务通过充沛的测试,足够稳固;
认真思考会发现,稳固牢靠的实时通信其实是一个比拟大的课题,并不应该成为实现业务、产生业务价值的一个次要工作,换言之,本人造轮子的投入产出并不高。声网在实时网络通信畛域耕耘多年,基于其技术积攒,在 Flat 我的项目中提供一系列十分有用的通信 APIs,这些 APIs 设计与 React 很像,比拟容易上手。上面咱们通过这些 APIs 进行同步与播送,解决互动性的问题。
让咱们回到前端代码里,在 app.js 的 App 类做一些批改:
初始化实例的 storage
咱们给每个 App 实例持有一份 storage
对象,storage
对象来自白板利用创立时失去的 context。这里的 storage.ensureState
用以确保 storage.state
蕴含某些初始值。context.storage
实际上关联了远端服务的一个存储实例,它实时监听到本地 storage 的变动,当变动产生时,将主动同步最新的 storage 到服务端。即便是不同的用户,同一房间雷同的利用实例,实际上会对应到同一个远端 storage,画一张图直观一些:
storage
关联关系图
弄明确 storage 的同步个性,咱们要做的就是 在游戏状态发生变化的时候更新 context.storage,以及 减少监听 context.storage 变动的回调事件,将远端 context.storage 同步到游戏(利用实例)中。
咱们将状态的 push/pull 办法做封装,使代码更利于保护。这里的 storage.setState
和 React 的 setState 相似,更新 storage.state
并同步到所有客户端。
游戏状态 -> 远端 storage
减少监听事件,addStateChangedListener
在有人调用 storage.setState()
后触发 (蕴含以后 storage
),在这里咱们编写将远端 storage
同步到游戏状态的逻辑。
远端 storage -> 游戏状态
分布式锁
构想这么一个场景,咱们的用户须要独特操作同一个 App 实例,比方共同完成一场解谜游戏,用户 A、B 简直同时点击了“提交”,后端接到提交申请,判断答案正确,而后为游戏实例散发新的题目,此时,若后端在为 A 散发题目的过程中 B 的申请达到,且也给 B 散发新的题目,会导致 A、B 前端收到不统一的新题目。此外,还有一种场景,用户 C 因为弱网或其余起因,提交后未马上收到反馈,反复频繁地点击提交,将导致发动反复申请,用户较多且申请工夫集中时,容易导致负载稳定,影响服务质量。
因而,咱们有必要为“提交”减少一个分布式的锁,使在某个 App 实例里,所有工夫里,只能由一个用户提交。
通过 context.storage 实现分布式锁
实例播送
当对于某个 App 实例,某个用户提交通过失去新的游戏状态(新的谜面与候选词等)后,须要将状态同步给其余用户。实际上咱们能够将获取新游戏与状态写入本地游戏这两步拆散,在进行播送时本人也会接管到,所有包含本人在内的用户监听到播送立刻写入本地游戏。如图所示:
先获取新状态再通过播送进行状态同步的流程
咱们能够利用播送与监听 API context.dispatchMagixEvent(event, payload)
和 context.addMagixEventListener(event, listener)
上述性能:
在游戏状态发生变化(提交胜利和重置)时播送
监听播送产生,并依据具体事件做不同操作
至此,咱们的逾越前后端的实例通信局部也实现了,实现了用户对 App 实例操作时交互的同步,并解决了如同时、反复提交这类的并发问题。此类问题在其余互动利用的开发中也普遍存在,这里提供了一些参考。
08 小结
声网 Flat 开源我的项目提供了白板 SDK,反对开发自定义 App,为在线教育和白板利用提供了微小的设想空间。本次分享从一个首次接触 Flat 开发者的视角,介绍了互动白板的特点,并从基于理论例子——实现一款互动小游戏,分享了小游戏前端框架的抉择与应用、整体架构设计思路、后端开发流程等。同时介绍一些实用的 window-manager API,并在实战中如何应用这些 APIs 来疾速解决一些本来比较复杂的问题。心愿能对大家开发 Flat 白板自定义利用、在线互动小游戏中提供一些参考和帮忙。因为工夫仓促,仍存在许多有待欠缺和优化的点,请大家不吝指出。抛砖引玉,互动教育、教育游戏等在国内外仍有较大的市场前景,心愿与大家有更多的交换与单干,谢谢大家。
- 参考:
https://github.com/netless-io/window-manager/blob/master/docs/develop-app.md
- https://github.com/netless-io/window-manager/blob/master/docs/app-context.md
- 成语解谜:
https://github.com/Zhao-hangtian/happy-star
- 大赛官网:
https://www.agora.io/cn/rte-hackathon-2022
- 大赛作品仓库:
https://github.com/AgoraIO-Community/RTE-2022-Innovation-Challenge