当前位置: 首页 > Web前端 > HTML

基于AcousticNetworkFlat搭建白板插件应用“成语解谜”的最佳实践

时间:2023-03-29 11:04:57 HTML

前言本文作者为赵航天。曾参加“2022RTE编程挑战赛”——“Track2场景化白板插件应用开发”,并凭借作品“习语解谜”获得赛道奖。《成语解谜》是一款基于交互式白板SDK开发的交互式小游戏应用。通过前端编码、白板API调用能力、后台逻辑定制等,实现了一款老少皆宜、寓教于乐的成语益智游戏。流程、步骤及相关技术栈在白板交互应用开发中具有一定的通用性。本文将分享项目的开发过程,包括一些关键功能的实现。希望与您交流,共同进步。可访问game.willtian.cn/idiom2/在线体验作品。01为什么要选择这么小的游戏?有几个原因。2000年上小学的时候,我第一次接触电脑和教育软件。里面有一些小游戏,真的是引导我学习了一些东西,比如一些名词概念,科学常识,对孩子很有帮助。.“白板”这个词给我的第一感觉就是回到了校园。在学校能遇到好同学好老师,有很多美好的回忆。小时候喜欢看成语词典,喜欢看故事书,后来在课堂上也玩过一些成语拼图之类的字谜游戏。20年的疫情期间,我会在家玩一些益智休闲游戏。能够玩到自己制作的游戏,我感到非常开心。另外,这类游戏很适合碎片化时间,让用户学到一些东西。特别适合喜欢休闲游戏的儿童和成人;对于长辈来说,操作更友好,内容更容易引起共鸣。从市场和社会的角度来看,它们都是有价值的。02什么是交互式白板SDK交互式白板官方名称为声网Flat(点击文末“阅读原文”了解更多),官方解释为:《私教的在线教学软件》可以直接使用,开箱即用,前后终端完全开源,可以快速搭建简单美观的在线课堂。”运行时,初始界面是这样的:交互式电子白板初始界面左侧的工具栏图标告诉我们,这是一个可以写、可以画的东西。它具有以下特点:1.交互性,每个房间对应一个交互式白板,默认房间内所有人都可以操作白板,并且交互效果对所有人可见;2、扩展性,除了基本的书写和涂鸦功能外,交互式白板支持自定义应用(点击工具栏底部的“田”字型图标可查看所有应用);个人认为支持各种APP是平板交互白板最强大的功能。通过Flat提供的SDK能力,我们可以实现很多复杂功能的白板应用。每个房间对应一块白板。交互式白板的内容,包括文字、涂鸦、App,都可以通过SDK中的WindowManager对象进行控制。您可以通过官方demo快速熟悉一个App的开发流程。利用窗口管理器的API接口,我们可以完成应用实例通信等操作,具体例子见下文。03架构规划在展开具体实例之前,首先介绍一下“成语解谜”项目的总体框架。如下图,我们将前后端分离,前端专注于页面绘制和交互,后端专注于问题生成和结果判断。用户在访问前端页面时无需下载完整词库,大大提高了访问速度。前端使用WindowManager的contextAPI接口同步广播Agora服务器上的App实例。前端App实例、SonicNet服务、游戏后台通信04界面设计我们采用“设计驱动”的开发模式,先画出设计图,再一步步将脑海中的画面变成现实code:designsketchgamemaster界面设计如上,交互设计如下:1.谜语上随机出现若干成语,这些成语与常用字符相关联作为生成约束。2.挖掘出成语之间的共通字,随机排列为候选字;3.用户通过“触摸->拖->放”候选词的交互操作完成猜谜;4、“提交”得到用户谜语的判断结果,对应通关和失败场景;5、【重置】将谜语和候选词恢复到游戏初始状态;6、“答案”通过弹窗显示灯谜中所含成语的信息,包括字体、读音、释义、出处和用例;(对于更复杂的场景,建议把直接场景切换的逻辑全部画出来,形成一个比较完整的需求文档)抓住主要矛盾,优先开发核心功能,产品原型实现后,继续打磨化解次要矛盾。05前端开发完成游戏的基本界面设计后,我们开始选择前端框架,完成界面开发。适合游戏开发的前端框架有很多,比如Three.js、Phaser、Cocos2d-js等,根据具体需求而定。个人感觉Three.js比较底层,写游戏的代码量可能比较大。Cocos2d-js封装度高,需要熟悉Cocos工具链。对于非职业玩家来说,上手难度大,技术可移植性不高。这里选择的是PixiJS,PixiJS是一个基于2DWebGL的渲染引擎,兼容HTML5Canvas。它有一系列合理、干净的API,支持Sprite,将对象抽象成各个层次的Container。类似于React/Vue的数据驱动设计,在PixiJS中,可以通过修改Container的参数来改变用户界面。Pixi的API实际上是最先被Flash使用的。经过反复改进,有Flash经验的同学非常好用。入口以“成语解谜”为例,介绍一下编码的一些细节。首先我们找到自己代码的挂载点,根据文档中给出的demo或者本文提供的例子找到这个入口文件:自定义应用的入口(src/index.js)noticeconstbox=context.getBox();在这一行中,框对应于此应用程序打开的窗口。我们通过box.mountContent将包含App实例$content的div容器挂载到窗口。App类接下来,我们定义App类。关键代码如下。App类(src/app.js)App类包含一个PIXI.Application实例。此外,App类还持有一些与App维度相关的变量和方法。例如:从setup(见src/index.js)传入的context(用于调用WindowManagerAPI)、App实例的id(用于区分App实例和前端)、layers(图层)、resizeObserver(用于监听界面变化和适配布局)getRandomString(生成每个游戏的token,用于后端交互)、storage(用于访问Agora服务器上App的状态)等。场景类我们为每个场景写一个场景类,这里只有一个场景。App类实例化Scene类并使用addChild将场景实例添加到渲染中。接下来我们为主界面写一个Scene。关键代码如下:Scene类(src/scene.js)实例化了Scene构造函数中的“Submit”、“Reset”、“Answer”三个按钮,并定义了相应的事件。我们在Scene中实例化类Idiom,Idiom的一个实例对应一组字谜和候选词,Idiom有一个子对象Piece,Piece对应每一个具体的词块。由于Scene的按键事件功能的需要,我们在Scene类中写了保存/读取Piece状态的方法。Idiom类&Piece类在Idiom类中,我们定义了拼图和候选(Piece)棋子的生成方法、重置方法、拖拽生效方法。在Piece类中实现拖动时的外观行为。Idiom类(src/idiom.js)Piece类(src/piece.js)整体效果主界面运行效果06后端开发实例关联与隔离由于词库比较大,用户每次操作会消耗大量时间loadacompletelexicon带宽和时间对用户体验影响很大。我们通过后台搭建,服务于谜语的获取、提交结果的验证、答案的获取,提升用户体验。上面“架构规划”中提到,我们和每个App实例都持有一个token,用来和后台通信,对应后台的游戏实例对象。UserGames的密钥是令牌。后端收到浏览器的请求后,会在UserGames中搜索对应的游戏实例BoardGame,获取当前游戏状态,包括拼图表、答案、answerDetail等。使用UserGames的key(token)来隔离游戏实例,关联前端App实例生成谜语如何生成谜语?基本算法思路是:1.对成语库进行预处理,建立所有成语的词索引NthOfChar*[]map[rune][][]rune,保存第n个词为m的信息;2.使用DFS递归搜索谜语。在当前成语中找到一个词k作为下一代起始节点,根据约束选择一个新的成语和新成语的放置:a.k必须出现在新成语中;b.放置新成语后必须保证当前谜语不被破坏;索引NthOfChar用于实现搜索过程中的剪枝;多解兼容性生成算法形成的拼图会同时生成唯一的答案。但实际上,答案未必是唯一的,尤其是在成语较多的情况下,交流几句也能得出一个合理的答案。在这种情况下,我需要对用户提交的成语进行逐一验证。如果成语库中共有N个成语,则为成语库中的成语生成字典树Trie,可以将搜索时间复杂度从O(N)降低到O(1),最多搜索4次。全局单例负责游戏实例生成的结构。GlobalBoard存储了全量的成语和中间数据信息。作为全局单例,减少了内存拷贝;对于每个问题(拼图)请求,它直接返回GlobalBoard生成的结果的副本。使用全局单例和状态拷贝优化内存使用07App实例通信实例状态的同步至此,我们基本实现了单机游戏。但是当我们打开两个浏览器标签模拟多用户操作时,我们会发现App的交互只对当前用户生效,其他用户是察觉不到的。表现为用户A打开App,将其拖到App窗口中合适的位置,开始游戏,将候选词与空词块交换,然后提交;同时,用户B在同一个房间,但只看到A打开App,拖拽App,看到的App内容与A的app显示内容不同步,无法感知A对A的操作app(可以看到A的鼠标光标移动,就是Flat背后的同步逻辑)。对于目前的问题,我们可以很自然地想到,一定有某种机制可以让用户在本地操作App实例后,将状态同步到一个所有用户都可以访问的远程服务,然后通知所有用户保存将远程服务的状态同步到本地App实例,重新渲染App画面,实现多用户交互。说到这里,你可能会想,我们写的后端服务能不能加上同步功能呢?我们来思考一下,做这样一个同步功能需要做些什么:1.设计一种通信机制,让本地实例能够主动感知远程状态的更新;2、处理超时、重连、弱网等;3.延迟低,足以承受业务波动的负载;4、服务经过全面测试,足够稳定;仔细思考后会发现,稳定可靠的实时通信其实是一个比较大的话题,不应该是实现业务、产生业务价值的关键一项主要工作,换句话说,就是做的输入输出轮子靠自己不高。AcousticNetwork在实时网络通信领域深耕多年。基于自身的技术积累,在Flat项目中提供了一系列非常好用的通信API。这些API的设计类似于React,并且相对易于使用。接下来,我们使用这些API进行同步和广播来解决交互性的问题。我们回到前端代码,在app.js的App类中做一些改动:初始化实例的存储,我们为每个App实例持有一个存储对象,存储对象来自于获取的上下文白板应用程序已创建。这里storage.ensureState用于确保storage.state包含一些初始值。context.storage实际上与远程服务的存储实例相关联。它实时监控本地存储的变化。当发生变化时,它会自动同步最新的存储到服务器。即使是不同的用户,同一个房间的同一个应用实例,其实也会对应同一个远程存储。画个图更直观:存储关系图。要了解storage的同步特性,我们需要做的就是当context.storage有变化时Updatecontext.storage,并添加回调事件监听context.storage的变化,将远程context.storage同步到游戏中(应用实例)。我们封装了state的push/pull方法,让代码更易维护。这里的storage.setState类似于React的setState,更新storage.state并同步到所有客户端。游戏状态->远程存储添加监听事件,addStateChangedListener在有人调用storage.setState()后触发(包括当前存储),这里我们写逻辑将远程存储同步到游戏状态。远程存储->游戏状态分布式锁想象这样一个场景,我们的用户需要共同操作同一个App实例,比如一起完成一个益智游戏,用户A和B几乎同时点击“提交”,后台connects来提交请求,判断答案是否正确,然后为游戏实例分发新的问题。这时候,如果后端正在为A分发题目,B的请求到了,又分发新题目给B,就会导致A、B前端收到的新题目不一致。另外还有一种场景,用户C因为网络弱或者其他原因,提交后没有立即收到反馈,频繁重复点击提交,会导致重复请求。当用户较多,请求时间集中时,容易造成负载波动。影响服务质量。因此,我们有必要为“提交”加一把分布式锁,使得在某个App实例中,始终只有一个用户可以提交。通过context.storage实现分布式锁实例广播。对于一个App实例,当用户提交并获得一个新的游戏状态(新的谜语、候选词等)后,需要将该状态同步给其他用户。其实我们可以把获取新游戏和写状态到本地游戏这两个步骤分开。广播的时候,我们也会收到。包括我们自己在内的所有用户都会收听广播并立即将其写入本地游戏。如图:先获取新状态,再通过广播同步状态我们可以使用广播监听APIcontext.dispatchMagixEvent(event,payload)和context.addMagixEventListener(event,listener)以上函数:当游戏广播监听广播时状态发生变化(提交成功并重置),根据具体的事件做不同的操作。至此,我们的跨前后端的实例通信也已经完成,实现了用户与App实例操作交互的同步,处理诸如同时,重复提交这样的并发问题。此类问题在其他交互式应用程序的开发中很常见,这里提供一些参考。08总结声网Flat开源项目提供白板SDK,支持自定义APP开发,为在线教育和白板应用提供了巨大的想象空间。本次分享从一个刚接触Flat的开发者的角度介绍了交互式白板的特点,并结合一个实际的例子——完成一个交互式游戏,分享了游戏前端框架的选择和使用,整体架构设计思路、后端开发流程等。同时介绍了一些实用的window-managerAPI,以及如何在实战中使用这些API快速解决一些原本复杂的问题。希望能为大家开发平板白板定制应用和在线互动游戏提供一些参考和帮助。由于时间仓促,还有很多需要改进和优化的地方,欢迎大家指出。国内外互动教育和益智游戏仍有很大的市场前景。希望能与您有更多的交流与合作。谢谢。参考:https://github.com/netless-io/window-manager/blob/master/docs/develop-app.mdhttps://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