最近遇到的几个 Quick-cocos2d-x 真机崩溃(二)

大概是 8 月中旬的时候, 我们项目发生了一个很严重的线上事故. 在版本更新之后, 部分 Android 玩家反馈点击按钮开始游戏或活动按钮会闪退.

开始收到这个反馈时, 并没有太在意, 心想是不是机型适配有问题 ? 加上当时有别的工作在忙, 就没有去理会. 大概一个小时后, 玩家的邮件像雪花一样纷纷而至, 我才开始意识到, 更新出问题了.

阅读更多

最近搞 iOS 版遇到的一些问题和技巧 (三)

一. iOS 内购返回商品无效 invalid product

我使用 Quick-cocos2d-x 内置的 store 类请求商品信息时, 收到这样的错误:

nvalidProductIdentifiers [CCStore_obj]
[CCStore_obj] productsRequestDidReceiveResponse() invalid pid: com.xxx.xxx

首先, 检查你请求的商品 id 在 iTunes 后台是否创建, 是否拼写错误. 如果没有问题, 那么就不太好办了, 会有很多原因导致这个问题.

ZZB_Amoy 博客的这篇文章总结了下可能的原因, 如下:

  • 创建的App ID是否启用了IAP功能。
  • 商品信息是否配置到iTurn Connect,并到达“Ready to Submit”状态。
  • 在iTurn Connect中创建Test User,并收取邮件激活。之后登录到测试用手机的设置页面中(Store选项)。
  • App的Bundle Id是否和后台配置的App Id一致。
  • 是否创建相应的provisioning profile,并用此签名App。
  • iTurn Connect后台配置完商品信息后,是否等待若干小时生效。
  • SKProductsRequest请求的商品Id必须和iTurn Connect中配置的一致。(如:com.test.product.xxx)
  • iTunes Connect中配置的银行信息是否正确。
  • 是否先删除旧App,再重新编译生成新的。
  • 请不要使用越狱手机测试。

下面说说我两次遇到这个问题的解决方案:

  1. 如果游戏发布区域中没有手机中 App Store 当前区的话, 需要先登陆下对应区域创建的测试账号, 将商店切换到对应区域.
  2. 完善苹果开发者账号所能完善的信息, 如付款信息呀什么的, 然后莫名其妙就解决了.
  3. 商品 id 大小写错误, 在 chrome 搜索时是忽略大小的.

注:在测试阶段,可以不用上传APP软件包,但必须创建测试用Apple Id,并在手机设置中(store选项)登录。

二. iOS 运行崩溃 unrecognized selector sent to instance

运行游戏过程中收到如下错误:

[1515:710439] -[AppController window]: unrecognized selector sent to instance 0x2c85c00
libc++abi.dylib: terminate_handler unexpectedly threw an exception

这个在接入某一个平台 sdk 时遇到的问题, 于是便问了下他们的技术, 很快解决了问题.

1. 修改 AppController.h 中 window 变量的声明形式

2. 修改 AppController.mm

虽然问题解决了, 但是我并不明白各种缘由. Google 了下, 大概明白了, 原来如此. 从错误中我们可以看到这句 [AppController window] , 从语法来看, 这是要调用 AppController 的 window 函数, 但是在我们之前的写法中没有实现这个函数, 便出错了. 而使用 @property 这个东西, 会自动帮你实现一个 window/setWindow 函数, 这样就不会找不到这个函数了.

三. 游戏在低于 ios9 的系统启动崩溃

这个也是在接入第三方 sdk 时遇到的问题, 游戏一启动就会崩溃, 收到错误如下:

dyld: Symbol not found: _OBJC_CLASS_$_SFSafariViewController
Referenced from: /var/mobile/Applications/CF4146B4-3F79-4644-86CA-F19E52E64BAA/superarmoreddivision.app/superarmoreddivision
Expected in: /System/Library/Frameworks/SafariServices.framework/SafariServices
in /var/mobile/Applications/CF4146B4-3F79-4644-86CA-F19E52E64BAA/superarmoreddivision.app/superarmoreddivision

Google 了一下, 没有任何人遇到过这样的问题, 这就十分棘手了, 完全不知从何入手. 经过一番探索, 找到了几个有用的线索:

  1. SFSafariViewController 这个类是 ios 9 才引入的, 这和我们已知的信息相符.
  2. 所幸的是我们的游戏有多个 Scheme , 每个 Scheme 接入不同的 sdk . 其他的 Scheme 的都可以正常运行.

