综合实验日志

相关经验记录部分

  1. Cocos可以善用分包加载和资源远程拉取代替自己请求,但是注意首场景资源要带好,待加载完成后切换下一个场景。

  2. 动态加载资源:

    1
    2
    3
    cc.loader.load("url", (err, res)=>{
    //res就是加载到的资源,图片是Texture2D,音频是AudioClip,可以进一步判断
    })

    可以promisify

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function ccAsyncLoad(url: string){ 
    return new Promise((resolve, reject) => {
    cc.loader.load(url, (err, res)=>{
    //res就是加载到的资源,图片是Texture2D,音频是AudioClip,可以进一步判断
    if(err)reject(err);
    else resolve(res);
    });
    });
    }
  3. 把对象的某一属性设置为undefined,并不能阻止该属性在for in时被遍历到!
    如果不希望被遍历到,要delete才行。

  4. typescript 类型声明的语法:

    • declare

      1
      2
      3
      4
      5
      declare const qwq: string;
      declare function qwqwq(): string;
      declare class YingYingGuai{
      yingyingying: string
      }
    • 此外,还可以利用|关键字表示多种可能;

      1
      2
      3
      4
      5
      6
      function requestPromise(options: {
      method: "GET" | "POST" | "PUT" | "DELETE"
      })
      {

      }
    • 可以用&表示继承一个类并实现一个接口的对象类型:见下面mongoose的用法

  5. mongoose用法:
    schema决定了真正被存进数据库的对象。由mongoose.Schema({})定义:

    1
    2
    3
    4
    5
    6
    7
    8
    interface Data{
    qwq: string
    }
    let DataSchema = mongoose.Schema({
    qwq: String
    });
    //Data & mongoose.Document表示继承了mongoose.Document类(从而定义了save、find等方法),同时具有Data的各种属性的对象。
    let datas = mongoose.model<Data & mongoose.Document>(DataSchema);

    如果Data里面还有更复杂的类对象,可以为每个类对象都定义一个Schema。如果不定义Schema,数据是存不进去的!以下是最保险的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class YingYingGuai{
    yingyingying: string
    }
    let YingYingGuaiSchema = mongoose.Schema({
    yingyingying: String
    });
    interface Data{
    qwq: YingYingGuai
    }
    let DataSchema = mongoose.Schema({
    qwq: YingYingGuaiSchema
    });
    let datas = mongoose.model<Data & mongoose.Document>(DataSchema);

    但是也可以定义为Mixed,但是在每次修改内容时必须手动标记修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class YingYingGuai{
    yingyingying: string
    }
    interface Data{
    qwq: YingYingGuai
    }
    let DataSchema = mongoose.Schema({
    qwq: mongoose.Schema.Types.Mixed
    });
    let datas = mongoose.model<Data & mongoose.Document>(DataSchema);
    //但是在每次修改时必须标记修改
    datas.markModified("qwq");
  6. 微信APP开发如果想存储自定义数据,要存到本地用户文件目录。该目录以用户维度隔离。
    目录的常量:wx.env.USER_DATA_PATH,用正常的FileSystem操作,或者是直接uploadFile、downloadFile都可以。

  7. 如果await非async函数,是不会报错,就相当于立即同步调用该函数了。

文档部分

一、程序设计概述

微信小游戏在近年来热度逐渐攀升,每时每刻都有大量的精彩小游戏出现,更是有越来越多的游戏被移植到了微信小游戏平台上。分析其原因,我认为主要有两点:一是它便捷、容易获取,无需安装额外的安装包,通过如今几乎人人都有的微信即可下载,下后便玩,无需额外的安装;其次,它通过与微信的开放能力的有效结合,用户使用微信一键登录,无需注册,并通过微信内分享、关系链数据等实现与好友的密切互动和共同游戏。
本实验利用课上所学和课下了解的Web前端、NodeJS后端、微信小游戏开发等各方面的知识,单人独立完成了一个阿瓦隆微信小游戏的全部内容。
整个项目分为三个部分:基于NodeJS的阿瓦隆应用服务器后端、基于Cocos Creator的微信小游戏前端和微信小游戏开放数据域。在提交的代码和Github中,分别以三个项目目录存放:avalonServer, cocosAvaloncocosAvalonOpenData。每个模块实现的具体内容详见第二部分具体叙述。

