前阵子过春节,在家看小孩没法出去玩,于是就码起来了,心血来潮想做个基于物理引擎的小游戏,于是愉快地打开了 Cocos Creator。
- Cocos Creator,主要用于视觉还原、主逻辑开发、跨端调试和编译。个人觉得这是超给力的游戏开发工具,把 component 的机制直接可视化,集成各种物理引擎、粒子引擎、UI 组件等功能,大大节省了游戏UI以及部分特效逻辑的研发成本。但小游戏版本的编译速度还是很慢啊。
- Visual Stuido Code,用于代码开发。这好像是最近最流行的代码撰写工具了,免费且稳定啊!
- 微信开发者工具,用于微信私有功能开发,云开发,小游戏提审。相比早期的版本微信开发者工具的体验优化了很多,但在文件监听侧还是有点问题,跟 Cocos Creator 的联动,经常会出现 Cocos Creator 编译好文件,微信开发者工具会报错的问题。
- Cool Edit Pro,音频资源裁切。
- TexturePacker,sprite资源集自动构建。
- Google Chrome,web版游戏调试。有些时候一些性能调优可以放在 chrome 上面,它有非常专业的调试工具。
从涉及的 IDE 看出来,其实开发一个游戏需要兼顾好多内容,尽管游戏提供的功能并不多,但实际研发时间却不短,花了整个春节假期(WTF,明明就几天)实际上,游戏开发相比页面开发,我认为前者更难,它更像一门综合技术,世界观构建、内容建设、美术构建、音效设计、程序算法等等,很多方面都需要涉猎,这里面的每项内容都够我钻研很长一段时间了。题外话,请善待独立做游戏的人,他们都在以高负荷的工作压力在研发和学习。(文章篇幅略长,想看鸡汤,不想看技术细节,可直接跳到最后)门外汉的游戏策划
虽然自身策划能力很业余,但在动手开发前,再简陋的策划步骤还是省不了的,预先策划有助于快速开发。
1.游戏的主线
早前预想的故事场景是开卡车在林中运输木材,核心玩法很简单:把木箱子运到指定的区域。整个游戏基于仿真物理场景,唯一一点超现实的功能是用蓝墨水画的线条会被实体化,会成为物理碰撞体,玩家可借此搭建桥梁、容器、障碍物等方式来协助搬运。
2.内容建设的手段
而为了解决内容构建的问题,我为游戏提供了一个开放的模式,那就是:玩家可自主创建关卡。
这样的话,关卡是能动态新增的,任何人都可以随时随地在手机上去为本游戏构建关卡,我也可以脱离电脑很方便地为游戏设计初始关卡。
这需要我在游戏开发中应用到原本网页设计的组件化思维,要把游戏里可能会出现的元素抽象为通用组件,然后构建一个编辑模式,让玩家拖动放置,进而设计出不同的关卡。
3.虚拟货币的换算机制
游戏引进了金币的概念,它是整个游戏世界里的通用货币,目前构思中给金币赋予的作用有两个:a. 以 20 比 1 的方式兑换提示机会;b. 以 2000 比 1 的方式购买卡车皮肤(暂未实现)
而金币的获取方式比较简单粗暴:签到、分享和看视频(还没到 1000 累计注册用户,所以没放开功能)
Cocos Creator 开发经验
鉴于内容太多,总的开发逻辑就不细说了,这里给一下开发过程中一些有用的经验。
关于布局Cocos Creator 为本游戏中提供了两种实用布局组件:
1.Widget,这能让某个元素自适应到任意位置。这是相当实用的控件了。使用方法非常简单,各种机型的自适应布局一下子就被兼顾了。
2.Layout,这能让游戏中的 Node 具备如网页 dom 一般的流行布局特性。它提供了常用的三种流体布局方式:横向、纵向以及网格。该组件对于本游戏的控件容器特别合适。
关于动画
Cocos Creator 提供的动画开发套件非常强悍。
这有两个点可以提一下:
1.动画定义,Cocos Creator 里为某个 Node 追加动画时,只要给它加 cc.Animation 组件,然后建立一个 animation-clip,针对它去可视化编辑动画的属性和帧状态,就能快速做出一个动画。
2.动画事件监听,Cocos Creator 的动画控件里有一个我觉得非常实用的功能,那就是可以为动画的某一帧定义自定义监听事件(该事件代码体可定义在 Node 对应的用户脚本组件中),例如物理的游戏里过关时会有个弹框动画,在动画播放到差不多的时候,会播放一个音效,利用的就是动画的自定义事件。
关于物理引擎
Cocos Creator 的物理引擎相当强悍,只要在程序开端执行:cc.director.getPhysicsManager().enabled = true;整个游戏世界就会进入物理监听状态,所有被定义成刚体(追加了 cc.RigidBody 组件)的 Node 将直接具备物理性质,通过追加 PhysicsCollider 控件可让 Node 具备指定热区的物理碰撞特性。
碰撞体有一个挺有用的方法,getAABB,这是获取碰撞体包围盒的方法,可以用其结合 rect 的 containsRect 方法来实现对某个矩形区域里是否包含某碰撞体的功能,在物理的游戏中箱子与目标区域的监听功能就是依靠它来实现的:
具体代码://获取自身包围盒 var selfAABB = this.node.getComponent (cc.PhysicsPolygonCollider). getAABB() if (otherAABB.containsRect(selfAABB)) { }
此外,物理引擎里还提供了一种比较实用的组件:关节组件。它可以定义一些常用的物理场景,比如本游戏里,汽车轮子上用的就是物理引擎的 WheelJoint 控件:
该控件可以模拟机车轮子的物理效果,让轮子跟某个刚体保持一定距离,并能自转。
其实除了这类型的关节组件外,官方还提供了很多别的关节组件,具体的用法可以参考 Cocos Creator 的[官方开发文档]对了,还要提一下:
cc.director.getPhysicsManager()
这段代码能返回全局的物理管理对象,本游戏用了该对象下面的 2 个方法:
1.testPoint 方法,该方法可以检测某个坐标点下是否存在物理碰撞体,物理的游戏中在某个刚体上禁止画线的功能就是依靠它来实现的:
2.rayCast 方法,该方法可以获取指定出发点到终点间射线所经过的刚体集合,物理的游戏中画线遇到刚体后禁止继续的功能就是依靠它来实现的:
这个方法的原生实现逻辑相当复杂,各种几何算法什么的,反正几何数学没学好的花叔如果要以原生方式实现,也只能勉强实现很挫的效果,但 Cocos Creator 直接就封装好供大家调用了,非常方便。
关于预制对象预制对象是 Cocos Creator 中很重要的节点处理机制,它可以把某个节点像场景那样单独存为一个文件,然后在不同场景中引用,并通过:
cc.instantiate()
方法进行预制对象节点的复制,这样可以实现节点逻辑的复用,用来做节点组件最合适不过了。物理的游戏中"创作模式"下的所有地图元素其实就是基于同一个预制对象。
不管是"创作模式"还是“闯关模式”里的地图元素的基础数据模型均来自该预制对象,如果需要新增地图元素,只要修改这个预制对象即可全局生效,可以说非常方便了。但大家要注意,预制对象对性能有一定的反向作用,具体可以翻翻 Cocos Creator 的论坛,可以说它是双刃剑。
关于 camera听说 2.0 版本的 Cocos Creator 对 camera 的逻辑进行了优化,我试用了一下真好用,目前 camera 是跟 group 绑定的,在游戏中可以定义多个 camera 来处理 node 的Color/Depth/Stencel,物理的游戏中用 camera 实现了各种元素的层级处理(给 camera 定义 Depth,然后指定不同 group 到 cullingMask 中)
此外依靠 camera,可以快速实现截图或放大镜的效果:
touchmove 回调方法中的相关代码:
var camera = this. assistCamera. getComponent ( cc.Camera ); // 新建一个 RenderTexture,并且设置 camera 的 targetTexture 为新建的 RenderTexture,这样 camera 的内容将会渲染到新建的 RenderTexture 中。 let texture = new cc.RenderTexture(); let gl = cc.game._renderContext; // 如果截图内容中不包含 Mask 组件,可以不用传递第三个参数 texture.initWithSize(100, 100, gl.STENCIL_INDEX8); camera.targetTexture = texture; // 渲染一次摄像机,即更新一次内容到 RenderTexture 中 this.assistCamera.x = touchLoc.x this.assistCamera.y = touchLoc.y camera.render(); var sf = new cc.SpriteFrame(texture) this.node.targetAssist. getComponent(cc.Sprite). spriteFrame = sf
想要了解 camera 的用法,还能研究一下官网提供的 demo,这里就不展开细说了.
关于性能调优讲真,毕竟我也不是太深入地去了解过 Cocos Creator 的底层原理,所以性能优化这块就只能从自己这个项目来给点小技巧,以下稍微讲讲:物理的游戏刚有初版的时候,性能特别糟糕,后来做了三项优化工作。
1.自动合图: 降低 DrawCall 是提升游戏渲染效率一个非常直接有效的办法,而两个 DrawCall 是否可以合并为一个 DrawCall 的一个非常重要的因素就是这两个 DrawCall 是否使用了同一张贴图,所以官方是建议合图的。 但我项目中用了那么多碎图,这时候让我去合图,岂不搞死我!苦恼之际以外发现 Cocos Creator 提供了一个强大的功能“自动合图(Auto Atlas)”
不用不知道,一用吓一跳,这功能可以把当前目录以及子目录下所有的图片文件以指定的算法去合并成 sprite 图,并且自动更新原有 spriteFrame 的引用,一下子就把全部碎图合成大图来按需调用了。
网络请求直接从几十个变成一两个。 然而....我发现 drawcall 也没降低多少。没花太多时间去想为什么,这个方式我就先不关注了。
2.减少节点: 节点如果没来得及释放,那么一定会导致 drawcall 上升,往这个方向想,我就想到 cc.instantiate(),在物理的游戏里只有这个方法会主动去新增节点,那么只要产生的临时节点及时销毁就行,但发现其实在复制完都调用了 removeFromParent() 方法,逻辑好像是对的。 但后来查了一下资料,原来 removeFromParent 方法执行后,节点并不会自动销毁,真正能让它销毁的是 node 的 destroy 方法。囧大了。应该换成 destroy。于是就全局搜索 removeFromParent,逐一替换,drawcall 就顺利降下来了。
3.优化代码逻辑: 除上述常规手段外,针对自己的代码也需要做一下优化,本游戏的代码逻辑中最有可能优化的地方是“画线”部分:
- 画线的主要逻辑是:当前场景 instantiate 一个用于画线的全屏尺寸的预制对象
- 监听节点上 touchmove,每次移动的时候对它上面的 cc.Graphics 组件进行 lineTo 的画线处理,同时存储每个移动点
- 用所有收集的移动点根据一个算法去构建 node 的物理碰撞区域
所以移动点越少越好,这样的话,优化的手段就有两个.
1.当前移动点与上一个移动点的直线距离少于一个限定值,就认为当前移动点无效。(相当于强制移动点的距离) 相应的判断代码很简单,利用 Cocos Creator 提供的向量求距的方法即可:
return lastPoint.sub(nowPoint).mag() >= 5;
2.“当前移动点跟上一个移动点的移动方向”如果跟“上一个移动点跟上上个移动点的移动方向”一样的话,那么上一个移动点即可销毁不做记录。(相当于把直线部分的移动点压减为两端点) touchmove 回调中相应代码如下:
//获得增长向量 var addVe = thisPos.sub( thisPos.prevPos ) //如果有前一个增长变量 if(thisPos.prevPos.prevPos){ //获取上一个点的增加向量 var lastAddVe = thisPos.prevPos.sub(thisPos.prevPos.prevPos) //如果两个增长向量相等,则这次的点替换前一个点 if(Math.abs( addVe.signAngle( cc.v2(1,0) ) - lastAddVe.signAngle( cc.v2(1,0)) )<0.1){ this.points[this.points.length-1]=thisPos this.physicsLinePoints[this.physicsLinePoints.length-1]=thisPos }else{ this.points.push(thisPos); this.physicsLinePoints.push(thisPos); } }else{ this.points.push(thisPos); this.physicsLinePoints.push(thisPos); }
依靠上述 3 种方式,我大概能把 drawcall 控制在 50 左右,但其实效果还是能优化。 Cocos Creator 就性能优化来说,还提供了“节点池”和"动态合图"的优化方式,本游戏目前还没有应用上,未来也许可以试试。
,