研究一下物理引擎,做了个游戏~

原文首发于微信公众号:MinProgram 

原作者:花叔   

Cocos 已获得转载授权


前阵子过春节,在家看小孩没法出去玩,于是就码起来了,心血来潮想做个基于物理引擎的小游戏,于是愉快地打开了 Cocos Creator。

1

然后到这个周末,一个算是较为完整的游戏 demo 上线了。

2

负责的工作

1年多前,我开始学习小游戏开发,并尝试按全栈的方式去独立开发小游戏。由于做小游戏都抱着学习的心态,所以什么不擅长就临时学一下。最后整个游戏的策划、交互、视觉设计、音效设计、前端开发、后台开发、性能调优、功能测试等工作都一人包揽了。

所用到的 IDE 包括(仅供参考,其他同行有更好的工具推荐的话,记得留言哦):

  • Photoshop,主要用于交互和视觉稿的设计。
  • 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.内容建设的手段

而为了解决内容构建的问题,我为游戏提供了一个开放的模式,那就是:玩家可自主创建关卡

11
10

这样的话,关卡是能动态新增的,任何人都可以随时随地在手机上去为本游戏构建关卡,我也可以脱离电脑很方便地为游戏设计初始关卡。

12

这需要我在游戏开发中应用到原本网页设计的组件化思维,要把游戏里可能会出现的元素抽象为通用组件,然后构建一个编辑模式,让玩家拖动放置,进而设计出不同的关卡。

3.虚拟货币的换算机制

游戏引进了金币的概念,它是整个游戏世界里的通用货币,目前构思中给金币赋予的作用有两个:a. 以 20 比 1 的方式兑换提示机会;b. 以 2000 比 1 的方式购买卡车皮肤(暂未实现)

13

而金币的获取方式比较简单粗暴:签到、分享和看视频(还没到 1000 累计注册用户,所以没放开功能)

13
15]

Cocos Creator 开发经验

鉴于内容太多,总的开发逻辑就不细说了,这里给一下开发过程中一些有用的经验。

关于布局

Cocos Creator 为本游戏中提供了两种实用布局组件:

1.Widget,这能让某个元素自适应到任意位置。这是相当实用的控件了。使用方法非常简单,各种机型的自适应布局一下子就被兼顾了。

16

2.Layout,这能让游戏中的 Node 具备如网页 dom 一般的流行布局特性。它提供了常用的三种流体布局方式:横向、纵向以及网格。该组件对于本游戏的控件容器特别合适。

17

关于动画

Cocos Creator 提供的动画开发套件非常强悍。

18

这有两个点可以提一下:

1.动画定义,Cocos Creator 里为某个 Node 追加动画时,只要给它加 cc.Animation 组件,然后建立一个 animation-clip,针对它去可视化编辑动画的属性和帧状态,就能快速做出一个动画。

19

2.动画事件监听,Cocos Creator 的动画控件里有一个我觉得非常实用的功能,那就是可以为动画的某一帧定义自定义监听事件(该事件代码体可定义在 Node 对应的用户脚本组件中),例如物理的游戏里过关时会有个弹框动画,在动画播放到差不多的时候,会播放一个音效,利用的就是动画的自定义事件。

20

 关于物理引擎

 Cocos Creator 的物理引擎相当强悍,只要在程序开端执行:

cc.director.getPhysicsManager().enabled = true;

整个游戏世界就会进入物理监听状态,所有被定义成刚体(追加了 cc.RigidBody 组件)的 Node 将直接具备物理性质,通过追加 PhysicsCollider 控件可让 Node 具备指定热区的物理碰撞特性。

21

碰撞体有一个挺有用的方法,getAABB,这是获取碰撞体包围盒的方法,可以用其结合 rect 的 containsRect 方法来实现对某个矩形区域里是否包含某碰撞体的功能,在物理的游戏中箱子与目标区域的监听功能就是依靠它来实现的:

22

具体代码:

//获取自身包围盒   var selfAABB = this.node.getComponent (cc.PhysicsPolygonCollider). getAABB()   if (otherAABB.containsRect(selfAABB)) {   }

此外,物理引擎里还提供了一种比较实用的组件:关节组件。

它可以定义一些常用的物理场景,比如本游戏里,汽车轮子上用的就是物理引擎的 WheelJoint 控件:

23

该控件可以模拟机车轮子的物理效果,让轮子跟某个刚体保持一定距离,并能自转。

24

其实除了这类型的关节组件外,官方还提供了很多别的关节组件,具体的用法可以参考 Cocos Creator 的官方开发文档

对了,还要提一下:

cc.director.getPhysicsManager()

这段代码能返回全局的物理管理对象,本游戏用了该对象下面的 2 个方法:

1.testPoint 方法,该方法可以检测某个坐标点下是否存在物理碰撞体,物理的游戏中在某个刚体上禁止画线的功能就是依靠它来实现的:

25

2.rayCast 方法,该方法可以获取指定出发点到终点间射线所经过的刚体集合,物理的游戏中画线遇到刚体后禁止继续的功能就是依靠它来实现的:

26

这个方法的原生实现逻辑相当复杂,各种几何算法什么的,反正几何数学没学好的花叔如果要以原生方式实现,也只能勉强实现很挫的效果,但 Cocos Creator 直接就封装好供大家调用了,非常方便。

关于预制对象