本游戏在设计中的亮点包括:

  1. 不只是游戏,更是工程:游戏的前后端实现解耦合,后端的不同模块之间实现解耦合,从而本后端可以被使用于任何形式的阿瓦隆前端,而后端的网络通信、文字和语音聊天、游戏平台模块可以被移植到任何的网络游戏后端。
  2. 使用TypeScript强类型语言完成编写,并大量使用ES6等新标准的Promiseasync/await等模型,使代码编写变得简洁,拒绝回调地狱;后端使用长轮询机制代替轮询,极大的降低了服务器压力的同时优化了用户体验。
  3. 前端部分极少具有游戏逻辑,几乎所有的游戏逻辑都由后端完成;即使是简单的前端逻辑,也进行了解耦合,通过核心逻辑层——控制器——引擎渲染层的方式进一步实现了视图逻辑分离的目标。

本游戏在使用中的亮点包括:

  1. 功能详尽完整。不止有核心的逻辑部分,更有根据玩家信息给出详细的技术统计、利用开放数据域展示好友排行,面向不同层级的新用户的帮助、游戏内的文字和语音聊天系统等丰富的配套支持,让用户真正感觉自己在使用一个APP而不只是执行一段游戏代码。
  2. 游戏的核心逻辑部分也经过仔细的打磨,玩家模块以头像的方式显示,互动感更强;合理筛选应该在游戏界面和不该在游戏界面直接显示的信息,后者以可开关的侧边栏的形式展示,既让游戏界面简洁不乱,也充当了玩家的得力助手,在需要时随时为玩家提供游戏的详细信息,起到了APP应有的游戏助手的作用。
  3. 实现了聚会游戏的重要核心功能之一,虽然较为复杂但是有很大意义,也是目前市面上已有的阿瓦隆小程序都没有实现的功能:语音聊天。
  4. 虽然受本人本能力所限少有原创素材,但所有素材都经过本人精心打磨加工。游戏中图片素材总计达到百余个,语音素材则包括利用百度AI开放平台生成了0-10数字提示、相关提示语音、多个背景音乐等素材。
  5. 游戏UI字号较大,方便阅读;功能简洁明了,游戏交互节点在保持风格基本统一的情况下使用鲜艳而有明确含义的颜色标记,让对阿瓦隆有所了解的人基本上都能不用阅读说明书,仅从标志颜色和图案就推断出其含义。
  6. 游戏UI细节丰富,几乎所有的按钮都有点击特效,按钮附近有炫酷的粒子特效,游戏面板展示图文并茂,力争在充分发挥计算机优势的同时保持游戏的原汁原味。游戏动画效果十足,从面板的展示、按钮的出现、点击与消失、身份的分配、任务结果的揭示、游戏信息的提示、任务结果的切换等多方面都有动画效果和/或渐变效果。

本游戏虽然与一般想象中充满人机互动的游戏不同,更像是个游戏工具;但是游戏的可玩性不应止于人机互动游戏,在如今便携设备高速发展、反而人与人之间交流减少的今天,把传统的人人交互游戏与计算机融合,我认为也是一种很好的游戏形式,对于了解相关规则的人来说具有很好的可玩性。本游戏也尽力让不了解规则的人能够入门,因此制作了许多页的游戏规则介绍在游戏中。对这样一款“以玩家充分互动”为目的的游戏,让玩家互动的开心和扮演法官的角色同样重要,游戏的玩家系统、游戏的交互和游戏核心逻辑同样重要。游戏逻辑虽然简单,但在玩家的聊天互动中变得变化无穷,而APP要做的是给他们这样的平台,让他们忘记APP,找到游戏人的乐趣。
实际上,这个程序的设计思路参考了现有市面上的狼人杀APP。市面上的确有狼人杀APP和狼人杀小游戏,也有阿瓦隆的安卓APP(大小有50多M,需要安装),也有界面简单、除了发牌之外无其他功能的微信小程序,但具有友好的用户界面、丰富的游戏功能和详细的数据统计、内置聊天系统的免安装的微信小游戏,本程序还是第一个,它充分结合了上述所有应用的优点,可以说填补了现有的空白。

