Cocos Creator游戏开发踩坑记录

最近在用Cocos Creator做项目,记录一下开发过程中遇到的各种坑和解决方法。

Android项目配置

项目结构

Creator构建Android项目后,目录结构大概是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
proj.android-studio/
├── app/
│ ├── build.gradle # 应用构建配置
│ ├── jni/
│ │ ├── Android.mk # NDK构建脚本
│ │ └── Application.mk # NDK应用配置
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── assets/ # 游戏资源
│ └── java/ # Java源码
├── build.gradle # 项目构建配置
├── gradle.properties # Gradle属性配置
└── settings.gradle # 项目设置

Gradle配置

项目级build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Top-level build file where you can add configuration options
buildscript {
repositories {
jcenter()
google() // 必须添加Google仓库
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
// NOTE: Do not place application dependencies here
}
}

allprojects {
repositories {
jcenter()
google()
}
}

应用级build.gradle配置:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import org.apache.tools.ant.taskdefs.condition.Os

apply plugin: 'com.android.application'

android {
compileSdkVersion 24
buildToolsVersion "26.0.2"

defaultConfig {
applicationId "com.game.xxx"
minSdkVersion 10
targetSdkVersion 23
versionCode 1
versionName "1.0"

externalNativeBuild {
ndkBuild {
if (!project.hasProperty("PROP_NDK_MODE") ||
PROP_NDK_MODE.compareTo('none') != 0) {
targets 'cocos2dcpp'
arguments 'NDK_TOOLCHAIN_VERSION=4.9'
arguments 'APP_PLATFORM=android-'+PROP_TARGET_SDK_VERSION

def module_paths = [
project.file("../../cocos2d").absolutePath,
project.file("../../cocos2d/cocos").absolutePath,
project.file("../../cocos2d/external").absolutePath
]

if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Windows使用分号分隔路径
module_paths = module_paths.collect {
it.replaceAll('\\\\', '/')
}
arguments 'NDK_MODULE_PATH=' + module_paths.join(";")
} else {
// Linux/Mac使用冒号分隔
arguments 'NDK_MODULE_PATH=' + module_paths.join(':')
}

arguments '-j' + Runtime.runtime.availableProcessors()
abiFilters.addAll(PROP_APP_ABI.split(':').collect{it as String})
}
}
}
}

sourceSets.main {
java.srcDir "src"
res.srcDir "res"
jniLibs.srcDir "libs"
manifest.srcFile "AndroidManifest.xml"
assets.srcDir "../../Resources"
}

signingConfigs {
release {
if (project.hasProperty("RELEASE_STORE_FILE")) {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.release
}
externalNativeBuild {
ndkBuild { arguments 'NDK_DEBUG=0' }
}
}
debug {
externalNativeBuild {
ndkBuild { arguments 'NDK_DEBUG=1' }
}
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':libcocos2dx')
}

常见编译错误

format-security错误

1
error: format not a string literal and no format arguments [-Werror=format-security]

解决方案:

Application.mk中添加:

1
APP_CPPFLAGS += -Wno-error=format-security

CreateProcess参数错误

1
error: process_begin: CreateProcess(...) make (e=87): 参数错误

原因: Windows对命令行长度有限制,.mk文件里包含的文件太多。

解决:

Application.mk中添加:

1
APP_SHORT_COMMANDS := true

malloc未声明

1
'malloc' was not declared in this scope

解决方案:
在源码中添加头文件包含:

1
#include <stdlib.h>

void*转换错误

1
invalid conversion from 'void*' to 'char*'

解决方案:
在malloc调用前添加类型强转:

1
char* buffer = (char*)malloc(size);

armeabi不支持

1
2
3
Android NDK: INTERNAL ERROR: The armeabi ABI should have exactly one
architecture definitions. Found:
Error:ABIs [armeabi] are not supported for platform.

解决方案:
gradle.properties中修改ABI配置:

1
2
3
4
5
# 修改前
PROP_APP_ABI=armeabi

# 修改后
PROP_APP_ABI=armeabi-v7a

支持的ABI列表:

ABI 说明
armeabi-v7a ARMv7及以上(推荐)
arm64-v8a ARM64(高性能)
x86 Intel 32位模拟器
x86_64 Intel 64位模拟器

横竖屏切换踩坑

问题描述

从竖屏适配改为横屏时,可能出现以下问题:

  • 界面整体显示为横屏
  • 但Scene内容显示为竖屏
  • 内容靠左,超出屏幕的部分被裁切

配置对比

错误配置:

1
2
3
4
5
6
7
8
9
10
{
"orientation": {
"landscapeLeft": true,
"landscapeRight": true,
"portrait": false,
"upsideDown": false
},
"packageName": "net.xxxxxxx",
"webOrientation": "portrait"
}

正确配置:

1
2
3
4
5
6
7
8
9
10
{
"orientation": {
"landscapeLeft": true,
"landscapeRight": true,
"portrait": false,
"upsideDown": false
},
"packageName": "net.xxxxxxx",
"webOrientation": "landscape"
}

关键发现: Android打包会受web-mobile配置里webOrientation参数影响,这是个坑!

