本文共 15495 字,大约阅读时间需要 51 分钟。
mota-js是一款用于做出魔塔类型游戏的HTML5 2d游戏引擎(),目前最新的版本是v2.66,由于原主力开发已经工作,因此很长一段时间没有大版本的更新。
最近在用样板做一个游戏的时候,体会到了样板的一些限制。主要有:1. 地图尺寸受限,超过一定尺寸(30x30)会到达性能瓶颈,尤其是在手机上会很卡顿。2. 素材尺寸受限。贴图种类被限制在32x32或32x48,尺寸更大做起来会很麻烦,没有一个精灵系统。3. 实现一些特效很困难。样板中大量使用了dom来分割地图场景与状态栏UI,一些联动特效难以实现,并且使用dom对做游戏开发而非前端开发的人来说是个噩梦。
去年针对这些问题改了一版pre3.0的运行时系统,但现在回看感觉问题很多,首先是设计模式选取不当,造成理解困难,其次对原样板的耦合太多,被原有框架所限制,导致渲染引擎的能力没有发挥出来。因此这两天决定重启项目,解决之前的问题。
这里主要施工的部分是运行时,编辑器有另外一位大佬在施工。游戏引擎的运行时系统,如果是复杂的游戏,其系统规模将会是庞大的,如图是《游戏引擎架构》一书中对现代游戏引擎的系统架构描述。
这样的架构在unity、ue4等引擎中有完整的体现,但在我要施工的对象上不可能有这么多。 HTML5游戏是运行在浏览器上的,因此浏览器解决了A1-A3、B、C以及部分D的问题。其次因为是2d游戏,许多核心系统中的东西用不到。然后因为魔塔是棋盘类单机游戏,位置是确定的方格,因此也不需要碰撞检测、骨骼动画、在线多人等模块。最后,因为是在原样板基础上进行的二次开发,很多游戏算法都已经实现过了。因此算下来,借助成熟的第三方库,工作量并不大,对预期的架构图修剪如下,预期工期在15天左右。调研第三方库:
实现资产管理(AssetsManager)。
资产管理中存在的坑:实现动画管理(AnimationManager)、精灵管理(SpriteManager)的部分功能。
实现一个地图的原型:瓦片地图绘制与角色移动。
对系统框架的进一步总结:
记录一个坑:
ES6中的箭头函数()=>{} 与 function有一个重要区别,那就是箭头函数中的this绑定的是函数体外的,也就是说在声明的时候就绑定了this,而不是像function一样在调用的时候才绑定。这两部分需要后续补充的内容:
今天主要实现事件系统和消息管理的部分架构。
之前的进度到了勇士能在地图移动,但是没办法和地图上的事件进行交互,主要就对这块进行施工。
事件,在传统角色扮演类游戏中一般特指会产生一系列剧情、动作的触发点或者npc,角色,通常指的是主角和npc一类会动会产生行为的对象。
在设计事件系统的架构的过程中,理论上可以把地图上能跑能动的有属性的对象都当成是角色,但纯图块(block)、和角色(带事件的块如npc)和勇士(玩家控制的对象)显然不属于同一种,单明显带有一种递增的关系:block -> actor -> hero (具体的关系等之后施工完了再完善)
在实现中,如果把所有带事件的点,都当成是一个个“角色”,那么勇士触发事件就可以当作是与角色之间的【交互】,简单的例子比如碰撞事件。
实现碰撞事件过程如下:
这个过程实现的是【碰撞】这类交互,但是实际游戏中,不止碰撞,比如有的点是空点,有的点是【战后事件】、【拾取后事件】……对原样板有的或没有的总结有如下类型的事件:
事件类型:
其中打!的是在地图上定义的事件,这些可能还不是全部,那么问题来了,要对所有事件单独写代码去判断吗?原样板是这么做的,感觉工作量会很恐怖,我怕工期赶不及,所以有了下面的消息管理。
消息管理(MessageManager)是一个处理行为产生的消息的模块。
这里定义一下“行为”的概念,一般来说,游戏中的对象都能产生行为,但不是所有行为都会产生消息。比如之前的控制器,是用户行为,但由于控制器是同步的,即时控制勇士,没有必要产生消息。但是勇士的行为会产生消息,因为勇士走出去触碰地图点,可能会产生诸如碰撞、到达、离开等一系列情况,这些情况是不由勇士自己处理的,必须交由消息中心处理。当勇士发送消息时,是一个生产者,消息管理中心的任务是找到一个消费者处理这个消息。比如前面11类事件,就是11种以上的消息,这些消息会在对应的代码执行时,如果有消费者成功消费了这条消息,就会返回消费情况,让生产者来进行判断下一步的情况——这个过程中,生产者不需要知道自己的消息被谁消费。这样只需要在合适的地方加入消息生产,就可以比较快速灵活地实现以上事件的处理。
在原样板中,事件与地图点是绑定的,这在写一些剧情的时候会很头疼,npc不能频繁移动,否则会满地都是事件点,真·移动事件可以解决这个问题。
在上面的事件实现中,所有事件点都会成为角色(Actor),角色可以包含数据,这个数据可以用于定位事件原点。这样移动事件本质上是移动角色,角色移动后,原点将不存在块,勇士不再触发事件,但触发移动后的角色时,就会触发其定位到原点的事件。 这一块刚开始做,需要对事件系统进行进一步的施工。首先处理两个bug。
第一个,开启新页面时,一些sprite会加载不出来,经过调试发现是PIXI的材质缓存机制导致的,开启strict模式即可。 第二个,在手机上出现点击失效的情况。经过调试发现是强制横屏时,指针对象没有做对应的适配导致的,对tink.js的源码进行修改后修复。然后基本完成移动事件和一个简单的打字机,然后做了一部分异步处理。
移动事件中移动事件点的关键在于对块信息和事件信息及时修改,事件管理以及地图管理订阅事件移动时发出的leave和arrive消息,对事件和块进行重定位即可。打字机之前就留了空,做起来也简单了,做了一个动画管理,申请一个【等待】的动画对象,作用于对话框绘制中的【依次绘制每个字符】,然后处理好回调即可。另外实现了一个控制字符\w,可以控制说话的速度,比如下面图就改变了两次语速。
动画这块主要的问题在于解决异步,比如,如何实现说话过程中执行下一个事件?目前还没有好的思路。(打字机演示图因审核问题删去
最近事多,估计工期又要延后了……
今日主要做了两个部分,一个是对资产管理进行了一定的补充,之前是基于原样板的图集包装的,为了测试sprite的优先级变化,就用PIXI导入了其他类型的材质,然后在编辑器里做了一些改动,通过调整数据的材质来改变贴图
另一个是试图解决异步的问题。据说有一种叫做promise的东西,我去找来看看大概明白了是怎么回事,但似乎原生的实现效率并不高,而且不一定适合引擎里的架构,就先在角色类和动画类进行实验,因为这两部分异步最多。
比如角色类,主要是移动,涉及到发送消息、移动动画,如果不用promise,会出现大量回调,可读性很差。 用promise改进后移动是这样的:const dx = core.utils.scan[direction].x, dy = core.utils.scan[direction].y;this.createAction('leave') .then(success=>{ if(this.animate.walk){ this.isMoving = true; this.animate.walk .get(this.sprite, { direction: direction, time: this.moveSpeed}) .onChange(()=>{ this.refreshPriority()}) .call(success); }else success() }) .then(success=>{ this.trasnform(dx, dy); this.isMoving = false; success(); }) .send('arrive') .then(success=>{ callback(); success(); });
其中涉及动画的部分使用了一个onChange,用于移动过程中的优先级变化。
这种形式相比于不停callback看上去好多了,当然仍然没有解决多个角色同时移动异步问题,因为这个还没有实现Promise.all的效果。这个留到明天解决。
今天打算完成异步事件执行的部分。
角色部分全部架构基本完成,目前全部写在ActorManager文件中,后续如果有增加新的角色部分可以考虑拆分,目前没必要。
关于多角色异步执行(即昨天提到的promise.all),本质上和原样板没有区别,就是用一个唯一code挂在全局,执行完毕后回调取消掉code。当所有异步code都取消时,即为完成一次all。
以一个行走事件的异步过程为例。
角色每次移动开始时和移动到达后都要发送消息,事件管理和地图管理接收该消息,并对地图和事件数据进行修改(实现事件移动的方法),此外还需要在移动开始前和结束后对异步进行记录,如果在移动的过程中,执行了等待全部异步事件(类似promise.all),则会在消息中心挂载一个一次性的回调函数,等到全部执行完毕后进行调用。同样也能实现竞争式如promise.race的效果。但目前暂未用到。目前有一个问题尚未解决——事件的碰撞,比如A要到B的位置,B要到C的位置,看上去能执行成功,但发往事件管理和地图管理的消息出现了竞争——无法确定谁先到达,如果A先到,发现B处已经有一个块,就会发生重叠的错误情况。预想的解决办法是同一个点碰撞后成为一个队列,先进先出,这样一来,即使A到达B的时候,B还没离开,在B离开时也能正确取出自己的事件。
最后做了一些关于地图特效的实验和接口,学习了一下PIXI包装的filter,实现一个简易的色调变化。但后来尝试做光影但遇到了一些困难,这块还是缺乏一些理论基础,有空了补一下。但特效毕竟不是目前引擎的重心,明天重点还是做核心的部分,至少要能执行完一个魔塔游戏的基本流程。
明日施工计划对象:事件系统(修bug,以及继续做基本事件的补充),战斗系统。
总结一下目前实现的结构。
AssetsManager: 资产数据库单例,管理包括材质、敌人信息、道具信息、技能信息、事件信息、角色信息等原始静态数据。只有加载这些数据后,才能初始化后续三部分。
SpriteManager:精灵管理单例,处理包括角色精灵、动画精灵、窗口精灵等动态数据的基本管理。后续计划做一个精灵缓冲池,防止进行大量增删行为(比如浏览地图)带来的开销。 AnimationManager:动画管理单例,实现各种特效的地方。提供动画执行单元实例的获取接口。 BattleManager(施工中):战斗管理单例。进行战斗数据管理,主要包括获取敌人数据、战斗伤害的计算。由于战斗本身是属于事件的部分,所以这部分纯粹是作为一个API接口,如,输入勇士信息,查询敌人、获取敌人信息并返回,不涉及到对实际运行数据产生的影响——但实际运行中的数据会影响到这里的计算结果。ControlManager:控制管理单例。包括两部分:1. 用户的输入指令管理 2. 输入指令后产生消息的管理。
MessageManager:消息处理单例。负责汇总各种消息来源的消息,并分发给各个监听者。 Listener:监听基类,相当于是为MessageManager专门配的一个接收者。所有被动接收消息进行处理的管理器都需要继承此类。 SceneManager:场景管理单例。负责场景绘制,包括状态栏、菜单栏、地图界面、UI界面等。 MapManager:地图管理单例。负责地图的状态存储,包括地图上的角色信息,地形信息等。 EventManager:事件管理单例。负责响应事件消息,如移动事件、战斗事件、自定义事件、转场事件等。今天首先做了一个Listener类,把之前的几个消息接收者都归总了起来,使之具有高扩展性。
使用的一个例子如下,增加一个新的战前事件,改变角色的属性:EventManager.on('beforeBattle', (obj, callback)=>{ obj.xxx = ...//处理可以异步,比如可以进行动画播放一类的操作 callback();//处理结束后要通知处理完毕})
理论上所有继承了Listener的实例都能注册接收这个消息然后进行处理,也能work,但是不应该这么做。因为在目前的实现中,如果消息接收者都进行异步处理,那么将是一种伪并发状态,无法确定先后,而事实上,不同模块的消息处理优先级是不同的,所以后续可能会调整模块消息接收的优先级,部分模块的消息处理很可能需要等其他模块都完成后才进行。
然后是战斗系统。借用了一些原样板的战斗计算内容,实现了技能的部分。
战斗系统是魔塔类游戏的核心,属于一种固定数值的回合制战斗,即勇者、敌人每回合互相造成伤害,直到一方倒下,在没有加特殊技能的情况下,其结果是能够通过公式解析出来的。在传统的三原塔里(4399的50层、新新、24层),是有战斗动画演示这个回合过程的,但在现代魔塔游戏里(以RM魔塔、H5魔塔为代表),这个战斗动画被基本取消了,玩家更多的重点关注在路线中,尤其以H5为甚,不仅取消了动画,还引入了瞬移,以加快游戏节奏,此外,玩家还需要查看大量的伤害数据,以及更高阶的数据信息,包括临界减伤表(加x点攻击减少x点伤害)、防御减伤表(1防减少x点伤害)等。这使得战斗系统有一定的计算负担,但是传统的战斗算法是有解析解的,所以问题不大。
但是,在一些蓝海塔加入一些特殊技能后,比如“第x回合造成x点伤害”、“怪物每回合增加x点防御”、这种,很难有解析算法,再加上魔塔中勇者数据是变化很快的,到处都是引起属性变化的宝石,使用缓存计算基本不太可能,很可能在计算一些大数据塔的过程中产生严重的卡顿。
因此关注战斗系统,首先就对技能进行一定的关注。技能本质上是一个影响战斗进程的特殊变量,正常的战斗过程如下(感觉这个过程可以叫做战斗管线了…):
怪物技能可以继承一个基类,基类的以上函数全部留空,即按默认来。技能覆盖对应的函数后,可以对特定过程的数据流发生变化。
举例来说,【硬化皮肤】技能1:怪物的防御力额外增加勇者50%攻击的数值。那么就继承getEnemyInfo函数,将敌人的防御数据修改即可。
但这么修改也有问题:修改不是线性的,多个技能时会发生冲突。比如有个技能2:【防御强化】怪物防御力增加20%。 因此每个函数添加一个修改单元,存放每个技能产生的修改结果,主要有两种,一种是百分比(percentage)、一种是固定数值变化(hardchange),接下来的写法就是:function( src_info, //原始信息 可参考 不可修改 modify_info, //修改信息,用来存放修改结果 ){ let hero = src_info.hero_info;// 来自上一个流程的结果 modify_info.def = ~~(hero.atk * 0.5);// 写法1:固定变化数值 modify_info.def = 0.2;//写法2:这样写的效果就是自身防御上升20%,会在固定数值修改完成之后进行}
一般来说这个模型对于大部分数值类的技能是够用的,但对于一些特定需求可能无法满足,一方面,比如勇者的某种属性依赖于怪物(比如勇者防御力增加怪物的某个属性值),另一方面比如回合类的技能,其本质是循环,需要对伤害计算进行大量修改,暂时不考虑。
此外一个优化点,现代魔塔的地图显伤包含了上图过程2、3、4的计算,而且互相独立,据鹿神介绍可以用Worker实现异步计算,明天学习一下。今天继续完善战斗系统。
昨天提到的Worker去看了,发现这个东西需要独立的上下文环境进行线程计算,和主线程之间只能通过通信进行交互,这就很麻烦了。后来想到可以用空闲计算的方法进行异步计算,但目前测试还没有到性能瓶颈,先暂时放弃优化这块。
战斗部分完善伤害计算,将显伤加入地图场景中。效果如下:
目前已经能够完成一个最简单的游戏流程:打怪、捡宝物、切换地图、对话,但还缺少一个重要的模块:状态管理。这里的状态,指的是包括勇者的数值、游戏变量、录像等实时信息,之所以要对这块进行管理,是因为涉及到一个重要功能:存读档。
SL大法玩过游戏的都知道,遇事不决就存档是rpg中的常见操作,在魔塔中更甚,玩一座有一定难度的塔会产生大量的存档,因为其中包含大量的路线分歧,经常需要频繁存读档,再加上H5有一个【自动存档】功能,因此对存读档的性能有一定的需求。 就之前的经验看,当塔层数较低(低于一百层)时,几乎不会有卡顿,但在层数上升到一百多以上时,由于地图数据读档和存档过程中反复刷新,会产生一定的延迟。 因此,如果要用样板制作大型的蓝海塔,有必要对存读档进行优化。明天进行状态管理部分的施工。
状态管理参考了一些博文( ),将游戏中状态分为如下几个部分:
本质上来说,这些都可以归为变量,但在考虑到对状态的存档读档的时候,又有一些区别,其中勇士状态和进程在存读档时是需要完全存储和加载的,不可分割,但是地图和事件就不一定了。
举例来说,一个游戏玩到第三关,后面还有四关没打,这时读取第二关的存档,那么存档中关于后面四关的信息是无需加载的,存储也一样。这可以通过脏标记来实现。但这样还不够。当游戏进行到中后期,已经改动了很多的地图状态,脏标记已经很难有优化效果了,需要另外找办法。
通过对原样板的观察测试发现,此时存读档的主要开销在于对全部地图数据的记录和读取,测试中两百层地图的读取大约需要200毫秒(5fps),这对逐帧绘制的一些特效会产生明显卡顿,可以通过懒加载的方式避免:只对读档的目标地图进行加载,其他的部分等到访问时再进行加载。
存档相对高效一些,但是也只有10fps,在自动存档时,也会影响一些需要高fps的画面,存档的优化可以通过建立存档树来解决。存档树演示如下:
假设存档3是之前试错的一条路,通过读档回存档1后,进行另一个选择,存储了存档2。注意到,无论是存档3还是存档2,都是在存档1的基础上进行的改动,那就意味着:存储的时候不必存储全部内容,只需要存储相对于父存档的改动即可。这个本质上也是一种脏标记的应用,但是会在每一次存档的时候,清除脏标记,所以脏标记会很少。
但是如果存档进行了这样的改动,读档也必须与之匹配才行。如果只存储相对改动,这无疑会增加读档的开销:读档需要去找存档树的关系进行拼接,不断查询存档,这是很费时的,因为存档不是全部都存在内存中。
这就产生了矛盾:加速存档,就会减慢读档,加快读档,就会减慢存档,有没有两全其美的办法,既能迅速读档也能迅速存档?……很遗憾,暂时没有,但可以优先解决自动存档,这样就解决了大部分可能的卡顿。
在游戏过程中,触发最为频繁的是自动存档,自动存档指的是在进行一些操作,如切换地图、战斗、开门等不可预知行为时进行的存档,相当于上个保险,防止误操作。目前的版本中支持一定步数的连续回档,即自动存档成一个队列,可以回到前几步的状态。
自动存读档是典型的适合存档树+懒加载的优化点,原因是每次存储和读档都修改极小,而且存档都在内存中(自动存档不会全部持久化),是一个天然的链式结构,因此可以对其进行着重优化。
实现上,先实现一个最简单的缓存基类,用于优化自动存读档。包含方法有:
然后地图管理包含一个继承缓存基类的对象,切换楼层以及对图块增删时进行标脏。
在战斗后加上自动存档,测试发现每次存储量都很小(一张地图),存取时间约20毫秒,基本不影响性能。 明天再考虑如何做手动存读档以及更复杂的树形分支。考虑以下三个基本功能:(规则1)
回退是一个之前没考虑的新功能,就我个人来说,一般用于load手滑多退了一步的情况,但也有说能用来分析路线?不过这些不重要。
演示如下,状态1~3是存储的三个存档,当前状态是未保存的进度。
往回读(load):
此时无法使用back,原因是前一个【当前状态】并没有存储,已经丢失(如果在读取时进行了存储则另算,先不考虑)。
再次load:此时可以通过back回到状态3。
为了实现手动存档读档,将在这个模型基础上对save、load、back进行第一次扩展:(规则2)
如下:
这样可以实现链式存档的手动存读。但存在一个问题:如果在读回状态1后,进行了新的状态保存,就会变成:
此时之前的模型不能适用,将再次扩充(规则3):
改动比较大的在load,演示如下,从状态4读到状态3:
这样实现的性能瓶颈在于查询各个存储的状态然后进行合并。据鹿神说并发读取存档开销并不大,试了一下localforage,读取900个存档只用了70ms。因此存读这块并不是问题,难的是如何实现这一块。有可能并不会需要这种LCA操作,一定深度后做一次全存是一个好的方法。
明天再尝试进行具体的实现施工…
存档实现预期超过预期时间,放弃,改为懒加载优化。
存档时:如果有没有修改过的楼层,就不必重新压缩,直接存入。
读档时:读取未解压的存档,只有访问目标楼层时,才解压目标楼层。完善材质类型,增加对tileset的支持。
调研自动元件的实现:
原样板的绘制方法不再适合PIXI的框架,需要基于ActorSprite增加一种多模态的元件,情况略有些复杂,但原理不变。
自动元件竣工。
每个自动元件图块包含四个小sprite,通过放置在四个角落拼凑为一个完整的图块,这四个图块的模式一共47种情况,由九宫格的边角决定。
上面的参考博文给出了“绘制情况-小元件”的映射表,但没有给出“边角-绘制情况”的映射,在此记录如下:
// javascriptlet edge = { };/** * 对mask符合filter的edge填充角落 * @param value * @param filter */function fillCorner(filter, value, mask) { mask = mask || 0xf; for(let i = 0; i < (1<<8); i++){ if((i & mask) == filter){ edge[i] = value; } }}// 0 边fillCorner(0, 47);// 1 边fillCorner((1<<0), 42); // 下fillCorner((1<<1), 43); // 右fillCorner((1<<2), 44); // 上fillCorner((1<<3), 45); // 左// 2. 2边fillCorner((1<<0) + (1<<2), 32); // 下 + 上fillCorner((1<<1) + (1<<3), 33); // 右 + 左 —— 对角无影响fillCorner((1<<1) + (1<<0), 35, 0xf | (1<<4)); // 右下*fillCorner((1<<1) + (1<<0) + (1<<4), 34, 0xf | (1<<4)); // 右下* —— 4fillCorner((1<<1) + (1<<2), 41, 0xf | (1<<5)); // 右上*fillCorner((1<<1) + (1<<2) + (1<<5), 40, 0xf | (1<<5)); // 右上* —— 5fillCorner((1<<3) + (1<<2), 39, 0xf | (1<<6)); // 左上*fillCorner((1<<3) + (1<<2) + (1<<6), 38, 0xf | (1<<6)); // 左上* —— 6fillCorner((1<<3) + (1<<0), 37, 0xf | (1<<7)); // 左下*fillCorner((1<<3) + (1<<0) + (1<<7), 36, 0xf | (1<<7)); // 左下* —— 7// 3. 3边// 缺左 左角无影响// 右满fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<4) + (1<<5), 16, 0xf | ((1<<4) + (1<<5)));// 右下fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<4), 17, 0xf | ((1<<4) + (1<<5)));// 右上fillCorner((1<<0) + (1<<2) + (1<<1) + (1<<5), 18, 0xf | ((1<<4) + (1<<5)));// 无右fillCorner((1<<0) + (1<<2) + (1<<1), 19, 0xf | ((1<<4) + (1<<5)));// 缺上fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<4) + (1<<7), 20, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 下满fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<7), 21, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 左下fillCorner((1<<0) + (1<<1) + (1<<3) + (1<<4), 22, 0xf | ((1<<4) + (1<<7))); // 缺 上 + 右下fillCorner((1<<0) + (1<<1) + (1<<3), 23, 0xf | ((1<<4) + (1<<7))); // 缺 上// 缺右fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<6) + (1<<7), 24, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左满fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<6), 25, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左上fillCorner((1<<0) + (1<<2) + (1<<3) + (1<<7), 26, 0xf | ((1<<6) + (1<<7))); // 缺 右 + 左下fillCorner((1<<0) + (1<<2) + (1<<3), 27, 0xf | ((1<<6) + (1<<7))); // 缺 右fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<5) + (1<<6), 28, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 上满 (存疑 26 27 44 45 ?)fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<6), 30, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 左上fillCorner((1<<1) + (1<<2) + (1<<3) + (1<<5), 29, 0xf | ((1<<5) + (1<<6))); // 缺 下 + 右上fillCorner((1<<1) + (1<<2) + (1<<3), 31, 0xf | ((1<<5) + (1<<6))); // 缺 下// 4. 4边let four = (1<<0) + (1<<1) + (1<<2) + (1<<3);// -------- 右下 右上 左上 左下 -----------edge[four + (1<<4) + (1<<5) + (1<<6) + (1<<7)] = 0;edge[four + (1<<4) + (1<<5) + (0<<6) + (1<<7)] = 1; // 缺左上edge[four + (1<<4) + (0<<5) + (1<<6) + (1<<7)] = 2; // 缺右上edge[four + (1<<4) + (0<<5) + (0<<6) + (1<<7)] = 3; // 缺左上 右上edge[four + (0<<4) + (1<<5) + (1<<6) + (1<<7)] = 4; // 缺右下edge[four + (0<<4) + (1<<5) + (0<<6) + (1<<7)] = 5; // 缺右下 左上edge[four + (0<<4) + (0<<5) + (1<<6) + (1<<7)] = 6; // 缺右下 右上edge[four + (0<<4) + (0<<5) + (0<<6) + (1<<7)] = 7; // 缺右下 右上 左上edge[four + (1<<4) + (1<<5) + (1<<6) + (0<<7)] = 8; // 缺左下edge[four + (1<<4) + (1<<5) + (0<<6) + (0<<7)] = 9; // 缺左上 左下edge[four + (1<<4) + (0<<5) + (1<<6) + (0<<7)] = 10; // 缺左下 右上edge[four + (1<<4) + (0<<5) + (0<<6) + (0<<7)] = 11; // 缺左上 左下 右上edge[four + (0<<4) + (1<<5) + (1<<6) + (0<<7)] = 12; // 缺左下 右下edge[four + (0<<4) + (1<<5) + (0<<6) + (0<<7)] = 13; // 缺左下 左上 右下edge[four + (0<<4) + (0<<5) + (1<<6) + (0<<7)] = 14; // 缺左下 右上 右下edge[four] = 15; // 都缺
最后得到的一个edge,是一个边角情况到绘制情况256-47的映射,这个可以作为绘制依据的查询。
地图的进一步完善是实现大地图。当前的地图只能提供一定宽度的显示,超过则会溢出到边界,无法正常运行。要实现更大尺寸的地图,在此先引入摄像机的概念。
Camera一般在3d游戏中使用比较多,因为涉及到成像透视等一系列需求,需要这个概念来帮助理解。在2d游戏中因为是平面的缘故,一般都是用画布概念,动画和游戏过程就是不断绘制的过程。但涉及到遮挡的时候,画布就无法帮助理解了。
2d摄像机本质也是一个画布,但它对应着一个游戏实体,也即照射的场景(Scene),之前做场景管理,不仅管理场景中的数据,还混入了渲染,而引入摄像机后,渲染的过程就应放到摄像机中,其逻辑为:
摄像机要存储视角(viewPoint),作为场景绘制的依据,其次,还需要一个渲染区域(renderArea)作为绘制目标——即画到窗口的何处。当绘制超出边界时,需要进行剪裁。最后,为了让视角跟随主角,需要有一个绑定对象到视角的方法。
目前实现能够在电脑上基本能够较为流畅地绘制一个52x52的大地图,但在低性能手机上会严重的帧率下降(下降至20fps),推测是由于大量的sprite更新导致的,可以考虑进行优化——使用缓冲池,只对当前画面的一部分进行刷新。
给大地图加了缓动后基本竣工,留下两个问题:1 手机上的性能优化 2 读档优化(大地图读档会导致大量sprite申请和销毁 这是没必要的),等之后细节优化再进行吧,今天主要进行事件部分的施工。
事件部分的改进点如下:
架构的施工记录到此基本接近尾声,后面就是添砖加瓦充实游戏性系统、细节优化以及找bug。对其中有价值的部分会专门开文章写,这篇不会再更新了,主要最近又忙起来了,不知道咕到什么时候才能全部做完……就这样吧。