二、程序功能特点和实现叙述

1.概述

本实验的全部代码使用TypeScript语言编写。这种语言作为JavaScript的强类型扩展,能够更多的把错误消灭在编译时,综合利用了Java类语言强类型的安全性优势和JavaScript的动态类型的简便性优势。考虑到各平台的整体情况,后端编译到ES.Next标准,前端由Cocos引擎按默认方式编译(ES5标准)。
按照现代Web应用程序的一般规范,前后端之间实现了完全的解耦合,两者之间通过约定好的API进行通信。后端的API与前端渲染完全分离,只发送游戏状态等前端所需要的游戏信息,而把信息转化为对用户友好的显示的过程由前端完成。这样,这个后端具有良好的可复用性,可以无需修改地支持未来可能开发微信公众号后台版本、电脑版本等。

2.后端

后端使用了NodeJS的Express框架编写,主要包括服务器网络通信模块(URM)、聊天模块(talk)、游戏平台模块(platform)和游戏核心逻辑模块(game)四部分构成。

2.1服务器网络通信模块(URM)

与简单的Web页面有所不同,本游戏在客户端不断发送http请求给给服务器的同时。也需要服务器能够向客户端推送消息。最初采用的是轮询模式,但经过测试表明此方式有很大的弊端,主要是如果客户端请求的频率过大,服务器压力会过大;而请求的频率过小,又会导致状态不能及时刷新。为此采用了长轮询的模式:客户端向服务器发起的状态刷新请求(update)不会立即返回,而是挂起,直到服务器认为需要返回此请求时才会返回。
此外,如何设计网络请求模块使得后端的其他部分能够向调用http请求一样简单的往客户端推送消息也是很重要的问题。基于以上,设计了状态刷新请求处理平台(Update Response Manager),独立于后端的逻辑部分。
在用户第一次登陆发起请求时,会得到一个标识符id,之后用户的所有update请求应携带这个id。URM在监听客户端的update请求的同时,暴露emit、json等方法给游戏逻辑部分,同时也为更复杂的应用预留了sendFile等接口。任何时候游戏逻辑部分只需简单的调用此接口传送要发送的数据给URM,就可以在用户的请求到达时将数据返回回去。
该模块从游戏逻辑部分只接受json\file等http响应数据,因此实现了解耦合,可被接入到任何需要长轮询模式发送信息给用户的Web应用中去。

2.2聊天模块(talk)

聊天模块是比较复杂、综合运用了各种Web技术的部分。主要分为文字聊天和语音聊天。
结合实践,聊天模块实例化了一个独立的URM用于消息收发。
对于文字消息,它监听用户发送文字消息的请求,并调用platform提供的接口获得所有与该用户在同一房间内的玩家信息,向他们的URM推送消息。
对于语音消息,受限于微信API的机制,则更为复杂。用户调用微信API完成录制后,将音频以文件的形式上传到服务器。服务器收到文件后,将其存储到静态文件服务可访问的区域,并向所有同房间内用户通知该用户发送了语音消息,并携带该消息的URL。客户端收到后再调用微信API完整下载语音静态文件,交由Cocos引擎的语音模块予以播放。

2.3游戏平台模块(platform)

游戏平台模块基于现有的网络游戏的一般实践,建立了用户管理、游戏房间等系统。
用户管理主要包括用户鉴权与登录态维护、用户数据维护等方面。目前的鉴权采用的是微信认证的方式,通过APPID和APPSecret调用微信的服务端API完成用户登录和用户基本信息获取等操作。对于用户数据,采用了MongoDB数据库进行持久化存储,妥善保管用户的游戏信息。
此外,大多数多人网络游戏都有房间的概念。因此设计了独立的房间模块,内置建立、加入、退出房间、获取房间列表等操作接口。房间中不涉及任何的游戏逻辑部分,仅保存一个Game引用到游戏核心逻辑,同时实现了对URM模块的良好接入。因此只需改变Game对象的定义,以上两个模块即可被用于任何的Web游戏当中。