这就可以肯定是某个 sdk 中使用了 SFSafariViewController 这个类, 但是还是没有办法定位是那个 sdk . 我不知道是否有一个命令查找符号引用, 因此只能采用最笨的排除法了, 我将引入的 sdk 依次删除, 看是否能够运行.

最终定位到了某个广告统计 sdk , 在询问其 ios 技术人员后得到了解决方案. 原来他们 sdk 需要optional 的形式引入 SafariServices.framework.

都怪我没有仔细阅读文档, 白白耽误了一段时间, 下次一定要注意!

四. Facebook 登录崩溃

集成 Facebook sdk 时, 调用登录接口游戏就会崩溃, 这个问题 Google 一下就能解决, 解决方案也很简单, 在 Info.plist 中加入下面几行代码即可:

1
2
3
4
5
6
7
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>

Stackoverflow 上的答案可以移步这里, Facebook 官网上也给了解答.

五. ios9 状态栏无法隐藏

隐藏状态栏在 ios9 上换了一种方式, 还是需要在 Info.plist 中进行配置:

1
2
3
4
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

Stackoverflow 上的答案可以移步这里.

六. showAlert 诡异崩溃

游戏内的一些弹框为了保证在游戏的最上层显示, 偷懒使用了 Quick-cocos2d-x 提供的 device.showAlert 接口. showAlert 内部使用 UIAlertView 实现, 运行一直良好, 有一天突然就不行了, 一调用就崩溃.

各种办法都试过了, 网上都说是线程安全问题, 我试了一下各种处理都不行, 打断点跟踪到最底层也无济于事. 几近绝望之时, @bin 的一句话提醒了我:

会不是屏幕方向的问题 ?

最终一番尝试, 删除了 RootViewController.mm 中几个屏幕方向相关的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
*/
// Override to allow orientations other than the default portrait orientation.
// This method is deprecated on ios6
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {

if (ConfigParser::getInstance()->isLanscape()) {
return UIInterfaceOrientationIsLandscape( interfaceOrientation );
}else{
return UIInterfaceOrientationIsPortrait( interfaceOrientation );
}

}

// For ios6, use supportedInterfaceOrientations & shouldAutorotate instead
- (NSUInteger) supportedInterfaceOrientations{
#ifdef __IPHONE_6_0
if (ConfigParser::getInstance()->isLanscape()) {
return UIInterfaceOrientationMaskLandscape;
}else{
return UIInterfaceOrientationMaskPortraitUpsideDown;
}
#endif
}

- (BOOL) shouldAutorotate {
if (ConfigParser::getInstance()->isLanscape()) {
return YES;
}else{
return NO;
}
}

这个 bug 出现之诡异, 解决方案之诡异, 在我遇到的 bug 中也算是很少见了.

更简洁的 lua 逻辑代码

爱因斯坦的质能方程 E=MC^2, 用在编程界同样适用 Error = More Code ^ 2. 代码越多, 出错的可能性就更大, 这个结论很正确呀. 那么我们如何使用更少的代码实现同样的需求呢 ?

一. 普通技

1. bool 值与 if 语句的择决

让我们来看一段代码:

1
2
3
4
5
6
local monthly_is_taken = app.player:getAttribute("monthly_is_taken")
if monthly_is_taken == true then
self._monthly_take:setButtonEnabled(false)
else
self._monthly_take:setButtonEnabled(true)
end

显然这个 if 语句是没有必要的, 我们可以直接使用 bool 进行函数参数传递:

1
2
local monthly_is_taken = app.player:getAttribute("monthly_is_taken")
self._monthly_take:setButtonEnabled(monthly_is_taken)

我们可以看到减少了 %60 的代码, 逻辑反而变得更清晰了.

2. 减少非必须的中间变量

我们都明白了中间变量的意义, 主要是为提高代码的可读性. 但是有时候中间变量的太多, 在增加码量的同时, 也会打断我们的我们的思路.

比如我们要算一个等差数列的和, 我们都知道使用公式 (首项+末项)*项数/2, 我们看一下这个实现:

1
2
3
4
5
local array = {1,3,5,7,9}
local array_len = #array
local first_element = array[1]
local last_element = array[array_len]
local sum = (first_element+last_element)*array_len/2

就不如下面这个实现:

