-
Notifications
You must be signed in to change notification settings - Fork 6
开发中踩过的坑
我选用cocos2d系列,原因主要有以下几点:
- 免费(unity的好大家都知道,但是发布到iOS和android好像各要2000美刀来着?)
- 跨平台
- 用户多
- 开源
用了这么段时间,发现的缺点嘛:
- 引擎更新经常大幅度不向下兼容(导致难以更新引擎、引擎资料断档,谁试谁知道)
- 文档烂(尤其是-lua,遇到问题基本上就要参照着看c++的文档和源码)
- 书籍也烂(要么就是api罗列,要么就是版本太旧;-lua的书就更别提了)
- 官方不务正业(2d功能还没完善就去搞3d,居然还要做VR;引擎配套的ide,好像都有三款以上了,哪一款真的能用?居然还在搞新的)
我使用过cocos2d-x引擎旗下的以下几个版本:
-
cocos2d-x
优点:
- 运行效率高
- 静态类型(我个人比较喜欢)
- IDE完善(VS + Visual Assist,c++的无敌组合了应该)
缺点:
- 不好做热更新(导致我改用lua的根本原因)
- 开发略慢
-
cocos2d-lua(cocos2d-community)
优点:
- 比较容易热更新
- 开发效率高
缺点:
- 官方态度暧昧,貌似处于半放弃状态(其实我用过一阵子cocos2d-community,但维护者太少了,前途未卜,遂放弃)
- 动态类型
- 开发环境比较糟糕(官方的直接无视,我试用多款第三方的ide后,现在使用VS Code作为编辑器,调试靠打印log)
-
cocos2d-js
优点:
- 比较容易热更新
- 开发效率高
- 官方主推
关于官方主推,我再多说两句。官方最近推出了一款名叫cocos creator的开发环境,大力宣扬其各种优点。 我也被迷得晕头转向,特意看了一阵子JS的书(我之前没接触过JS),试用了一个星期的creator,甚至有打算改用它来开发BabyWars。
直到我发现,在creator里,只要加入一个含有200张左右的图片的plist文件到资源管理器,整个环境立马卡的半死(后续每一个操作的响应时间拖长到以分钟计算)乃至崩溃,我就知道是我想太多了(官方还想拿这货跟unity比?呵呵呵)。
是的,我确实没给官方付过一分钱,但是官方能不能不要把资源放在这种华而不实的玩意上面,多完善一下引擎已有功能不好吗?你们怎么知道没有人愿意为你们买单呢?
缺点:
- require等各种配置能让人蛋疼死
- 动态类型
- 开发环境比较糟糕
- 我没发现-js比起-lua有什么优点
综上所述,我最终选择了cocos2d-lua。
- lua的垃圾回收是自动进行的,而且能够正确处理循环引用的情况,因此程序员一般不需要为垃圾回收而操心
- 在引擎的c++侧中,垃圾也都是自动回收的
当我们在lua中新建一个cocos2d的对象时(以sprite为例):
local s = cc.Sprite:create()我们实际上是在引擎的c++侧新建了一个Sprite对象,同时使lua侧的变量s“绑定”到它。
需要注意的是,就如同在c++中调用Sprite:create()那样,上面的代码所生成的c++侧的Sprite对象是会在下一帧开始之前被自动回收的。
如果不加以阻止,那么在c++侧的Sprite对象被回收之后,lua侧的s就变成了空壳,试图调用c++侧的方法(如s:setVisible())都会以失败告终。
c++侧的Sprite对象被回收,并不会使得lua侧的s变成nil;另一方面,手动把s置为nil,也不会使得c++侧的Sprite对象被马上回收。
顺带一提,阻止自动回收主要有两种方法:
- 调用s:retain()(后续需要在合适时机调用s:release(),否则对应的c++侧的Sprite对象将无法被回收)
- 把s加入到其他的不会被自动回收的cocos2d对象容器中,比如display.getRunningScene():addChild(s)(这其实也是在c++侧内部调用了retain())
从lua 5.2起,如果一个对象存在名为__gc的元方法,那么这个对象被回收时,该元方法将被调用,参数就是该对象。由此,我们可以在这个元方法里实现类似于析构函数的操作。
非常遗憾的是,在lua 5.1(也就是cocos2d-lua所使用的版本)中,仅当该对象是userdata时,__gc才会被调用。因此我们无法为纯lua对象实现类似析构函数的功能了。
普通情况下,要缩放cc.Node只需直接调用node:setScale()即可;引擎会以该node的锚点为中心对node进行缩放。简单吧?
但如果需要以某一特定位置为中心进行缩放,那解决方案可就不是这么直观了(事实上,我整整消耗了一天在这问题上)。
为了方便叙述,以下以“鼠标位置”作为缩放中心(在触摸操作下,一般以两个触摸点的中点为缩放中心;这并非核心问题,这里从略),以滚轮滚动触发缩放。这样,本问题可以举例描述为:假设缩放前,鼠标正指向地图上的某片树叶,那么不论如何缩如何放,只要鼠标没移动过,那么鼠标应该始终指向该片树叶(换言之,该片树叶也必须一直处在屏幕的某个相同位置)。
上面已经提过,node:setScale()会以node的锚点进行缩放。因此首先能想到的解决办法就是:
- 在需要进行缩放的时候,先获取鼠标的位置,并转换为节点空间下的坐标
- 把node的锚点设置为上述坐标对应的点(需要设置ignoreAnchorPointForPosition,否则node马上就会移动)
- 愉快地进行缩放
示例代码如下:
local ViewZoomTest = class("ViewZoomTest", cc.Scene)
function ViewZoomTest:ctor()
-- 创建一个背景sprite;预先设置了ignoreAnchorPointForPosition
self.m_Background = cc.Sprite:createWithSpriteFrameName("bg.png")
self.m_Background:ignoreAnchorPointForPosition(true)
self:addChild(self.m_Background)
--创建鼠标事件侦听器,侦听鼠标滚动事件
self.m_Listener = cc.EventListenerMouse:create()
self.m_Listener:registerScriptHandler(function(event)
-- 获取鼠标位置,转换为节点空间下的坐标
local cursorPosInNode = self.m_Background:convertToNodeSpace(cc.Director:getInstance():convertToGL(event:getLocation()))
-- 归一化上述坐标并设置锚点
local width = self.m_Background:getContentSize().width
local height = self.m_Background:getContentSize().height
local ax, ay = cursorPosInNode.x / width, cursorPosInNode.y / height
self.m_Background:setAnchorPoint(ax, ay)
-- 愉快地进行缩放
local scale = self.m_Background:getScale() + event:getScrollY() / 100
self.m_Background:setScale(scale)
end, cc.Handler.EVENT_MOUSE_SCROLL)
self:getEventDispatcher():addEventListenerWithSceneGraphPriority(self.m_Listener, self)
return self
end
return ViewZoomTest
-- 在别的什么地方创建ViewZoomTest的实例并运行
display.runScene(ViewZoomTest:create())代码很好写,过程很愉快,但运行结果是令人内心崩溃的。简单地说,就是背景图片在缩放时不停地进行着幅度或大或小的抖动,最终要么缩放中心“收敛”到了鼠标位置,要么整个图直接从屏幕上消失。显然,这是不可接受的。
至于悲剧的起因,则是和引擎内部的绘图方式有关。回想上面的代码,我们已经设置了ignoreAnchorPointForPosition,而且全程没有改动过背景的position,因此引擎在绘图时,总是试图让背景图能够回到最初的位置,而这正导致了背景图的抖动。
由于cc.Node是所有显示节点的父类,所以不可能贸贸然去修改它(事实上,我是尽力避免写任何一行c++代码,虽然c++才是我最熟悉的语言),因此我便开始寻找各种办法解决这个问题。这一找就是一整天……
最后,我终于确信,通过改变锚点来改变缩放中心的做法是行不通的。那么,不改变锚点的话,节点的缩放就会无视鼠标位置,仅以一个固定的点为中心进行缩放。
这样,要让节点看上去像是在以鼠标位置为中心在进行缩放,唯一的办法就是缩放完了以后,恰当地改变节点的位置。
至于怎样才叫恰当?请看解决方案:
- 把缩放中心点zoomCenterInWorld从世界坐标转换为节点空间坐标zoomCenterInNode
- 调用setScale进行缩放
- 把zoomCenterInNode重新转换为世界坐标scaledZoomCenterInWorld
- 以两个世界坐标的差值为位移,设置节点位置
示例代码:
local zoomCenterInNode = self:convertToNodeSpace(zoomCenterInWorld)
self:setScale(scaleValue))
local scaledZoomCenterInWorld = self:convertToWorldSpace(zoomCenterInNode)
self:setPosition(self:getPositionX() - scaledZoomCenterInWorld.x + zoomCenterInWorld.x,
self:getPositionY() - scaledZoomCenterInWorld.y + zoomCenterInWorld.y)就是这么简单。至于为什么奏效?其实有了代码就很好懂了,这里就不废话了;)
首先,有以下背景知识:
- cc.Director的实例自带一个cc.EventDispatcher的实例
- 每个cc.Node及其子类的实例也都自带一个cc.EventDispatcher的实例,但在默认情况下,这个实例和director中的实例是同一个。
也就是说,在默认情况下,以下两个调用所返回的东西是一样的:
cc.Director:getInstance():getEventDispatcher()
some_node:getEventDispatcher()
在某些情况下(比如希望避免太多的event listener污染director中的event dispatcher),我们需要给某些特定的node设置单独的event dispatcher。
幸运的是,cocos2d-lua已经为我们提供了这样一个接口:cc.Node:setEventDispatcher()。Node的子类也都继承了这个方法。
不幸的是,这个接口实际上是有问题的:具体而言,调用之后,向新的event dispatcher里注册的任何listener,都永远不会被触发:
local MyScene = class("MyScene", cc.Scene)
function MyScene:ctor()
local eventDispatcher = cc.EventDispatcher:new()
eventDispatcher:setEnabled(true) -- 新建的event listener是默认禁用的,必须调用setEnabled使其启用
self:setEventDispatcher(eventDispatcher)
local listener = createSomeListener()
self:getEventDispatcher():addEventListenerWithSceneGraphPriority(listener, self) -- listener永远不会被触发,无论是什么类型
return self
enddirector同样有setEventDispatcher()的接口。奇怪的是,作为对比,调用此接口后,向新的event dispatcher注册的listener仍然可以顺利触发。
无(如果您知道解决方案,还望留言赐教,非常感谢!)。
调用ccui.ListView:setContentSize(width, height)来设置ListView的可见大小。
- 设置合适的可见大小(调用ccui.ListView:setContentSize())
- 用合法参数调用ccui.ListView:setBackGroundImage()。这个函数有两个参数,按顺序分别是:
- 图片资源的名称或完整路径
- 图片资源类型,有以下取值: - ccui.TextureResType.localType(默认,图片是单个图像文件时使用,可忽略) - ccui.TextureResType.plistType(如图片是plist中的一个时使用,不可忽略)
- 这函数特么居然有两个参数?参数不对不写log,引擎test直接忽略了第二参数,网上还查不到资料,人干事?? 我卡了半天,最后是突然想起c++下同名函数需要传入资源类型的参数,试验后才找到答案的。给跪。
- ccui下有许多类的方法都有类似设定,如遭遇类似问题可以参照本方案解决。
- 调用ccui.Button:setTitleFontName(),以ttf文件的路径为参数
- 调用ccui.Button:getTitleRenderer():enableOutline()。 enableOutline()有两个参数,分别是outline的颜色和尺寸,具体可查阅cc.Label:enableOutline()的文档。
我找到解决方案的过程如下:
- 查找ccui.Button的资料(包括test,api文档,网上文章),发现没有直接给title文字加轮廓的函数
- 按常理,既然已有了cc.Label,那么Button没理由重写一次渲染文字的代码,很可能是直接用了Label来实现title文字
- 果然发现了ccui.Button:getTitleRenderer(),返回的是cc.Label,Button正是用这个renderer来渲染title的
- 查找给Label文字加轮廓的办法,是cc.Label:enableOutline()
- 尝试直接调用ccui.Button:getTitleRenderer():enableOutline(),发现无效
- 查找给文字加轮廓的相关信息,发现加轮廓的前提是这个Label必须是用ttf(或systemfont,由于使用范围有限,可无视)创建
- 既然上面的调用无法成功加上轮廓,就说明失败的原因可能是这个title renderer并非用ttf渲染,因此我们需要搞清楚这个renderer的值的来龙去脉
- 查引擎源代码UIButton.cpp,发现renderer最初会被赋值为Label:create()的返回值,此时它的渲染方式还未确定
- 继续读代码,发现决定renderer是以什么方式渲染的,是ccui.Button:setTitleFontName()!这个函数会对传入的字符串参数做判断,如果是一个文件的路径,就会试图以TTF或BMFont的方式来渲染title,否则会用systemfont。 而我先前传入的参数,决定了renderer会用systemfont。按道理应该也能成功加上轮廓的,但不知道为什么失败了
- 都查到这地步了,死马当活马医,改了参数调用ccui.Button:setTitleFontName()使得renderer以TTF的方式渲染
- 再调用ccui.Button:getTitleRenderer():enableOutline(),终于成功加上轮廓
解决问题自然高兴,但这种问题其实是不应该出现的。ccui.Button试图封装掉title文字的渲染方式,但并不很成功。 一旦像我这样遇到程序行为不符预期的情况,就很可能得花几倍的力气去查找原因。其实,如果setTitleFontName()能换个名字,暗示一下它可以接受文件路径作为参数,我就肯定不会在这里卡这么久。
最后,加其他文字特效的方法和上面的解决方案类似,就不多说了。