前言
本期游戏来自腾讯基础支持团团长@花叔。
正文从这里开始~~
前段时间春节,在家照顾孩子,没法出去玩,就开端写代码,一时兴起,想做一个基于物理引擎的小游戏,就开心的开了它(以下简称ccc)。
然后这个周末,一个相对完整的游戏demo上线了。
视频效果如下:
负责任的工作
一年多前,我开始学习小游戏开发,尝试用全栈的方式独立开发小游戏。因为开发小游戏的时候总是抱着学习的心态,所以会临时学点自己不擅长的东西。最后,游戏策划、交互、视觉设计、音效设计、前端开发、后端开发、性能调优、功能测试等工作都由我包揽了。
用到的IDE包括(仅供参考,如果有其他同事有更好的工具可以推荐):
:主要用于交互和视觉设计。
:主要用于视觉还原、主逻辑开发、跨端调试编译。个人觉得这是一个超强的游戏开发工具,直接将机制可视化,集成了各种物理引擎、粒子引擎、UI组件等功能,大大节省了游戏UI和一些特效逻辑的研发成本。不过小游戏版本的编译速度还是很慢。
Code: 用于代码开发。这个好像是最近最火的代码编写工具了,免费又稳定!
微信开发者工具:用于微信私密功能开发、云端开发、小游戏审核等。相比早期版本,微信开发者工具的体验有了很大的提升,但在文件监控方面还存在一些问题,与ccc联动时,ccc编译文件时经常会报错。
Cool Edit Pro:音频资源修剪。
:资源集自动构建。
:网页版游戏调试,有时候可以做一些性能调优,有很专业的调试工具。
从涉及的IDE来看,开发一款游戏其实需要考虑的东西非常多,虽然游戏提供的功能并不是很多,但实际研发时间也不短,占用了整个春节假期(WTF,才几天啊)。
其实相比于网页开发,我觉得游戏开发更难一些,它更像是一门综合技术,需要世界观构建、内容构建、美术构建、音效设计、程序算法等多方面的知识,这里面的每一点内容都足够我研究很久了。
另外,请善待那些制作独立游戏的人。他们都在高工作压力下开发和学习。
初学者的游戏规划
虽然我的规划能力很业余,但在开始开发之前,我仍然不能跳过哪怕是最简单的规划步骤。提前规划有助于快速开发。
1.游戏主线剧情
之前设想的故事场景是驾驶卡车在森林里运送木材,核心玩法很简单:将木箱运送到指定区域。
整个游戏以模拟的物理场景为基础,唯一超现实的地方是用蓝色墨水画出的线条会实体化,成为物理碰撞体,玩家可以利用它们搭建桥梁、集装箱、障碍物等,辅助运输。
2.内容构建手段
为了解决内容建设的问题,我为游戏提供了一种开放的模式,那就是:玩家可以自主创建关卡。
这样就可以动态添加关卡,任何人都可以随时随地在手机上为游戏构建关卡。我也可以方便地在没有电脑的情况下为游戏设计初始关卡。
这就需要我将原本应用于网页设计的组件化思维运用到游戏开发中,需要将游戏中可能出现的元素抽象成通用的组件,然后构建一个允许玩家进行拖放的编辑模式,从而设计出不同的关卡。
3.虚拟货币的兑换机制
游戏引入了金币的概念,金币是整个游戏世界的通用货币,目前金币被赋予两种功能:a.以20比1的比例兑换提示机会;b.以2000比1的比例购买卡车皮肤(尚未实现)。
获取金币的方式比较简单粗暴:登录、分享、观看视频(目前注册用户不足1000人,因此功能未开放)
ccc开发经验
由于内容太多,就不详细讲整体的开发逻辑了,下面说一下开发过程中的一些有用的经验。
关于布局
ccc为该游戏提供了两个实用的布局组件:
1.这个可以让元素自适应到任意位置,这个很实用的控件,使用方法很简单,一下子就考虑到了各种模型的自适应布局。
2. 让游戏中的Node具备了类似Web DOM的流行布局特性,提供了三种常用的流体布局方式:水平、垂直、网格,特别适合作为本游戏的控件容器。
关于动画
ccc提供的动画开发工具包非常强大。
有两点需要提一下:
1.动画定义:在ccc中给Node添加动画时,只需要给Node添加一个cc.组件,然后创建一个-clip,通过可视化编辑动画的属性和帧状态,就可以快速制作动画。
2.动画事件监听。ccc动画控件里有一个功能我觉得很实用,就是可以对动画的某一帧定义自定义监听事件(事件代码体定义在Node对应的用户脚本组件里即可)。比如物理游戏中过关的时候会有弹窗动画,动画快播放完的时候会播放音效,这就利用到了动画的自定义事件。
关于物理引擎
ccc的物理引擎非常强大,在程序开始时执行:
cc.director.getPhysicsManager().enabled = true;
整个游戏世界将进入物理监控状态,所有定义为刚体(添加了cc.组件)的Node将直接拥有物理属性,通过添加控件可以让Node在指定的热区拥有物理碰撞特性。
碰撞体有一个很好用的方法,就是获取碰撞体包围盒的方法,它可以和rect方法结合使用,实现判断某个矩形区域是否包含某个碰撞体的功能,在物理游戏中就是依赖它来实现盒子和目标区域的监听功能:
具体代码:
//获取自身的边界框
varselfAABB = this.node.getComponent (cc.PhysicsPolygonCollider).getAABB()
if (otherAABB.containsRect(selfAABB)) {}
此外物理引擎还提供了更加实用的组件:关节组件。
它可以定义一些常用的物理场景。例如,在这个游戏中,车轮使用了物理引擎的控制:
该控制可以模拟火车车轮的物理效果,使车轮与刚体保持一定距离并自行旋转。
其实除了这一类关节组件之外,官方还提供了很多其他的关节组件,具体使用方法可以参考ccc的官方开发文档:
哦,我还得提一下:
cc.director.getPhysicsManager()
此代码返回全局物理管理器对象。本游戏使用了该对象的以下两种方法:
1.方法,该方法可以检测某个坐标点下是否存在物理碰撞体,物理游戏中禁止在刚体上画线的功能就是依靠它来实现的:
2.方法,该方法可以获取射线在指定的起点和终点之间经过的刚体集合,物理游戏中遇到刚体后禁止线路继续的功能就是依靠它来实现的:
这个方法原生的实现逻辑还是比较复杂的,包括各种几何算法,反正几何学得不好的华叔如果想原生实现的话,只能达到很不尽人意的效果,不过ccc直接封装了方便大家调用,很方便。
关于预制对象
预制对象是ccc中非常重要的节点处理机制。
它可以将节点保存为像场景一样的单独文件,然后通过以下方式在不同的场景中引用它:
cc.instantiate()
方法复制预制的对象节点,以便可以重用节点逻辑,这最适合节点组件。
实体游戏“创造模式”中的所有地图元素实际上都是基于同一个预制物体。
无论是“创造模式”还是“闯关模式”中地图元素的基本数据模型都来自于这个预制对象,如果需要添加新的地图元素,只需要修改这个预制对象就可以全局生效,非常方便。
但是大家要注意,预制对象对性能有一定的负面影响,具体可以去论坛看看,可以说是一把双刃剑。
关于
听说ccc2.0版本优化了逻辑,试了一下确实好用,目前是绑定的,在游戏里可以定义多个节点来处理节点,在实体游戏中用来实现各种元素的层次化处理(先给出定义,然后指定不同的)
另外,还可以快速实现截图或者放大镜的效果:
回调方法中相关代码:
varcamera = this. assistCamera. getComponent ( cc.Camera );
// 新建一个 RenderTexture,并且设置 camera 的 targetTexture 为新建的 RenderTexture,这样 camera 的内容将会渲染到新建的 RenderTexture 中。
lettexture = newcc.RenderTexture();
letgl = 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();
varsf = newcc.SpriteFrame(texture)
this.node.targetAssist. getComponent(cc.Sprite). spriteFrame = sf
如果想了解使用方法,也可以研究一下官网提供的demo,这里就不再赘述了:
关于性能调优
说实话,毕竟我对ccc的底层原理理解的不是很深入,所以只能结合自己的项目给出一些性能优化方面的小建议,下面是一些小建议:
这个物理游戏刚出来的时候,性能特别差,所以我们后来做了三次优化。
1. 自动合并图像
缩减是提升游戏渲染效率的一个很直接有效的方法,决定两张纹理能否合并为一张的一个很重要的因素就是是否使用同一张纹理,所以官方建议将纹理进行合并。
但我的项目里用到了太多的零散图片,要把它们合并起来可真是太麻烦了!在为此烦恼的时候,我发现ccc提供了一个强大的功能“自动合并”。
用了才知道,用了才会有惊喜。该功能可以把当前目录及子目录中的所有图片文件按照指定的算法合并成一张图片,并自动更新原始引用。所有零散的图片可以一次性合并成一张大图,以备不时之需。
网络请求的数量直接从几十个下降到一两个。
但是…发现并没有减少多少,也没花太多时间去想为什么,就不再关注这个方法了。
2.减少节点
如果节点没有及时释放,那肯定会导致增加。沿着这个方向思考,我想到了 cc.()。在实体游戏中,只有这个方法会主动添加新的节点。那么只要及时销毁生成的临时节点就没事了。但是我发现复制之后调用了 () 方法,逻辑好像是对的。
但是后来查了资料发现该方法执行完之后,节点不会自动销毁,真正能销毁的是节点的方法,好尴尬啊,应该用 替换。于是全局搜索,一个一个替换,成功还原。
3.优化代码逻辑
除了上面说的常规方法,还需要优化自己的代码。这款游戏代码逻辑中最有可能优化的地方就是“画线”部分:
画线的主要逻辑是:
当前场景有一个用于绘制线条的全屏预制对象。
监控节点每次移动时都会在cc.组件上画线,并存储每个移动点。
根据算法利用所有收集到的移动点构建节点的物理碰撞区域
因此移动点越少越好,这种情况下优化的手段有两种:
1.若当前移动点与前一个移动点之间的直线距离小于某个极限值,则认为当前移动点无效。(相当于强制移动点的距离)
相应的判断代码很简单,利用ccc提供的向量距离方法:
returnlastPoint.sub(nowPoint).mag() >= 5;
2、若“当前移动点与前一个移动点的移动方向”与“前一个移动点与前一个移动点的移动方向”相同,则可以将前一个移动点破坏,不记录。(相当于将直线部分的移动点简化为两个端点)
回调中对应代码如下:
//获得增长向量
varaddVe = thisPos.sub( thisPos.prevPos )
//如果有前一个增长变量
if(thisPos.prevPos.prevPos){
//获取上一个点的增加向量
varlastAddVe = 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);
}
靠着上面三种方法我大概可以控制在50左右,但效果还是可以优化的。
在性能优化方面,ccc还提供了“节点池”和“动态地图合成”的优化手段,本作目前还未应用,不过未来或许可以尝试一下。
终于
我终于说完了我想说的话,现在我想表达我的最后的想法。
华叔在互联网行业工作快10年了,现在他已经不需要单纯的去执行了,也许有人指导他一下就能把工作做好。那么有人会问,你还那么辛苦的画图、写代码、做demo干什么?还有必要吗?
我给他讲了一个故事,有一天,我在指导另一个同事的项目,我有很多争论和理由,但是我的同事反问我:你有多久没写代码了?事情是这样的。
我一时愣住了。
虽然我还是回答了,但是后来想想还好我之前研究过他的项目,不然光是问一句“你有多久没写代码了?”就已经把主动权拱手让人了。
大部分企业利润都是建立在信息不对称的基础上的,谁处在信息的上游,谁就更容易掌控局面,管理者和策划者提供方向,这当然很重要,但信息不对称大多发生在技术层面。
我见过CP如何使用技术来欺骗需求者,尤其是在高科技的事情上,欺骗他们很容易。
有时候,我们进行转型,从一个渠道转到另一个渠道,将技术转移到管理上,以为自己找到了更好、更适合的方向。
其实我看到更多的可能性是:1.这个职业到了瓶颈,他们选择转型来规避瓶颈;2.他们因为专业能力突出而被赋予管理权力,然后他们认为自己必须转型;3.他们真的不适合这个职业
第三点就不说了,前两点你有没有想过?你想转行的领域是否需要你?你辛苦培养出来的技能如果不时不时练习,就会慢慢消失。你愿意因为一点小瓶颈就放弃它们吗?
技术与管理并不冲突,但为什么要因为管理而抛弃技术,失去对技术创造力的追求呢?
你还能定义出你是谁吗?
最后引用一位前辈的话:“我习惯先弄清楚如何实现某件事,然后再给予指导”。
谁不想站在信息的最前沿?
你是不是觉得我平时都是靠吹牛来引导很多内部小游戏的开发呢?
关于此