1
2
local array = {1,3,5,7,9}
local sum = (array[1]+array[#array])*#array/2

这样的话, 我们上一个示例的代码可以进一步精简:

1
self._monthly_take:setButtonEnabled(app.player:getAttribute("monthly_is_taken"))

3. 使用 elseif 优化 if 语句

如果是逻辑相悖的判断条件, 我们可以使用 elseif 语句连接, 而不用多个 if 语句.

1
2
3
4
5
6
7
8
9
10
11
if self.item_id == "43" then
-- do some thing
end

if self.item_id == "69" then
-- do some thing
end

if self.item_id == "75" then
-- do some thing
end

我们可以修改为:

1
2
3
4
5
6
7
if self.item_id == "43" then
-- do some thing
elseif self.item_id == "69" then
-- do some thing
elseif self.item_id == "75" then
-- do some thing
end

这样修改后, 对逻辑的执行时间也优化哟, 因为一但有一个 if 语句命中, 后面的 elseif 都不会再去判断了.

4. 使用 config 优化 if-elseif 语句

如果一个逻辑中有大量的 if-elseif 语句, 我么就可以使用 config 的形式替换掉它, 使得逻辑更加简洁.

让我们看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if _data.type == GameEnum.MailType.MAIL_TYPE_SYSTEM then
self._title:setString(_data.content.content.subtype)
self._name:setString(TextEnum.CNReset.SYSTEM)
self.title = TextEnum.CNReset.SYSTEM_INFORMATION
elseif _data.type == GameEnum.MailType.MAIL_TYPE_ALLIANCE_KICK then
self._title:setString(TextEnum.CNReset.KICK)
self._name:setString(TextEnum.CNReset.SYSTEM)
self.title = TextEnum.CNReset.KICK
elseif _data.type == GameEnum.MailType.MAIL_TYPE_ALLIANCE_JOIN then
self._title:setString(TextEnum.CNReset.JOIN_IN)
self._name:setString(TextEnum.CNReset.SYSTEM)
self.title = TextEnum.CNReset.JOIN_IN
elseif _data.type == GameEnum.MailType.MAIL_TYPE_ALLIANCE_REJECT then
...

这是一段关于邮件标题的逻辑, 这里只节选出了 1/4 的代码, 真的是又臭又长. 我们可以这样子去优化它:

1
2
3
4
5
6
7
8
9
10
11
12
13
local CONFIG = {
[GameEnum.MailType.MAIL_TYPE_SYSTEM] = {title = XXX, name = XXX},
[GameEnum.MailType.MAIL_TYPE_ALLIANCE_KICK] = {title = XXX, name = XXX},
[GameEnum.MailType.MAIL_TYPE_ALLIANCE_JOIN] = {title = XXX, name = XXX},
[GameEnum.MailType.MAIL_TYPE_ALLIANCE_REJECT] = {title = XXX, name = XXX},
...
}

local config = CONFIG[_data.type]
if config then
self._title:setString(config.title)
self._name:setString(config.name)
end

因为只是代码节选, 所以上面修改是一段伪代码, 但是看起来超级清爽的有木有! 对于一开始无法确定的数据如何配置呢? 我们可以配置一个 function, 用的时候取出来调用就可以啦.

二. 黑科技

1. 数据默认值的设定

当我们拿到一段数据后, 总是要先预处理数据, 后面才是使用数据. 预处理阶段很重要的一步就是某些数据的默认值.

1
2
3
4
5
6
7
8
9
10
11
12
function sum3(_num1, _num2, _num3)
if not _num1 then
_num1 = 0
end
if not _num2 then
_num2 = 0
end
if not _num3 then
_num3 = 0
end
return _num1 + _num2 + _num3
end

很繁琐是不是, 这时候我们可以使用 and 和 or 来优化默认值的设置:

1
2
3
4
5
6
function sum3(_num1, _num2, _num3)
_num1 = _num1 or 0
_num2 = _num2 or 0
_num3 = _num3 or 0
return _num1 + _num2 + _num3
end

or 的前面部分是 nil 或者 false 的情况下, 返回这个表达式的值后面部分. 下面我列举一下常用类型的默认值用法:

1
2
3
4
5
6
7
8
9
10
-- number
a = a or 0
-- string
a = a or ""
-- function
a = a or function()end
-- table
a = a or {}
-- boolean
a = a == nil and true

这里值得一提的是 boolean 类型, 如果希望默认值是 false 话, 就不需要默认值, 因为 nil 和 false 对于判断来说以意义一致. 而如果希望默认值是 true 的话, 并不是 a = a or true, 而是 a == nil and true, 大家可以细想一下其中的含义.

2. table 中元素的初始化

比如我们要统计一个列表中, 每个元素出现的次数:

1
2
3
4
5
6
7
8
local list = {1,2,2,3,1,3}
local counter = {}
for i,v in ipairs(list) do
if not counter[v] then
counter[v] = 0
end
counter[v] = counter[v] + 1
end

因为 counter 不可能提前初始化好, 所以总是要判断存不存在这个元素, 我们也可以利用上面提到的技巧做这个事情:

1
2
3
4
5
local list = {1,2,2,3,1,3}
local counter = {}
for i,v in ipairs(list) do
counter[v] = (counter[v] or 0) + 1
end

是不是变得很简洁 ?

最近搞 iOS 版遇到的一些问题和技巧 (二)

一. XCode: Could not find Developer Disk Image

101138-1ab6ab96d37904bd.jpeg-4.5kB
解决方案:
http://www.jianshu.com/p/3930df903a44
这个问题可能是因为你 XCode 没有下载对应 iOS 的 SDK 导致, 一般情况需要同步更新 XCode.

二. XCode: 无法导出 Archive 的项目

这个问题有可能是你项目 Team 选择的是一个没有开发者资格的账号导致的, 虽然可以正常开发, 真机调试, 但是是不能发布的, 所以也无法 Export .
解决方案:

  1. 更换一个有开发者资格的账号重新 Archive 导出, 但是 Bundle ID 就得换一个了.
  2. 通过命令行工具导出.
    在 Organizer 中找到你想导出的 Archive, 右键选择在文件夹中显示, 复制路径, 打开终端:
    1
    xcodebuild -exportArchive -exportFormat ipa -archivePath your-archive-file-name.xcarchive -exportPath ~/Desktop/test.ipa

三. XCode: App Installation failed

Unknown.png-45.1kB
很可能是之前手机已经装过一个同 Bundle ID 的应用, 但是现在换了签名.
解决方案:
删掉手机已经安装的那个应用就可以啦.

四. XCode: Failed to code sign “xxxx”

QQ20160626-0.png-437.7kB
签名失败了, 这种情况一般发生在使用别人给的证书打包时. 这时候我们项目 Build Setting > Code Signing Identity 就不能选择 iOS Developer, 而是要选择导入的签名文件.

五. 内购: 无法连接到 iTunes Store

如果没有发布应用的话, 需要用沙盒测试账号来测试. 我们需要先在 设置>iTunes Store 和 App Store注销账号, 然后打开游戏, 开始购买, 这时候输入你的测试账号. 成功后如果有跳转 App Store 的话或者绑定付款方式的话, 不同理会, 再返回应用购买就可以了.

六. 崩溃: showAlert 崩溃

某一次突然, 一旦调用 Quick-cocos2d-x 提供的 device.showAlert 就会崩溃, 断点调试无果, 崩溃时提示的内容也不尽相同.
解决方案:
删除 RootViewController.mm 中所有和屏幕方向代码, 就解决啦.

1
2
3
4
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
- (NSUInteger) supportedInterfaceOrientations
- (BOOL) shouldAutorotate
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation

七. 崩溃: iOS 9.2+ 播放视频崩溃

感谢 @子龙山人 大神提供的解决方案, 点击这里查看.

UIVideoPlayer-ios.mm 文件 ~VideoPlayer() 函数中的 dealloc 修改为 release 即可.

1
2
-        [((UIVideoViewWrapperIos*)_videoView) dealloc];
+ [((UIVideoViewWrapperIos*)_videoView) release];

Quick-cocos2d-x 使用静态库加速 iOS 打包

Quick-Cocos2d-x 项目的 iOS 工程使用 Tgarget Dependencise 依赖 cocos2d_libcocos_lua_bindings 工程.

QQ20160529-0.png-27kB

这样子在 iOS Archive 时会重新编译这两个项目, 十分痛苦, 尤其是一次出七八个渠道的包, 好几个小时就耗在里面了.

为什么不用静态库, 编译出 .a , 使用时直接链接就可以了嘛.

一. 编译静态库

找了一下, 原来早已经有小伙伴想到了这点, 这篇文章 Build cocos2d-x fat static library 就特别棒. 从中我们可以发现一个特别有用的脚本 buildstaticlib.sh, 可以直接使用 xcode 工程编译出静态库.

不过这个脚本只能编译出 Release 版, 我修改下可以传入 configuration, 这样我们可以分别编译出 Debug 和 Release 版的静态库啦, 我修改后的文件在这里.

因为我们要编译出多个静态库, 所有又写了另一个脚本 build.sh 调用 buildstaticlib.sh , 内容如下:

1
2
3
4
5
6
7
./buildstaticlib.sh $QUICK_V3_ROOT/cocos/scripting/lua-bindings/proj.ios_mac/cocos2d_lua_bindings.xcodeproj "libluacocos2d iOS" "Release"

./buildstaticlib.sh $QUICK_V3_ROOT/build/cocos2d_libs.xcodeproj "libcocos2d iOS" "Release"

./buildstaticlib.sh $QUICK_V3_ROOT/cocos/scripting/lua-bindings/proj.ios_mac/cocos2d_lua_bindings.xcodeproj "libluacocos2d iOS" "Debug"

./buildstaticlib.sh $QUICK_V3_ROOT/build/cocos2d_libs.xcodeproj "libcocos2d iOS" "Debug"

运行成功后会在当前目录生成 4 个 .a 文件, 下一步中将会用到.

1
2
3
4
5
6
7
.
├── build.sh
├── buildstaticlib.sh
├── libcocos2d\ iOS-debug.a
├── libcocos2d\ iOS.a
├── libluacocos2d\ iOS-debug.a
└── libluacocos2d\ iOS.a

二. 使用静态库

使用 XCode 打开 proj.ios_mac 目录下的 xxx.xcodeproj 工程.

1. 移除 Tgarget Dependencise

首先移除对 cocos2d_libcocos_lua_bindings 工程的依赖, 右键点击 Delete 然后选择 Remove reference 就可以.

QQ20160529-1.png-49.2kB

2. 添加 Other Linker Flags

我们静态库的依赖是在这里添加的, 在 Debug 和 Release 选项中分别加入对应的静态库.
QQ20160529-3.png-117.8kB

这样就完成啦, 尝试一下 Archive 的速度吧 !

三. 其他

1. 调试环境与生产环境

我们改成静态库后, 调试 cocos 引擎的代码会多有不便, 而且一旦修改了 cocos 的代码, 就得重新生成静态库, 对于开发阶段太不友好了.

我们的解决方案, 就是再建立一个 debug 工程, 这个工程依旧使用依赖项目的方式编译 cocos , 调试流程和以前一致. 上线打包时则使用我们的静态库版本, 多渠道也做在这个工程中, 享受静态库带来的编译加速.

最终我们的目录结构是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── runtime-src
│   ├── Classes
│   ├── proj.android
│   ├── proj.android_no_anysdk
│   ├── proj.android_studio
│   ├── proj.ios_mac
│   ├── proj.win32
│   └── proj.wp8-xaml
└── runtime-src-debug
├── Classes
└── proj.ios_mac

2. 生产环境工程瘦身

这一步可有可无, 我的代码洁癖又犯了, 所以顺手改了一下.

这时的生产环境除静态库外的内容和调试环境几乎一致, 然而有一些东西是我们用不到的:

  1. mac 平台对应的内容
  2. Classes/runtime 下的内容

删除这些时改动了 AppDelegate 中的东西, 这也上一步为什么从 runtimes-src 目录复制了一份.

3. 进一步加速编译

这一步我们目前还没有做, 只是一个想法.

修改完使用静态库后, 编译速度得到了很大的提升, 但还没有达到极致, 因为 quick 特有的 c++ 文件还是以文件形式存在于工程中的. 所有 Archive 的时候还是有一百多个源文件需要编译.

如果我们能进一步拆分, 新建一个 lib 工程, 将 quick 的源文件添加和依赖项目添加进去, 我们的游戏只依赖这样的一个静态库, 是否可以达到一个极致的编译速度 ?

4. 静态库文件的版本管理

在编译出 debug 版的静态库之前, 我还有想法将这几个静态库压缩上传到 git 上, 编译出 debug 版之后, 我就一个想法, ignore them !

所以我最终的策略 将这几个 .a 在 git 上忽略掉, 同时在那个目录保留了一个编译脚本, 谁要用到 iOS 项目的时候, 发现没有 .a , 自己运行脚本编译一份就可以啦 !

5. 编译脚本优化 ?

现在那个编译脚本会编译出一个 fat(armv7 armv7s arm64 i386 x86_64) 版的静态库, 内部实现其实是编译了好多次, 导致现在编译时间非常长.

思考:

  1. 是否有必要编出 i386 x86_64 版本 ?
  2. 看到虾神的一篇文章貌似说可以以 armv7+arm64, i386+x86_64 组合两次打出所有版本.

6. 最终 Archive 出的包会比使用源文件大 ?

看到网上有过这个说法, 我没有在修改前后分别 Archive 对比包体, 不太严谨.

但和我之前某一次的包相比, 只大了几百KB, 还不太确定是不是与使用静态库有关系, 大家在修改时可以注意对比一下.

cocos2d-x 优化游戏资源体积

一. 删除无用资源

在我们版本迭代的过程中, 总有一些图片被废弃掉, 如果当时忘记删除的话, 久而久之也就忘记了. 如果在上线前不做一次整理的话, 它们就会残存在你的资源中, 浪费包的体积.

为避免这种情况, 我们可以做的是:

1. 废弃的图片一定要及时删除

2. 编写废弃资源查找工具

可以用到的系统命令是 ack, 我们可以通过 brew install ack 安装, 使用的效果:

关于 ack 的更多用法请移步这里.

二. 使用替代对象

1. 使用9图

大家都知道图片在拉伸的过程中会失真, 那么如何避免这个情况呢? 使用9图.

注: 配图来自http://mux.baidu.com/?p=1506

这样我们就可以将一张很大的图缩小到很小, 然后使用9图拉伸, 起到节省资源的目的. 9图在 cocos 中的对象是 Scale9Sprite, 具体用法可以参考这篇文章.

2. 通过修改色调实现资源复用

知乎上有一篇问题讲的就是这个:
拳皇中的人物变色是如何实现的?
知乎日报上的这篇

cocos2d-x 版由 @偶尔e网事 大神实现, 对应的对象是 SpriteWithHue, 目前已经默认集成到了 cocos2d-x 中.

这样我们就可以将原来只是色调差异的图片用程序来实现啦~

3. 使用平铺

注: 配图来自http://bullteacher.com/7-textures.html

游戏中的有些图片完全可以通过平铺实现, 这样的话我们就可以让美术只出一个平铺单元的图片,在程序中去实现平铺.

首先, 平铺的这个功能是 opengl 层面就支持的, 详情大家可以移步这里, cocos2d-x 中实现平铺很简单:

1
2
3
4
5
6
-- 首先, 使用平铺单元图片创建一个精灵
local sprite = cc.Sprite:create("your_repeat_image.png")
-- 然后, 设置纹理参数
sprite:getTexture():setTexParameters(gl.LINEAR, gl.LINEAR, gl.REPEAT, gl.REPEAT)
-- 最后, 将这个精灵的纹理矩形设置为我们想要的大小
sprite:setTextureRect(cc.rect(0,0,1024,1024))

注意: 平铺单元图片的尺寸只能是2的幂

三. 压缩

1. 无损压缩

无损压缩还是十分值得推荐的, 它的原理知乎上这个答案讲的很清晰, 我节选其中的关键文字:

1.核心原理很简单,通俗的解释一下,就是由于PNG格式的灵活性,他可以有很多种方式表示同一张图片,不同方式有时就会导致文件大小不一样…
2.还有一点是PNG采用的是deflate算法,也非常的灵活,他的压缩率和encoder的实现有关,不同的encoder使用的时间,压缩出来的大小都不一样…
3.当然除了上面这两点是真正的无损压缩以外,还有减小PNG文件大小的方式就是去除一些对图片本身没有任何影响的metadata…

所以无损压缩纯粹是单方面的受益, 是一定要做的.

我们无损压缩主要用到的工具: ImageOptim

2. 有损压缩

有损压缩会损失一部分的图片质量, 但带来的受益还是十分可观的. 这是一个抉择的过程, 以最小的代价获取最大的受益, 甚至不能批量处理, 可能需要一张一张的人肉对比压缩.

我们有损压缩主要用到的工具: PP鸭

四. 选择正确的图片格式

1. 将无 alpha 通道的 png 图片存储为 jpg

2. 选用压缩率更高的图片格式

五. 其他

1. 圆形图片只使用 1/4

然后在程序中翻转3次,得到其他角度的图片. 一般会用在图片尺寸特别大的场景.

如上图, 我们游戏中一个全屏幕的雷达就是通过这个方案减少图片体积的.

2. 缩小图片

将展示精度不强的图片(比如: 游戏背景上的小装饰, 爆炸的序列帧)缩小, 在程序中放大.

3. 特殊方案分离png的透明通道

用jpg和黑白色png作为遮罩实现透明
用shader使图片背景透明
cocos2dx中使用JPG图和只带Alpha的PNG图合成渲染

我们之前曾经采取过其中的一个方案, 将一张 png 图片拆分为 jpg+alpha.png 的形式, 整体的包体小了近 25% , 不过也带来的其他的一些副作用.

建议大家使用这类黑科技前一定要做好调研和测试用例, 评估一下实际的收益.

Quick-cocos2d-x EditBox 几个小技巧

我们项目中的输入框使用的都是 EditBox , 但是 EditBox 还存在一些问题, 这里给大家分享一下我们的解决方案.

一. 字体过大

用过 EditBox 的同学都知道这样一个情况, EditBox 在创建时是无法传入字体大小的, 字体大小默认和 EditBox 的 size 一致. 如果要修改字体大小的话, 就必须有程序的参与, 十分讨厌.

而我们聪明的设计师 @大勇同学 则想到了一个非常棒的办法, 使用一张透明的9图来创建 EditBox, 后面再放置一个真实效果的 Scale9Sprite , 这样就可以实现字体比边框小很多的输入框了.

二. 多行输入

多行输入是一个很有必要的事情, 我们在写邮件, 军团公告等界面都有类似的需求, 然而 EditBox 并不能很好的支持多行输入, 不同平台间也存在差异, 一直很头疼这件事情.

然而团队中另外一位成员 @小齐同学 却用另一种十分脑洞的方案解决了这个问题, 着实让人佩服. 他的思路是这样子的:

创建一个和需求大小一致的 EditBox, 同时创建一个 LalbelTTF , 将 dimensions 属性设置为需求大小. 处理 EditBox 使之看不见, 但又能正常输入, 同时监听输入文字变化事件, 在事件中修改 LalbelTTF 的文字.

核心就是让 EditBox 承担只文字输入的功能, 而让另外一个 LalbelTTF 来承担文字显示的功能. 实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function EditBoxUtil.multiline(_editbox, _label, _params)
_params = _params or {}

-- 本来这个应该只是Android上的设置, 但是为了避免平台的差异性, 因此统一处理
_editbox:setCascadeOpacityEnabled(true)
_editbox:setOpacity(0)
if device.platform == "android" then
-- 避免文字过大导致Android系统崩溃
_editbox:setFont("Helvetica",2)
elseif device.platform == "ios" or device.platform == "mac" then
_editbox:setFont("Helvetica",0)
end

_editbox:registerScriptEditBoxHandler(function(event)
if event == "began" then
_editbox:setText(_label:getString())
elseif event == "changed" then
_label:setString(_editbox:getText())
elseif event == "return" then
_label:setString(_editbox:getText())
end
if _params.listener then
_params.listener(event, _editbox)
end
end)
end

这段代码和简单, 但背后所遇到的坑却不少, 且听我来道一道:

1. 为什么要调用 setFont

如果只是想设置字体大小, EditBox 明明有提供 setFontSize 接口, 为什么要调用 setFont ? 请看 setFontSize 实现:

1
2
3
4
5
6
7
8
void EditBox::setFontSize(int fontSize)
{
_fontSize = fontSize;
if (_editBoxImpl != nullptr && _fontName.length() > 0)
{
_editBoxImpl->setFont(_fontName.c_str(), _fontSize);
}
}

可以看到 setFontSize 在没有设置字体名称 _fontName 时是没有作用的.

2. 为什么要分平台来实现

在接入 android 前,我们是没有分平台实现的, 只是 setFont("Helvetica",0) , 在 iOS 上没有任何问题, 但是在 Android 上会 catch 到 divide by zero 崩溃, 估计是某一个地方用 fontsize 做被除数了吧 , 于是 Android 上改为设置透明度.

3. Android 输入过多文字后会崩溃(OOM)

崩溃在 Cocos2dxBitmap.javagetPixels 函数中:

1
2
3
4
5
6
7
8
9
10
11
private static byte[] getPixels(final Bitmap bitmap) {
if (bitmap != null) {
final byte[] pixels = new byte[bitmap.getWidth() * bitmap.getHeight() * 4];
final ByteBuffer buf = ByteBuffer.wrap(pixels);
buf.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(buf);
return pixels;
}

return null;
}

new byte 这里触发了报错的原因是 Out Of Memory 异常 ! 调试发现 bitmap 的宽高惊人的达到了 12000x600 !

经过 @bin 的提醒, 发现这里可能是因为 EditBox 字体过大的原因, 因为 EditBox 会默认设置字体字体大小和 Scale9Sprite 的 PreferredSize 一直, 就可能设置字体大小为 100+ , 文字一多尺寸当然就上去了! 所以便有了这么一行:

1
2
3
if device.platform == "android" then
-- 避免文字过大导致Android系统崩溃
_editbox:setFont("Helvetica",2)

二. 屏蔽 Emoji 输入

按照要求游戏中玩家可以输入文字的地方都是不能够输入 Emoji 表情的, 原因有两点:

  1. 不同系统间表现存在差异
  2. EditBox Android 版输入确认后会变乱码
  3. 后台搜索玩家时不太方便

因此, 我们需要屏蔽 Emoji 表情的输入, 我们有两种做法:

  1. 无法输入, 弹出键盘点击表情没有反应
  2. 输入完成后, 游戏内点击提交时提示非法

我们采用的是第二种方案, 这个无法通过纯 lua 代码实现, 需要分平台去做.

1. iOS

修改 UIEditBoxImpl-ios.mm 文件的 shouldChangeCharactersInRange 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (BOOL)textField:(UITextField *) textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
__block BOOL returnValue = NO;
[string enumerateSubstringsInRange:NSMakeRange(0, [string length]) options:NSStringEnumerationByComposedCharacterSequences usingBlock:
^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {

if ([substring rangeOfCharacterFromSet: [NSCharacterSet characterSetWithRange:NSMakeRange(0xFE00, 16)]].location != NSNotFound) {
returnValue = YES;
}

const unichar high = [substring characterAtIndex: 0];

// Surrogate pair (U+1D000-1F9FF)
if (0xD800 <= high && high <= 0xDBFF) {
const unichar low = [substring characterAtIndex: 1];
const int codepoint = ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;

returnValue = (0x1D000 <= codepoint && codepoint <= 0x1F9FF);

// Not surrogate pair (U+2100-27BF)
} else {
returnValue = (0x2100 <= high && high <= 0x27BF);
}
}];

if (returnValue) {
return NO;
}


if (getEditBoxImplIOS()->getMaxLength() < 0)
{
return YES;
}

NSUInteger oldLength = [textField.text length];
NSUInteger replacementLength = [string length];
NSUInteger rangeLength = range.length;

NSUInteger newLength = oldLength - rangeLength + replacementLength;

return newLength <= getEditBoxImplIOS()->getMaxLength();
}

这段代码是我从 https://github.com/woxtu/NSString-RemoveEmoji 中提取出来的.

2. Android

Android 上的实现也很简单, 主要是需要创建一个新的 InputFilter 用来过滤 Emoji 表情. 需要修改 Cocos2dxEditBoxDialog.java 文件成员变量添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static InputFilter EMOJI_FILTER = new InputFilter() {

@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
for (int index = start; index < end; index++) {
int type = Character.getType(source.charAt(index));
if (type == Character.SURROGATE || type == Character.OTHER_SYMBOL || type == Character.PRIVATE_USE) {
return "";
}
}
return null;
}
};