2.4游戏核心逻辑模块(game)

这一模块是按照阿瓦隆的规则,对游戏逻辑做出的具体实现。由于这是一个桌游,需要对用户的各种不同的行为做出相应这部分是工作量较大但是其中重复劳动量较大、可复用性相对低的部分。
它监听发送到game的请求,改变游戏状态,完成游戏逻辑的计算并返回给玩家(通过调用URM)。一个完整的游戏,从收到房主的gameStart请求开始,构造Game对象。随后,Game对象监听了大部分客户端主动发来的请求和大量使用URM向客户端推送消息。到一局游戏结束,把游戏的结果信息持久化存储到数据库,共未来可能的技术统计、反作弊等使用,同时通知platform模块改变数据库,修改玩家的得分等信息。

3.前端

由于后端的存在,前端事实上已经很少有游戏逻辑相关的内容,即使有也大多是对游戏内容加以简单转化(枚举值转为字面值、利用对象数据生成可渲染文字模板)等。
但是仍然尽最大努力实现了视图与逻辑的分离。整个前端首先基于cocos引擎,因此绕不开的是场景的概念。游戏有三个场景:预加载场景、登录与房间场景、房间内场景。
预加载场景只用于等待分包加载,待加载完成后会自动切换到登录与房间场景。
除此之外的各个场景内部,都采用了核心逻辑——控制器——Cocos节点与组件——渲染的模型。
每个场景有一个CoreLogic节点,仅挂载CoreLogic脚本组件,负责游戏的核心逻辑与数据初始化、与服务器进行通信、提供错误处理\确认面板等接口、操作控制器等。例如具体而言,房间内场景的CoreLogic组件(类名为CoreLogicRoom)具有初始化场景的功能,它的start方法和其中调用的异步方法roomInit利用上个场景传来的数据对游戏进行初始化;如果上次用户是中途退出的,则会自动请求相应接口刷新完整游戏状态供用户恢复到游戏当中,完全避免了“一人断线、全房遭殃”的局面。
此外它还建立了状态刷新请求循环,监听从服务器发来的事件;并提供registerEvent方法供其他地方注册事件监听。这样,其他部分代码只要监听某个事件,这样从外面看来,就如同是服务端主动给客户端推送的消息一样的处理方式即可,弥合了http一般应用中服务器和客户端在事件处理方面的差异。此外还提供了pressFor、pressAgainst等等等等方法,写好了请求API的具体方式,供其他节点只需简单调用就实现了把游戏事件发送给服务器。总之,CoreLogic负责那些不得不由前端完成的游戏逻辑工作,主要是网络请求。
课上讲过,“界面是状态的函数”,那控制器就相当于函数实现。控制器包含很多种,在本项目中大多以Control为结尾,如状态提示控制器StatusControl、玩家控制器PlayersControl、聊天控制器TalkingControl等等。它们暴露接口给CoreLogic(其中一般有一个render接口),CoreLogic利用网络通信等逻辑得到的游戏数据经过简单处理后传给控制器,并调用控制器的render方法,此步骤传输的还是对象数据。而控制器的render方法根据实际情况把对象数据变为文字、图片等数据,再调用该控制器控制的各个Cocos组件的各个接口完成渲染操作。
此外,微信提供的接口较为复杂,不便使用,在本例中由于反复使用而受影响较大的是网络请求接口。为此,实现了一个tool.ts包括对request、upload、download等请求的promisify化封装。对于一些错误处理、提示玩家再次确认、显示加载中等标志也都实现了promisefy化封装,例如加载中动画标志暴露给外界的接口就仅有async函数when,它接受一个promise,作用就是在传入的promise执行期间显示加载中标志,并传递该promise的resolve或reject结果。
基于以上的工具类,项目大量使用async/await编程风格,这样可以对其他函数在必要的时候任意地await,使得网络请求等必须异步的操作即之后的逻辑处理就像同步函数一样清晰;同时除部分动画的插值计算、滚动列表等的状态维护外,极少有在update里每帧调用的内容,避免了一些无谓的函数调用,也使得编程更自由灵活。

