Cocos Creator开发踩坑记录

Cocos Creator开发踩坑记录

用Cocos Creator开发游戏有一段时间了,从UI交互到平台适配,踩了不少坑。记录一下实战经验和解决方案。

UI交互开发

可拖动按钮

游戏里经常需要能拖动的按钮,但又不想触发点击。实现思路:拖动时禁用按钮。

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
const { ccclass, property } = cc._decorator;

@ccclass
export default class DraggableButton extends cc.Component {
onLoad() {
const button = this.node.getComponent(cc.Button);

if (button) {
// 拖动时禁用按钮
this.node.on(cc.Node.EventType.TOUCH_MOVE, () => {
button.interactable = false;
});

// 松手或取消时恢复
this.node.on(cc.Node.EventType.TOUCH_END, () => {
button.interactable = true;
});

this.node.on(cc.Node.EventType.TOUCH_CANCEL, () => {
button.interactable = true;
});
}
}

onDestroy() {
this.node.off(cc.Node.EventType.TOUCH_MOVE);
this.node.off(cc.Node.EventType.TOUCH_END);
this.node.off(cc.Node.EventType.TOUCH_CANCEL);
}
}
事件 触发时机 处理
TOUCH_MOVE 触摸移动 禁用按钮
TOUCH_END 触摸结束 恢复按钮
TOUCH_CANCEL 触摸取消 恢复按钮

事件冒泡与穿透

1
2
3
4
5
6
7
8
// 设置事件不吞噬(穿透)
this.node._touchListener.setSwallowTouches(false);

// 事件监听
this.node.on(cc.Node.EventType.TOUCH_END, (event) => {
console.log("Node clicked");
// event.stopPropagation() // 阻止冒泡
}, this);

事件阶段:CAPTURE(捕获)→ TARGET(目标)→ BUBBLE(冒泡)

自定义鼠标样式

PC端游戏可以自定义鼠标:

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
const { ccclass, property } = cc._decorator;

