Cocos Creator DragonBones动画远程加载完全指南

引言

DragonBones 是一款流行的开源骨骼动画编辑器,广泛应用于游戏开发中。在 Cocos Creator 项目中,动画资源往往需要动态加载,特别是在需要热更新或远程资源管理的场景中。本文将详细介绍如何在 Cocos Creator 中实现 DragonBones 动画的远程加载,包括资源解析、组件初始化和动画播放的完整流程。

DragonBones 资源结构

资源文件说明

DragonBones 导出的资源通常包含三个文件:

文件类型 扩展名 说明
纹理图集 _tex.png 动画帧图片合集
纹理配置 _tex.json 纹理坐标和区域信息
骨骼数据 _ske.json 骨骼结构、动画数据
1
2
3
4
dragon_res/
├── hero_tex.png # 纹理图集
├── hero_tex.json # 纹理配置
└── hero_ske.json # 骨骼数据

JSON 结构解析

骨骼数据 (_ske.json):

1
2
3
4
5
6
7
8
9
10
11
12
{
"version": "5.5",
"armature": [
{
"name": "hero",
"bone": [...],
"slot": [...],
"skin": [...],
"animation": [...]
}
]
}

纹理配置 (_tex.json):

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "hero",
"SubTexture": [
{
"name": "head",
"x": 0,
"y": 0,
"width": 100,
"height": 100
}
]
}

远程加载实现

基础加载示例

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
91
92
93
94
95
96
97
98
99
100
101
102
// DragonBonesLoader.js
const { dragonBones } = window;

class DragonBonesLoader {
/**
* 加载并播放 DragonBones 动画
* @param {cc.Node} parent - 父节点
* @param {string} baseUrl - 资源基础 URL
* @param {string} name - 动画名称
* @param {string} armatureName - 骨架名称
* @param {string} animName - 动画名称
*/
static loadAndPlay(parent, baseUrl, name, armatureName, animName) {
// 构建资源 URL
const imageUrl = `${baseUrl}/${name}_tex.png`;
const atlasUrl = `${baseUrl}/${name}_tex.json`;
const skeUrl = `${baseUrl}/${name}_ske.json`;

// 按顺序加载资源
this.loadResources(imageUrl, atlasUrl, skeUrl)
.then(({ texture, atlasJson, skeJson }) => {
// 创建动画节点
this.createArmatureDisplay(parent, texture, atlasJson, skeJson, armatureName, animName);
})
.catch(error => {
console.error('Failed to load dragonbones:', error);
});
}

/**
* 加载资源
*/
static loadResources(imageUrl, atlasUrl, skeUrl) {
return new Promise((resolve, reject) => {
// 1. 加载纹理图片
cc.loader.load(imageUrl, (err, texture) => {
if (err) {
reject(new Error(`Failed to load image: ${err}`));
return;
}

// 2. 加载纹理配置
cc.loader.load({ url: atlasUrl, type: 'txt' }, (err, atlasJson) => {
if (err) {
reject(new Error(`Failed to load atlas: ${err}`));
return;
}

// 3. 加载骨骼数据
cc.loader.load({ url: skeUrl, type: 'txt' }, (err, skeJson) => {
if (err) {
reject(new Error(`Failed to load skeleton: ${err}`));
return;
}

resolve({
texture: texture,
atlasJson: atlasJson,
skeJson: skeJson
});
});
});
});
});
}

/**
* 创建 ArmatureDisplay 组件
*/
static createArmatureDisplay(parent, texture, atlasJson, skeJson, armatureName, animName) {
// 创建节点
const animNode = new cc.Node('DragonBonesNode');
animNode.parent = parent;

// 添加 ArmatureDisplay 组件
const dragonDisplay = animNode.addComponent(dragonBones.ArmatureDisplay);

// 创建纹理资源
const atlasAsset = new dragonBones.DragonBonesAtlasAsset();
atlasAsset.atlasJson = atlasJson;
atlasAsset.texture = texture;

// 创建骨骼资源
const dbAsset = new dragonBones.DragonBonesAsset();
dbAsset.dragonBonesJson = skeJson;

// 设置资源
dragonDisplay.dragonAtlasAsset = atlasAsset;
dragonDisplay.dragonAsset = dbAsset;

// 设置骨架名称
dragonDisplay.armatureName = armatureName;

// 播放动画
dragonDisplay.playAnimation(animName, 0);

return dragonDisplay;
}
}

// 导出
window.DragonBonesLoader = DragonBonesLoader;

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在主场景中使用
cc.Class({
extends: cc.Component,

properties: {
// 动画显示区域
animArea: cc.Node
},

onLoad() {
// 加载远程动画
DragonBonesLoader.loadAndPlay(
this.animArea,
'https://your-cdn.com/dragon_res',
'hero',
'hero',
'idle'
);
}
});