三、游戏操作说明

本游戏是一个名为阿瓦隆的多人聚会游戏,有点类似于狼人杀,其主要的游戏环节是所有玩家发言讨论。受篇幅所限,以下仅讲解简单的游戏操作,并且其内容已经完全被游戏内置帮助所覆盖。游戏内置的帮助图文并茂,建议您查阅游戏内置帮助,或自行参考更详尽的规则说明(来自网络)如下:https://www.jianshu.com/p/181377317cb3
您登录并授权后,首先进入的是登录界面。您可以创建房间、加入房间,或是查看好友排行、查阅帮助。(您第一次登陆时,系统会自动为您弹出帮助界面。)创建房间后,您会自动进入房间;在房间列表中点击房间按钮,会出现绿色的加入按钮,再点击即可加入。
游戏界面中,底部有一个大聊天界面,显示所有玩家的发言和游戏的关键信息;下面聊天框,您可以输入并发送消息;右下角语音按钮,按住说话,松开发送,功能同一般的微信语音聊天。
等待房间人满后,房主即可点击开始游戏按钮。之后丰富的语音和文字提示将会提示您当前的状态,足够清晰醒目的按钮只要您对规则稍有了解就会知道如何操作。
整个游戏的环节主要就是组队、投票、任务三个。
当到您组队时,点击组队玩家,他的头像周围出现黄色框表示表示被选中。选好后点击提交即可。游戏中每轮任务有固定的人数,是在游戏中间的五个指示灯提示的。从左到右数,第一个灰色指示灯表示当前第一个未执行任务,也就是本轮组队的所需人数。
投票按钮,绿色为同意,红色为反对;任务按钮蓝色为成功,红色为失败。
测试说明:考虑到您可能无法凑齐足够的人(至少5人才能开启游戏),后端已经开启了大量只缺1个人的房间。您直接加入即可在其中游戏。房间内除您以外的角色由简单的后端脚本控制,可以模拟人的行为。另外考虑到您和他人一起测试的需求,亦开设了一些缺2人的房间,您可以和其他体验者一同进入,测试聊天等功能。如果您人够当然建立一个一般的房间直接游戏更是可以的。

四、游戏优化与游戏测试

针对本游戏实际遇到的问题,进行了大量的测试与优化。
由于本游戏设计是需要许多玩家参与的,为了便于测试,最好有由机器控制角色与测试者一同游戏。为此,专门在后端编写了脚本,脚本中有代码可以模拟人的行为,见test目录下。
此外,还提供了API供测试者随时调用,在测试房间不足时随时新建测试房间;设计了服务器测试脚本,可以同时开启大量的房间,对服务器表现进行测试,测试脚本位于test/LocalTest.ts和TestOnePerson.ts。结果表明对于约200个用户在房间内的情况,服务器运行正常。

针对前端遇到的问题,也进行了各种优化。

  1. 针对图片文件数量过多和尺寸过大的问题,使用了TexturePackerGUI将大量的图打包为图集(Atlas),在Cocos中直接使用;
  2. 针对反复轮询消耗大量哪个服务器资源的问题,考虑到WS具有应对不稳定连接能力较差等问题,最终选用了长轮询作为解决方案。结果表明在长轮询模式下,服务器性能表现良好。
  3. 针对原来使用富文本RichText实现的房间列表在房间数较多时加载较慢的情况,查阅资料知道Cocos对富文本的实现比较复杂;此外渲染房间列表也并不需要反复渲染一些固定的文字,如“梅林”、“派西维尔”等。最终在此处放弃了富文本,改用普通Label进行渲染。结果表明在本人手机的测试条件下,渲染30个房间的列表耗时从约8s下降到2.5s。