预制对象是 Cocos Creator 中很重要的节点处理机制,https://docs.cocos.com/creator/manual/zh/asset-workflow/prefab.html,它可以把某个节点像场景那样单独存为一个文件,然后在不同场景中引用,并通过:

cc.instantiate()

方法进行预制对象节点的复制,这样可以实现节点逻辑的复用,用来做节点组件最合适不过了。

物理的游戏中”创作模式”下的所有地图元素其实就是基于同一个预制对象。

27

不管是”创作模式”还是“闯关模式”里的地图元素的基础数据模型均来自该预制对象,如果需要新增地图元素,只要修改这个预制对象即可全局生效,可以说非常方便了。

但大家要注意,预制对象对性能有一定的反向作用,具体可以翻翻 Cocos Creator 的论坛,可以说它是双刃剑。

关于 camera

听说 2.0 版本的 Cocos Creator 对 camera 的逻辑进行了优化,我试用了一下真好用,目前 camera 是跟 group 绑定的,在游戏中可以定义多个 camera 来处理 node 的Color/Depth/Stencel,物理的游戏中用 camera 实现了各种元素的层级处理(给 camera 定义 Depth,然后指定不同 group 到 cullingMask 中)

28

此外依靠 camera,可以快速实现截图或放大镜的效果:

29

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,这里就不展开细说了:https://github.com/cocos-creator/demo-camera

关于性能调优

讲真,毕竟我也不是太深入地去了解过 Cocos Creator 的底层原理,所以性能优化这块就只能从自己这个项目来给点小技巧,以下稍微讲讲:

物理的游戏刚有初版的时候,性能特别糟糕,后来做了三项优化工作。

1.自动合图

降低 DrawCall 是提升游戏渲染效率一个非常直接有效的办法,而两个 DrawCall 是否可以合并为一个 DrawCall 的一个非常重要的因素就是这两个 DrawCall 是否使用了同一张贴图,所以官方是建议合图的。

但我项目中用了那么多碎图,这时候让我去合图,岂不搞死我!苦恼之际以外发现 Cocos Creator 提供了一个强大的功能“自动合图(Auto Atlas)”

30

不用不知道,一用吓一跳,这功能可以把当前目录以及子目录下所有的图片文件以指定的算法去合并成 sprite 图,并且自动更新原有 spriteFrame 的引用,一下子就把全部碎图合成大图来按需调用了。

31

网络请求直接从几十个变成一两个。

然而….我发现 drawcall 也没降低多少。没花太多时间去想为什么,这个方式我就先不关注了。

2.减少节点

节点如果没来得及释放,那么一定会导致 drawcall 上升,往这个方向想,我就想到 cc.instantiate(),在物理的游戏里只有这个方法会主动去新增节点,那么只要产生的临时节点及时销毁就行,但发现其实在复制完都调用了 removeFromParent() 方法,逻辑好像是对的。

但后来查了一下资料,原来 removeFromParent 方法执行后,节点并不会自动销毁,真正能让它销毁的是 node 的 destroy 方法。囧大了。应该换成 destroy。于是就全局搜索 removeFromParent,逐一替换,drawcall 就顺利降下来了。

3.优化代码逻辑

除上述常规手段外,针对自己的代码也需要做一下优化,本游戏的代码逻辑中最有可能优化的地方是“画线”部分:

32

画线的主要逻辑是:

  • 当前场景 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 就性能优化来说,还提供了“节点池”和”动态合图”的优化方式,本游戏目前还没有应用上,未来也许可以试试。

最后

终于把要讲的讲完了,最后感慨一下。

花叔怎么说也从事互联网快 10 年了,现在对于平时的工作也不需要单纯的执行,也许从旁指导一下就算完事了。所以有人会问:你怎么还孜孜不倦地画图、写代码、做 demo 呀?还需要吗?

我就给他说了一件事,从前有一天,我指导另一个同事的项目,振振有词有理有据,但同事反问我一句:你多久没写代码了?这里面是 xxxxx 这样的。

我瞬间懵了一下。虽然还是答上去了,但后来我就在想,幸好他这个项目我以前研究过,不然就凭那句“你多久没写代码了”,我就直接把主导权送出去了。

企业获利大多是基于信息不对称,谁是信息上游,谁就更有可能控制局面;管理者、策划者给的是方向,固然重要,但信息不对称的事情大多在于技术层。

我见过 cp 怎样用技术忽悠需求方,特别是高新技术层面的事情,忽悠起来简直轻而易举。

有时候,我们转型、通道转通道、技术转管理,以为是找了个更好更适合的方向。

其实我看到更多可能的:1.专业到了瓶颈,对瓶颈逃避而选择转型罢了;2.专业能力突出被授予管理权力,然后就以为要转型了;3.真的不适合本专业

第三点且不说,前两点,何曾想过你想转向的领域,缺你吗?你苦心经营的技术不时而锻炼就会淡忘,就愿意因为一点点小瓶颈而舍弃吗?

技术和管理本不冲突,但为何要因管理而丢弃技术,失去对技术创造力的追求。

还能不能好好定义自己是什么人哦?

最后,引用一前辈的话:“我这人习惯自己先搞清楚怎么实现再指导。”

谁不想当信息的上游啊。

你以为我平时指导多内部小游戏的研发,靠的是吹水咩….


花叔,是一名腾讯的 UI 工程师,同时也是一名游戏开发狂热爱好者,欢迎各位开发者点击【阅读原文】与作者直接交流,也欢迎关注他的微信公众号:MinProgram,小小程序,大大思路!