@ccclass
export default class CustomCursor extends cc.Component {
@property(cc.Prefab)
cursorPrefab: cc.Prefab = null;

private cursor: cc.Node = null;

onLoad() {
// 隐藏默认鼠标
cc.game.canvas.style.cursor = 'none';

// 创建自定义鼠标
this.cursor = cc.instantiate(this.cursorPrefab);
this.cursor.parent = this.node;

// 监听鼠标移动
this.node.on(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
}

onMouseMove(event: cc.Event.EventMouse) {
const pos = event.getLocation();
this.cursor.position = cc.v2(
pos.x - cc.winSize.width / 2,
pos.y - cc.winSize.height / 2
);
}

onDestroy() {
cc.game.canvas.style.cursor = '';
this.node.off(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
}
}

要点:

  1. cc.game.canvas.style.cursor = 'none'隐藏默认光标
  2. 预制体做鼠标精灵
  3. 屏幕坐标转世界坐标
  4. 组件销毁时恢复光标

动画系统

stopAllActions的坑

stopAllActions()只停当前节点的Action,子节点的不会停:

1
2
3
4
5
6
7
// 停当前节点
this.node.stopAllActions();

// 子节点要自己停
this.node.children.forEach(child => {
child.stopAllActions();
});

Tween动画

循环动画:

1
2
3
4
5
6
7
cc.tween(this.node)
.repeatForever(
cc.tween()
.by(0.3, { position: cc.v2(0, -30) })
.by(0.3, { position: cc.v2(0, 30) })
)
.start();

组合动画:

1
2
3
4
5
6
7
8
cc.tween(this.node)
.to(1, { position: cc.v2(100, 100), scale: 1.5 })
.delay(0.5)
.to(1, { position: cc.v2(0, 0), scale: 1 })
.call(() => {
console.log("Animation complete");
})
.start();
方法 说明 示例
to 绝对值变化 .to(1, { x: 100 })
by 相对值变化 .by(1, { x: 100 })
delay 延迟 .delay(0.5)
call 回调 .call(callback)
repeat 重复N次 .repeat(3, tween)
repeatForever 无限循环 .repeatForever(tween)

网络通信

WebSocket证书问题

Android平台用WSS需要配置证书,Web和iOS不需要。

Web/iOS直接用:

1
this.wss = new WebSocket("wss://your-server.com");

Android需要证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (cc.sys.isNative && cc.sys.os === cc.sys.OS.ANDROID) {
// 从curl官网下载证书:https://curl.se/docs/caextract.html
let cacert = cc.url.raw('resources/Files/cacert.pem');

// MD5加密管道处理
if (cc.loader.md5Pipe) {
cacert = cc.loader.md5Pipe.transformURL(cacert);
}

this.wss = new WebSocket(url, null, cacert);
} else {
this.wss = new WebSocket(url);
}

配置步骤:

  1. curl官网下载 cacert.pem
  2. resources/Files/cacert.pem
  3. 代码里用上面的方式创建WebSocket

WebSocket状态管理

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
class WebSocketManager {
private ws: WebSocket = null;
private url: string = "";

connect(url: string) {
this.url = url;

if (cc.sys.isNative && cc.sys.OS.ANDROID) {
this.connectWithCert(url);
} else {
this.ws = new WebSocket(url);
this.bindEvents();
}
}

private bindEvents() {
this.ws.onopen = (event) => {
console.log("WebSocket connected");
};

this.ws.onmessage = (event) => {
console.log("Received:", event.data);
};

this.ws.onerror = (event) => {
console.error("WebSocket error:", event);
};

this.ws.onclose = (event) => {
console.log("WebSocket closed");
this.reconnect();
};
}

private reconnect() {
setTimeout(() => {
this.connect(this.url);
}, 3000);
}
}

调试工具

ccc-devtools

网页端预览调试神器,能实时看节点树、改属性。

功能:

  • 实时显示场景节点树
  • 同步更改节点属性
  • 输出节点、组件引用到控制台
  • 标记UI节点位置
  • 独立显示FPS等调试信息

安装:

  1. 下载:https://github.com/potato47/ccc-devtools
  2. 解压 preview-template.zip 到项目根目录
  3. 刷新浏览器,F12打开开发者工具
  4. Cocos标签页查看节点树

使用:

1
2
3
4
5
6
7
8
9
10
11
// 输出节点引用
const node = cc.find("Canvas/Player");
console.log(node);

// 查看组件
const sprite = node.getComponent(cc.Sprite);
console.log(sprite);

// 修改属性(实时查看)
node.x = 100;
node.color = cc.Color.RED;

VSCode配置

过滤.meta文件:

全局配置(settings.json):

1
2
3
4
5
{
"files.exclude": {
"**/*.meta": true
}
}

项目配置(.vscode/settings.json):

1
2
3
4
5
6
7
{
"files.exclude": {
"**/*.meta": true,
"**/build": true,
"**/temp": true
}
}

Android平台开发

NDK版本选择

不同NDK版本兼容性不一样:

NDK版本 状态
21.1.6352462 正常(推荐)
26.1.10909125 报错:implicit conversion错误

遇到error: implicit conversion from 'int' to 'float'就降级到NDK 21.x。

头条渠道权限问题

头条渠道有个坑:授权同意之前不能获取敏感权限,否则会检测为违规。

问题场景:

1
2
3
行为阶段:授权前行为
行为名称:注册传感器监听器
检测屏幕旋转方向的权限

解决:

修改Cocos2dxActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onResume() {
Log.d(TAG, "onResume()");
paused = false;
super.onResume();

if (gainAudioFocus) {
Cocos2dxAudioFocusManager.registerAudioFocusListener(this);
}
Utils.hideVirtualButton();
resumeIfHasFocus();

// 注释掉这行,授权后再初始化
// mCocos2dxOrientationHelper.onResume();
}

SDK初始化放在同意协议后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SDK.init(appActivity, new Callback() {
@Override
public <T> void onResult(int type, boolean isOk, T result) {
if (type == Callback.TYPE_INIT) {
appActivity.runOnGLThread(new Runnable() {
@Override
public void run() {
Cocos2dxJavascriptJavaBridge.evalString(
"window.GameJsb.sdkInitCallback(" + isOk + ")"
);
}
});
}
}
});

Android剪贴板

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
public String copyTextFromClipboard() {
String result = "";
try {
if (Looper.myLooper() == null) {
Looper.prepare();
}

ClipboardManager clipboard = (ClipboardManager)
this.getSystemService(Context.CLIPBOARD_SERVICE);

ClipData clipData = clipboard.getPrimaryClip();
int count = clipData.getItemCount();

for (int i = 0; i < count; i++) {
ClipData.Item item = clipData.getItemAt(i);
CharSequence text = item.coerceToText(this);
result += text.toString();
}

Toast.makeText(getApplicationContext(),
"成功获取剪贴板内容:" + result,
Toast.LENGTH_LONG).show();

} catch (Exception e) {
e.printStackTrace();
}
return result;
}

UI线程调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static String getClipboardText() {
FutureTask<String> future = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return appActivity.copyTextFromClipboard();
}
});