进阶实现

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
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
class CachedDragonBonesLoader {
constructor() {
// 缓存已加载的资源
this.cache = new Map();
}

/**
* 获取缓存键
*/
getCacheKey(baseUrl, name) {
return `${baseUrl}/${name}`;
}

/**
* 加载资源(带缓存)
*/
load(baseUrl, name, armatureName) {
const cacheKey = this.getCacheKey(baseUrl, name);

// 检查缓存
if (this.cache.has(cacheKey)) {
console.log('Using cached resources:', cacheKey);
return Promise.resolve(this.cache.get(cacheKey));
}

// 加载新资源
const imageUrl = `${baseUrl}/${name}_tex.png`;
const atlasUrl = `${baseUrl}/${name}_tex.json`;
const skeUrl = `${baseUrl}/${name}_ske.json`;

return this.loadResources(imageUrl, atlasUrl, skeUrl)
.then(resources => {
// 存入缓存
this.cache.set(cacheKey, resources);
return resources;
});
}

/**
* 创建动画显示(使用缓存)
*/
createDisplay(parent, baseUrl, name, armatureName, animName) {
return this.load(baseUrl, name, armatureName)
.then(({ texture, atlasJson, skeJson }) => {
// 创建新的节点和组件(资源可复用)
const animNode = new cc.Node(name);
animNode.parent = parent;

const dragonDisplay = animNode.addComponent(dragonBones.ArmatureDisplay);

const atlasAsset = new dragonBones.DragonBonesAtlasAsset();
atlasAsset.atlasJson = atlasJson;
atlasAsset.texture = texture;

const dbAsset = new dragonBones.DragonBonesAsset();
dbAsset.dragonBonesJson = skeJson;

dragonDisplay.dragonAtlasAsset = atlasAsset;
dragonDisplay.dragonAsset = dbAsset;
dragonDisplay.armatureName = armatureName;
dragonDisplay.playAnimation(animName, 0);

return dragonDisplay;
});
}

/**
* 清除缓存
*/
clearCache() {
this.cache.clear();
}

/**
* 清除特定资源缓存
*/
removeFromCache(baseUrl, name) {
const cacheKey = this.getCacheKey(baseUrl, name);
this.cache.delete(cacheKey);
}
}

// 单例实例
window.DragonBonesCache = new CachedDragonBonesLoader();