修改 onCreate 函数 setFilters 处逻辑:

1
2
3
4
5
if (this.mMaxLength > 0) {
this.mInputEditText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(this.mMaxLength), EMOJI_FILTER });
}else{
this.mInputEditText.setFilters(new InputFilter[] { EMOJI_FILTER });
}

关于 Emoji 的相关修改都已经推送到了 github 上, 点击这里查看.

最近搞 iOS 版遇到的一些问题和技巧

一. 编译运行

1. 运行时提示 identity 无效

The identity used to sign the executable is no longer valid.
Please verify that your device’s clock is properly set, and that your signing certificate is not expired. (0xE8008018).

解决方案:

  1. 打开 Preferences > Accounts
  2. 选中项目对应的 Apple ID > 点击右下角 View Details...
  3. 点击弹出框左下角的 Download All > 等待完成后点击 Done 关闭
  4. 再次运行项目, 等待弹出框, 点击 reset.

2. iPhone5 上获得的设备尺寸为 960x640

表现出来是竖屏游戏上线有黑边, 跟踪发现获得的设备尺寸为 960x640, 而非 1136x640.

解决方案:

添加一张尺寸为 1136x640 名为 `Default-568h@2x.png` 的启动图即可.

参考资料:
http://discuss.cocos2d-x.org/t/getframesize-get-wrong-screen-size/7657

3. XCode issue 页面只显示错误, 不显示警告

cocos2d-x 在 XCode 中编译 warning 太多, 出错后 error 会被淹没在一大堆的警告中, 得拖动半天才能找得到.

解决方案:

在页面最下方有一个感叹号型按钮, 点击选中即可.

二. 上传应用

1. 多任务支持

XCode 7 error: “A launch storyboard or xib must be provided unless the app requires full screen”

解决方案:

我们勾上全屏即可:


目前就这些, 后面会持续更新 !