appActivity.runOnUiThread(future);

try {
return future.get();
} catch (Exception e) {
Log.e("Clipboard", "Error", e);
return "";
}
}

平台发布

Google Play关联风险

2022年后注册的开发者账号,如果因为关联被封,基本是这些原因:

类别 风险等级
IP地址、身份信息
卡号、手机号
Google Analytics、Facebook、AppsFlyer
包名、类名、资源文件
文案、图片、协议文本

IP清洁度验证:干净的IP注册时不会出现手机号验证界面。

Pixel设备调试

开启开发者模式:

  1. 设置 > 系统 > 关于手机
  2. 连续点击版本号10次
  3. 返回 > 开发者选项 > 开启USB调试

WiFi网络受限

Pixel连WiFi经常有感叹号,Captive Portal检测Google服务器的问题。

解决:

1
2
3
4
5
6
7
8
9
10
11
# 删除默认检测地址
adb shell settings delete global captive_portal_https_url
adb shell settings delete global captive_portal_http_url

# 设置国内服务器
adb shell settings put global captive_portal_http_url \
http://captive.v2ex.co/generate_204
adb shell settings put global captive_portal_https_url \
https://captive.v2ex.co/generate_204

# 切换飞行模式再切回来

传输文件

  1. USB线连接手机和电脑
  2. 下拉通知栏,点击USB选项
  3. 选”传输文件(MTP)”模式
  4. 电脑上打开”我的电脑”
  5. 双击Pixel设备访问文件

TLS时间同步

Pixel要求时间误差1分钟内,否则TLS连接失败。

1
2
adb shell settings put global auto_time 1
adb shell settings put global auto_time_zone 1

热更新

视频资源热更新问题

Cocos Creator 2.4.10原生平台,热更新后的MP4可能无法播放。

原因:Cocos2dxVideoView.java里数据源设置方式问题。

解决:修改cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxVideoView.java

1
2
3
4
5
// 修改前(有问题)
mRetriever.setDataSource(mVideoUri.toString(), new HashMap<String, String>());

// 修改后(正常)
mRetriever.setDataSource(mCocos2dxActivity.getContext(), mVideoUri);

第三方平台接入

微信小游戏

服务端口配置

报错”工具的服务端口已关闭”:

  1. 微信开发者工具 > 设置 > 安全设置
  2. 开启服务端口

获取OpenID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 前端调用
window["wx"].login({
success: (res) => {
if (res.code) {
console.log("Code:", res.code);
requestOpenId(res.code);
} else {
console.log("登录失败:", res.errMsg);
}
}
});

// 后端换取
async function requestOpenId(code) {
const response = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
);
const data = await response.json();
console.log("OpenID:", data.openid);
}

获取用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const button = wx.createUserInfoButton({
type: 'text',
text: '获取用户信息',
style: {
left: 10, top: 76, width: 200, height: 40,
lineHeight: 40, backgroundColor: '#ff0000',
color: '#ffffff', textAlign: 'center',
fontSize: 16, borderRadius: 4
}
});

button.onTap((res) => {
if (res.errMsg.indexOf(':ok') > -1) {
console.log("用户信息:", res.userInfo);
}
});

域名配置

小程序只能和配置的域名通信:

  • https(wx.request, wx.uploadFile, wx.downloadFile)
  • wss(wx.connectSocket)

开发环境可以跳过校验:开发者工具 > 设置 > 项目设置 > 不校验请求域名

H5游戏平台

CrazyGames

1
2
3
4
5
6
7
CrazyGames.SDK.init();

CrazyGames.SDK.ad.requestAd("midgame", {
adStarted: () => console.log("Ad started"),
adFinished: () => console.log("Ad finished"),
adError: () => console.log("Ad error")
});

Poki

1
2
3
4
5
6
PokiSDK.gameLoadingStart();
PokiSDK.gameLoadingFinished();

PokiSDK.commercialBreak().then(() => {
console.log("Commercial break complete");
});

Cocos Creator整体来说挺好用的,跨平台发布省很多事。但坑也不少:

  1. WebSocket证书Android和iOS要分开处理
  2. NDK版本要选稳定的,新版经常有坑
  3. 头条渠道的权限检测很严格
  4. Google Play关联检测越来越严
  5. 热更新后视频播放要注意

建议多看官方文档,遇到问题先查github issue。