2. 加载进度管理

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
91
92
class DragonBonesLoadManager {
constructor() {
this.loadingTasks = new Map();
this.progressCallbacks = new Map();
}

/**
* 加载并跟踪进度
*/
loadWithProgress(baseUrl, name, onProgress) {
const taskKey = `${baseUrl}/${name}`;

if (this.loadingTasks.has(taskKey)) {
// 复用已有任务
return this.loadingTasks.get(taskKey);
}

const loadPromise = this.doLoad(baseUrl, name, (progress) => {
const callbacks = this.progressCallbacks.get(taskKey) || [];
callbacks.forEach(cb => cb(progress));
});

this.loadingTasks.set(taskKey, loadPromise);

// 注册进度回调
if (onProgress) {
if (!this.progressCallbacks.has(taskKey)) {
this.progressCallbacks.set(taskKey, []);
}
this.progressCallbacks.get(taskKey).push(onProgress);
}

// 清理
loadPromise.finally(() => {
this.loadingTasks.delete(taskKey);
this.progressCallbacks.delete(taskKey);
});

return loadPromise;
}

/**
* 执行加载
*/
doLoad(baseUrl, name, onProgress) {
const urls = [
{ url: `${baseUrl}/${name}_tex.png`, type: 'progress', weight: 0.4 },
{ url: `${baseUrl}/${name}_tex.json`, type: 'txt', weight: 0.3 },
{ url: `${baseUrl}/${name}_ske.json`, type: 'txt', weight: 0.3 }
];

let totalProgress = { texture: 0, atlas: 0, ske: 0 };

const loadResource = (config, index) => {
return new Promise((resolve, reject) => {
const onLoadProgress = config.type === 'progress'
? (completedCount, totalCount, item) => {
const progress = completedCount / totalCount;
if (index === 0) totalProgress.texture = progress * config.weight;
else if (index === 1) totalProgress.atlas = progress * config.weight;
else totalProgress.ske = progress * config.weight;

const total = totalProgress.texture + totalProgress.atlas + totalProgress.ske;
onProgress(total);
}
: undefined;

cc.loader.load(config.url, onLoadProgress, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};

return Promise.all(urls.map((config, index) => loadResource(config, index)))
.then(([texture, atlasJson, skeJson]) => ({ texture, atlasJson, skeJson }));
}
}

// 使用
const manager = new DragonBonesLoadManager();

manager.loadWithProgress(
'https://cdn.example.com/anims',
'hero',
(progress) => {
console.log(`Loading progress: ${(progress * 100).toFixed(1)}%`);
// 更新 UI 进度条
}
).then(resources => {
console.log('Load complete');
});

3. 错误重试机制

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
class ResilientDragonBonesLoader {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
}

/**
* 带重试的加载
*/
loadWithRetry(url, type = '') {
return new Promise((resolve, reject) => {
let attempts = 0;

const tryLoad = () => {
attempts++;

const loadOptions = type ? { url, type } : url;
cc.loader.load(loadOptions, (err, result) => {
if (err) {
if (attempts < this.maxRetries) {
console.warn(`Load failed, retrying (${attempts}/${this.maxRetries}): ${url}`);
setTimeout(tryLoad, this.retryDelay * attempts);
} else {
reject(new Error(`Failed to load after ${this.maxRetries} attempts: ${url}`));
}
} else {
resolve(result);
}
});
};

tryLoad();
});
}

/**
* 加载完整资源
*/
loadComplete(baseUrl, name) {
const imageUrl = `${baseUrl}/${name}_tex.png`;
const atlasUrl = `${baseUrl}/${name}_tex.json`;
const skeUrl = `${baseUrl}/${name}_ske.json`;

return Promise.all([
this.loadWithRetry(imageUrl),
this.loadWithRetry(atlasUrl, 'txt'),
this.loadWithRetry(skeUrl, 'txt')
]).then(([texture, atlasJson, skeJson]) => ({
texture,
atlasJson,
skeJson
}));
}
}

完整组件封装

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

@ccclass
export default class DragonBonesComponent extends cc.Component {
@property(cc.String)
resourceUrl: string = '';

@property(cc.String)
armatureName: string = '';

@property(cc.String)
defaultAnimation: string = '';

@property(cc.Boolean)
playOnLoad: boolean = true;

@property(cc.Boolean)
cacheResources: boolean = true;

private _isLoaded: boolean = false;
private _display: dragonBones.ArmatureDisplay = null;

onLoad() {
if (this.playOnLoad && this.resourceUrl) {
this.loadAndPlay();
}
}

/**
* 加载并播放
*/
loadAndPlay() {
if (this._isLoaded) return;

const parts = this.resourceUrl.split('/');
const name = parts.pop() || '';
const baseUrl = parts.join('/');

DragonBonesLoader.loadAndPlay(
this.node,
baseUrl,
name,
this.armatureName,
this.defaultAnimation
);

this._isLoaded = true;
}

/**
* 播放指定动画
*/
play(animName: string, playTimes: number = 0) {
if (this._display) {
this._display.playAnimation(animName, playTimes);
}
}

/**
* 暂停
*/
pause() {
if (this._display) {
this._display.pause();
}
}

/**
* 恢复
*/
resume() {
if (this._display) {
this._display.resume();
}
}

/**
* 停止
*/
stop() {
if (this._display) {
this._display.stop();
}
}
}

常见问题

1. 跨域问题

1
2
3
4
5
6
7
8
9
10
11
12
// 确保 CDN 设置了 CORS 头
// Access-Control-Allow-Origin: *

// 或使用代理
const proxyUrl = 'https://your-proxy.com?url=';
DragonBonesLoader.loadAndPlay(
this.node,
proxyUrl + encodeURIComponent('https://cdn.com/dragon_res'),
'hero',
'hero',
'idle'
);

2. 内存释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 清理资源时释放内存
function releaseDragonBones(node) {
const display = node.getComponent(dragonBones.ArmatureDisplay);
if (display) {
display.destroy();
}

// 释放纹理
const texture = display.dragonAtlasAsset.texture;
cc.loader.release(texture);

// 释放骨骼数据
cc.loader.release(display.dragonAsset);
}

3. 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 批量预加载
function preloadDragonBonesResources(baseUrl, names) {
const urls = [];
names.forEach(name => {
urls.push(`${baseUrl}/${name}_tex.png`);
urls.push(`${baseUrl}/${name}_tex.json`);
urls.push(`${baseUrl}/${name}_ske.json`);
});

cc.loader.load(urls, (completedCount, totalCount) => {
console.log(`Preloading: ${completedCount}/${totalCount}`);
}, (err) => {
if (err) {
console.error('Preload failed:', err);
} else {
console.log('Preload complete');
}
});
}

总结

DragonBones 远程加载关键点:

  1. 资源准备:确保导出格式正确,三个文件齐全
  2. 加载顺序:先加载纹理图片,再加载 JSON 配置
  3. 组件创建:创建 DragonBonesAtlasAsset 和 DragonBonesAsset
  4. 资源管理:使用缓存和预加载优化性能
  5. 错误处理:添加重试机制提高成功率

通过以上方法,可以在 Cocos Creator 中实现灵活的 DragonBones 动画远程加载和管理。