代码层适配

AppDelegate.cpp适配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 ||
CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
// Win32开发环境设置固定分辨率
cocos2d::Size designResolutionSize = cocos2d::Size(800, 480);
glview->setDesignResolutionSize(
designResolutionSize.width,
designResolutionSize.height,
ResolutionPolicy::FIXED_HEIGHT
);
glview->setFrameSize(800, 480);
#else
// 移动设备获取实际屏幕尺寸
Size frameSize = glview->getFrameSize();
glview->setDesignResolutionSize(
frameSize.width,
480,
ResolutionPolicy::FIXED_HEIGHT
);
#endif

DragonBones动画系统

动态加载DragonBones

本地资源加载方式:

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
onLoad() {
// 创建动画节点
let animNode = new cc.Node();
animNode.parent = this.node;

// 添加ArmatureDisplay组件
this.armatureDisplay = animNode.addComponent(dragonBones.ArmatureDisplay);

// 监听动画完成事件
this.armatureDisplay.addEventListener(
dragonBones.EventObject.COMPLETE,
this.animationEventHandler,
this
);

// 动态加载resources目录下的DragonBones资源
cc.loader.loadResDir('dragonbone/effect/txk', (err, assets) => {
if (err || assets.length == 0) return;

for (let i = 0; i < assets.length; i++) {
if (assets[i] instanceof dragonBones.DragonBonesAsset) {
this.armatureDisplay.dragonAsset = assets[i];
}

if (assets[i] instanceof dragonBones.DragonBonesAtlasAsset) {
this.armatureDisplay.dragonAtlasAsset = assets[i];
}
}

// 设置骨骼名称并播放动画
this.armatureDisplay.armatureName = "txk";
this.armatureDisplay.playAnimation("wait_1", 1);
});
},

animationEventHandler(event) {
if (event.type === dragonBones.EventObject.COMPLETE) {
// 动画完成后循环播放
this.armatureDisplay.playAnimation("wait_1", 1);
}
}

远程资源加载

从服务器加载:

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
let animNode = new cc.Node();
animNode.parent = cc.find('Canvas');
let dragonDisplay = animNode.addComponent(dragonBones.ArmatureDisplay);

// 远程资源URL
let image = 'http://localhost:7456/res/raw-assets/eee_tex-1529064342.png';
let ske = 'http://localhost:7456/res/raw-assets/eee_ske-1529065642.json';
let atlas = 'http://localhost:7456/res/raw-assets/eee_tex-1529065642.json';

// 按顺序加载:图片 -> 图集配置 -> 骨骼配置
cc.loader.load(image, (error, texture) => {
cc.loader.load({ url: atlas, type: 'txt' }, (error, atlasJson) => {
cc.loader.load({ url: ske, type: 'txt' }, (error, dragonBonesJson) => {
// 创建图集资源
let atlasAsset = new dragonBones.DragonBonesAtlasAsset();
atlasAsset.atlasJson = atlasJson;
atlasAsset.texture = texture;

// 创建骨骼资源
let asset = new dragonBones.DragonBonesAsset();
asset.dragonBonesJson = dragonBonesJson;

// 绑定资源
dragonDisplay.dragonAtlasAsset = atlasAsset;
dragonDisplay.dragonAsset = asset;

// 播放动画
dragonDisplay.armatureName = 'box_anim';
dragonDisplay.playAnimation('box_anim', 0);
});
});
});

注意:循环播放可能会报错Cannot read property 'loopComplete' of null,建议用事件监听处理。

按钮事件系统

模拟点击

手柄适配时可能需要代码触发按钮点击:

1
2
// 获取按钮组件并触发点击事件
this.node.getComponent(cc.Button).clickEvents[0].emit(['click']);

事件绑定方式

方式一:EventHandler API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建点击事件处理器
var clickEventHandler = new cc.Component.EventHandler();
clickEventHandler.target = this.node; // 目标节点
clickEventHandler.component = "MyComponent"; // 组件名称
clickEventHandler.handler = "callback"; // 回调方法
clickEventHandler.customEventData = "xxx"; // 自定义数据

// 绑定到按钮
var button = this.node.getComponent(cc.Button);
button.clickEvents.push(clickEventHandler);

// 回调函数定义
callback(event, customEventData) {
// event.target 为按钮绑定的节点
// customEventData 为 "xxx"
}

方式二:on API(推荐)

1
2
3
4
5
6
// 更简洁的事件绑定方式
this.node.on('click', this.callback, this);

callback(event) {
// event.detail 为cc.Button组件
}

JNI跨平台调用

JS调用Android方法

Creator通过jsb.reflection提供JNI调用:

1
2
3
4
5
6
7
8
9
10
if (jsb) {
// 调用Android静态方法
let result = jsb.reflection.callStaticMethod(
"org/cocos2dx/javascript/AppActivity", // 类路径
"pay", // 方法名
"(Ljava/lang/String;)Ljava/lang/String;", // 方法签名
"1" // 参数
);
cc.log(result);
}

方法签名说明:

类型 签名
void V
boolean Z
int I
long J
float F
double D
String Ljava/lang/String;
Object L包名/类名;
String[] [Ljava/lang/String;

Java方法定义

1
2
3
4
5
6
7
public class AppActivity extends Cocos2dxActivity {
// JS可调用的静态方法
public static String pay(String index) {
// 实现支付逻辑
return "success";
}
}

完整JNI示例

C++层封装:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

#define PAGENAMEPATH "org/cocos2dx/cpp/AppActivity"

std::string callJavaPay(const std::string& index) {
JniMethodInfo minfo;

// 获取静态方法信息
bool isHave = JniHelper::getStaticMethodInfo(
minfo,
PAGENAMEPATH,
"pay",
"(Ljava/lang/String;)Ljava/lang/String;"
);

if (!isHave) {
CCLOG("jni:no method");
return "";
}

// 转换参数为jstring
jstring idx = minfo.env->NewStringUTF(index.c_str());

// 调用Java方法
jstring result = (jstring)minfo.env->CallStaticObjectMethod(
minfo.classID,
minfo.methodID,
idx
);

// 转换返回值
char* rtn = nullptr;
jclass clsstring = minfo.env->FindClass("java/lang/String");
jstring strencode = minfo.env->NewStringUTF("utf-8");
jmethodID mid = minfo.env->GetMethodID(
clsstring,
"getBytes",
"(Ljava/lang/String;)[B"
);

jbyteArray barr = (jbyteArray)minfo.env->CallObjectMethod(
result,
mid,
strencode
);

jsize alen = minfo.env->GetArrayLength(barr);
jbyte* ba = minfo.env->GetByteArrayElements(barr, JNI_FALSE);

if (alen > 0) {
rtn = (char*)malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}

minfo.env->ReleaseByteArrayElements(barr, ba, 0);

return std::string(rtn);
}

第三方SDK集成

JAR包导入

步骤1:proj.android-studio/app/目录下创建libs文件夹

步骤2: 复制JAR包到libs目录

步骤3: 配置build.gradle

1
2
3
4
5
6
7
8
9
10
11
dependencies {
// 自动加载libs目录下的JAR文件
implementation fileTree(include: ['*.jar'], dir: 'libs')

// Cocos2d-x依赖
implementation fileTree(
include: ['*.jar'],
dir: 'F:/CocosCreator204/resources/cocos2d-x/cocos/platform/android/java/libs'
)
implementation project(':libcocos2dx')
}

Assets资源拷贝

SDK可能需要额外的资源文件,打包时自动拷贝:

1
2
3
4
5
6
7
8
9
10
11
android {
// ...
}

// 在build.gradle末尾添加资源拷贝任务
variant.mergeAssets.doLast {
copy {
from "${buildDir}/../../../../../treeSDKAssets"
into "${buildDir}/intermediates/assets/${variant.dirName}/treeSDKAssets"
}
}

AndroidManifest配置

按SDK文档添加必要的权限和Activity:

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
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yourcompany.yourapp">

<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher">

<!-- SDK Activity声明 -->
<activity android:name="com.thirdsdk.SDKActivity" />

<!-- 主Activity -->
<activity android:name="org.cocos2dx.javascript.AppActivity"
android:screenOrientation="landscape"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

平台判断与音频适配

平台判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 判断是否为iOS平台
if (cc.sys.os == cc.sys.OS_IOS) {
// iOS特定逻辑
cc.log("Running on iOS");
}

// 完整的平台判断
switch (cc.sys.os) {
case cc.sys.OS_IOS:
// iOS处理
break;
case cc.sys.OS_ANDROID:
// Android处理
break;
case cc.sys.OS_WINDOWS:
// Windows处理
break;
}

7.2 音频格式适配

注意: iOS设备不支持OGG格式,建议统一使用MP3:

1
2
3
4
5
6
7
8
9
10
// 根据平台选择音频格式
let audioUrl = cc.sys.os == cc.sys.OS_IOS
? "res/audio/sound.mp3"
: "res/audio/sound.ogg";

cc.loader.loadRes(audioUrl, cc.AudioClip, (err, clip) => {
if (!err) {
cc.audioEngine.playEffect(clip, false);
}
});

手机振动功能

检测与调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 判断是否支持振动
supportsVibrate() {
return "vibrate" in navigator;
},

// 开始振动(默认1秒)
startVibrate(duration = 1000) {
if (navigator.vibrate) {
// 标准API
navigator.vibrate(duration);
} else if (navigator.webkitVibrate) {
// WebKit前缀API
navigator.webkitVibrate(duration);
}
},

// 使用示例
onTouchStart() {
if (this.supportsVibrate()) {
this.startVibrate(200); // 振动200毫秒
}
}

总结一下

Cocos Creator跨平台开发方便是方便,但坑也不少。Android配置、横竖屏、DragonBones、JNI调用、SDK集成这些环节都得仔细处理。

核心要点:

  1. Gradle配置要理解,常见错误有套路
  2. 横竖屏注意webOrientation配置
  3. DragonBones本地/远程加载方式要区分
  4. JNI方法签名要搞对
  5. SDK集成记得配置资源拷贝

参考: