接上一节:游戏角色动画:从入门到商用(一)
一般游戏做到上面一步就可以了,如果要进一步的优化,会发现每个图集都会产生 plist 文件,多个图集那么 plist 也会很多。通常一个角色的资源都是一次性加载进内存,图片资源因为有最大尺寸限制,只能分为多张不同的文件中,但 plist 文件并没有限制,有没有办法将多个图集的 plist 文件合并成一个文件呢?不幸的是,查阅了 TP的官方文档并没有这个功能。观察 plist 文件的结构:
<plist version="1.0">
<dict>
<key>frameskey>
<dict>
<key>body_100354_idle_4_00.pngkey>
<dict>
<key>framekey>
<string>{{2,636},{204,320}}string>
<key>offsetkey>
<string>{-17,-15}string>
<key>rotatedkey>
<false/>
<key>sourceColorRectkey>
<string>{{159,133},{204,320}}string>
<key>sourceSizekey>
<string>{556,556}string>
dict>
...
dict>
plist>
可以发现 plist 文件实际上是一种 xml 格式存储的文件,其原点 (0, 0) 在左上角,其中保存了每张图片的:
再看cocos creator 的官方文档中SpriteFrame的构造函数:
cc.SpriteFrame(texure:cc.Texture2D, rect: cc.Rect, rotated:bool, offset:cc.Vec2, originalSize:cc.Size)
创建精灵帧时,需要plist中的 frame、rotated、offset和sourceSize四个值。因此可以自己定义一个配置文件格式,在创建帧动画时,可以通过每一帧的名字,在配置文件中获取它所在的图集及其在图集中的位置信息。除此之外,需要要知道总共有多少图集,每个动作总共有多少方向,每个方向总共有多少帧数,帧率。最终配置文件格式如下:
{
"cnt": 图集数量,
"act": {
"actionName": [方向数, 该方向的总帧数, 帧率]
},
"frames": {
"filename": [图集序号, rect, rotated, offset, originalSize],
},
}
可以通过脚本,读取 TexurePakcer 生成的多个 plist 文件,生成上一个上述格式的配置文件。导出来的配置文件内容如下:
将所有 plist 合并成一个配置文件之后,角色动画的创建步骤:
loadConfig() {
let url = "demo/body/100354/100354";
cc.resources.load(url, cc.JsonAsset, (error: Error, jsonAsset: cc.JsonAsset) => {
if (error) {
console.error(error.message || error);
return;
}
this.config = jsonAsset.json;
this.loadTotalCnt = this.config["cnt"];
this.loadTexure()
});
}
loadTexure() {
let url = "demo/body/100354/100354-" + this.loadIndex;
cc.resources.load(url, cc.Texture2D, (error: Error, texure: cc.Texture2D) => {
if (error) {
console.error(error.message || error);
return;
}
this.textureList.push(texure)
// 加载帧动画资源
if (this.loadIndex == this.loadTotalCnt) {
this.onLoadTexure();
return;
}
this.loadIndex += 1;
this.loadTexure()
});
}
onLoadTexure() {
let dir_cnt, frame_cnt, fps, frames;
let actions = this.config["actions"];
for (let actionName in actions) {
if (this.actions.indexOf(actionName) < 0) {
this.actions.push(actionName);
}
let item = actions[actionName]
dir_cnt = item[0];
frame_cnt = item[1];
fps = item[2];
for (let dir = 0; dir < dir_cnt; dir++) {
frames = this.getFrames(actionName, frame_cnt, dir);
this.createAnimationClip(actionName, dir, fps, frames);
}
}
let actionName = "idle";
this.actionIndex = this.actions.indexOf(actionName);
this.playAnimation(actionName, this.dir, cc.WrapMode.Loop);
}
getFrames(actionName, frame_cnt, dir) {
let frames = []
for (let i = 0; i < frame_cnt; i++) {
let filename = "body_100354_" + actionName + "_" + dir + "_" + (i < 10 ? "0" + i : i);
let item = this.config["frames"][filename];
let texIndex = item[0];
let tex = this.textureList[texIndex];
let rect = new cc.Rect(item[1], item[2], item[3], item[4]);
let rotated = !!item[5];
let offset = cc.v2(item[6], item[7]);
let size = new cc.Size(item[8], item[9]);
let frame = new cc.SpriteFrame(tex, rect, rotated, offset, size);
frame.name = filename;
frames.push(frame);
}
return frames;
}
这一节的主要目的是减少文件数量,优化加载时文件 IO。代码的主要差别时,需要先加载配置文件,然后通过配置文件创建动画的精灵帧。
角色除了有不同动画,不同方向之后,还有多个动画节点,前面的例子是使用的角色身体动画,根据设计还会有武器和翅膀。动画节点的划分主要时根据角色可环装的部分设计的,如果一个部分支持环装,就要做成独立的动画节点。有多个节点之后,节点的遮挡关系会随着方向变化,默认角色层级,按角色朝向玩家设置,当玩家背向玩家时,修改层级。
ndActor: cc.Node = null; // 角色节点
ndActorBody: cc.Node = null; // 角色身体
ndActorWeapon: cc.Node = null; // 角色武器
ndActorWing: cc.Node = null; // 角色翅膀
dir: number = 4; // 方向
actions: string[] = [];// 动作列表
actionIndex: number = 0;// 动作下标
aniResCache: object = {}; // 配置文件
onLoad() {
this.ndActor = this.createActorNode();
this.createBody((node) => {
this.ndActorBody = node;
this.onPartLoadFinish();
});
this.createWeapon((node) => {
this.ndActorWeapon = node;
this.onPartLoadFinish();
});
this.createWing((node) => {
this.ndActorWing = node;
this.onPartLoadFinish();
});
}
createActorNode() {
// 创建角色节点
let node = new cc.Node();
node.parent = this.node;
return node;
}
onPartLoadFinish() {
if (this.ndActorBody && this.ndActorWeapon && this.ndActorWing) {
let actionName = "idle"
this.actionIndex = this.actions.indexOf(actionName);
this.playAnimation(actionName, this.dir, cc.WrapMode.Loop);
}
}
createAnimationNode() {
let node = new cc.Node();
let sprite = node.addComponent(cc.Sprite);
sprite.sizeMode = cc.Sprite.SizeMode.RAW;
sprite.trim = false;
node.addComponent(cc.Animation);
node.parent = this.ndActor;
return node;
}
createBody(cb) {
let part = "body";
let no = "100354";
let node = this.createAnimationNode();
node.name = part + "name";
this.loadConfig(part, no, (config) => {
this.onLoadConfig(node, part, no, config, cb);
});
}
createWeapon(cb) {
let part = "weapon";
let no = "100354";
let node = this.createAnimationNode();
node.name = part + "name";
this.loadConfig(part, no, (config) => {
this.onLoadConfig(node, part, no, config, cb);
});
}
createWing(cb) {
let part = "wing";
let no = "100307";
let node = this.createAnimationNode();
node.name = part + no;
this.loadConfig(part, no, (config) => {
if (config) {
this.onLoadConfig(node, part, no, config, cb);
}
});
}
loadConfig(part: string, no: string, cb) {
let url = "demo/" + part + "/" + no + "/" + no;
cc.resources.load(url, cc.JsonAsset, (error: Error, jsonAsset: cc.JsonAsset) => {
if (error) {
console.error(error.message || error);
cb(null);
return;
}
cb(jsonAsset.json);
});
}
onLoadConfig(node, part, no, config, cb) {
if (!this.aniResCache[part]) {
this.aniResCache[part] = {};
}
this.aniResCache[part][no] = {
"config": config, // 配置文件
"loadTotalCnt": config["cnt"], // 图集数量
"loadIndex": 0, // 已加载下标
"textureList": [], // 动画纹理对象
}
this.loadTexure(node, part, no, cb)
}
loadTexure(node, part, no, cb) {
let item = this.aniResCache[part][no];
let url = "demo/" + part + "/" + no + "/" + no + "-" + item.loadIndex;
cc.resources.load(url, cc.Texture2D, (error: Error, texure: cc.Texture2D) => {
if (error) {
console.error(error.message || error);
} else {
item.textureList.push(texure)
}
if (item.loadIndex === item.loadTotalCnt) {
this.onLoadTexureFinish(node, part, no, cb);
return;
}
item.loadIndex += 1;
this.loadTexure(node, part, no, cb)
});
}
onLoadTexureFinish(node, part, no, cb) {
let item, dir_cnt, frame_cnt, fps, frames;
let partConfig = this.aniResCache[part][no];
let actions = partConfig.config["actions"];
for (let actionName in actions) {
if (this.actions.indexOf(actionName) < 0) {
this.actions.push(actionName);
}
item = actions[actionName]
dir_cnt = item[0];
frame_cnt = item[1];
fps = item[2];
for (let dir = 0; dir < dir_cnt; dir++) {
frames = this.getFrames(part, no, actionName, frame_cnt, dir);
this.createAnimationClip(node, actionName + "_" + dir, fps, frames);
}
}
cb(node);
}
getFrames(part, no, actionName, frame_cnt, dir) {
let partConfig = this.aniResCache[part][no];
let frames = []
for (let i = 0; i < frame_cnt; i++) {
let filename = part + "_" + no + "_" + actionName + "_" + dir + "_" + (i < 10 ? "0" + i : i);
let item = partConfig.config["frames"][filename];
let tex = partConfig.textureList[item[0]];
let rect = new cc.Rect(item[1], item[2], item[3], item[4]);
let rotated = !!item[5];
let offset = cc.v2(item[6], item[7]);
let size = new cc.Size(item[8], item[9]);
let frame = new cc.SpriteFrame(tex, rect, rotated, offset, size);
frame.name = filename;
frames.push(frame);
}
return frames;
}
createAnimationClip(node: cc.Node, clipName: string, sample: number, spriteFrames: cc.SpriteFrame[]) {
// 创建动画剪辑
let clip = cc.AnimationClip.createWithSpriteFrames(spriteFrames, sample);
node.getComponent(cc.Animation).addClip(clip, clipName);
}
playAnimation(actionName, dir, mode) {
let real_dir = this.getRealDir(dir);
let clipName = actionName + "_" + real_dir;
let scaleX = dir < 5 ? 1 : -1;
if (dir === 0 || dir === 1 || dir === 7) {
this.ndActorBody.zIndex = 1;
this.ndActorWeapon.zIndex = 2;
this.ndActorWing.zIndex = 3;
} else {
this.ndActorBody.zIndex = 2;
this.ndActorWeapon.zIndex = 3;
this.ndActorWing.zIndex = 1;
}
// 播放动画
let aniState = this.ndActorBody.getComponent(cc.Animation).play(clipName);
aniState.wrapMode = mode;
this.ndActorBody.scaleX = scaleX;
aniState = this.ndActorWeapon.getComponent(cc.Animation).play(clipName);
aniState.wrapMode = mode;
this.ndActorWeapon.scaleX = scaleX;
aniState = this.ndActorWing.getComponent(cc.Animation).play(clipName);
aniState.wrapMode = mode;
this.ndActorWing.scaleX = scaleX;
}
代码中,分别创建身体,武器,翅膀的动画,都加载完成之后,再播放。效果如图:
动画资源2d游戏中占有量最大,也很通用的资源,为了资源的加载过程,复用已经加载的资源,抽离动一个画资源管理器,来加载动画配置、加载动画纹理、创建动画节点。动画资源加载之后,最好不要在开始就把所有的动画剪辑都创建出来。按身体、武器、翅膀3个组成部分,5方向资源,10个动作计算,那么每个角色就有 150 个动画,若开始就创建这 150 个动画剪辑,可能很多都用不到。因此再抽离一个动画控制器,在播放动画时,再创建动画剪辑,并将创建好的动画记录下来,避免重复创建。
/**
* 动画控制器
* 功能:
* 创建动画剪辑
* 动画播放控制
*/
const { ccclass, property } = cc._decorator
@ccclass
export default class AniCtrl extends cc.Component {
type: string = ""
name: string = ""
cache = null
animation: cc.Animation = null
clipMap = {}
init(type: string, name: string, cache) {
this.type = type
this.name = name
this.cache = cache
this.animation = this.node.getComponent(cc.Animation);
}
initClip(actionName, realDir) {
let clipName = actionName + "_" + realDir
if (this.clipMap[clipName]) {
return this.clipMap[clipName]
}
let action = this.cache.config.actions[actionName]
if (!action) {
return;
}
let frameCnt = action[1];
let fps = action[2];
let frames = this.getFrames(actionName, realDir, frameCnt);
let clip = cc.AnimationClip.createWithSpriteFrames(frames, fps);
this.animation.addClip(clip, clipName);
this.clipMap[clipName] = clip;
return clip
}
getFrames(actionName, dir, frameCnt) {
let frames = []
for (let i = 0; i < frameCnt; i++) {
let filename = this.type + "_" + this.name + "_" + actionName + "_" + dir + "_" + (i < 10 ? "0" + i : i);
let item = this.cache.config["frames"][filename];
let tex = this.cache.texList[item[0]];
let rect = new cc.Rect(item[1], item[2], item[3], item[4]);
let rotated = !!item[5];
let offset = cc.v2(item[6], item[7]);
let size = new cc.Size(item[8], item[9]);
let frame = new cc.SpriteFrame(tex, rect, rotated, offset, size);
frame.name = filename;
frames.push(frame);
}
return frames;
}
play(actionName, dir, startTime, loop, speed = 1.0, delay = 0){
let config = this.cache.config
if (!config.actions[actionName]) {
console.error("no action " + actionName + " in " + this.type + "_" + this.name)
return;
}
let realDir = this.getRealDir(dir)
if (!this.initClip(actionName, realDir)) {
return;
}
this.node.scaleX = dir < 5 ? 1 : -1;
let clipName = actionName + "_" + realDir
let aniState = this.animation.play(clipName, startTime)
if (!aniState) {
console.error("no clip " + clipName + " in " + this.type + "_" + this.name)
return
}
aniState.wrapMode = loop ? cc.WrapMode.Loop : cc.WrapMode.Normal
aniState.speed = speed
aniState.time = startTime
aniState.delay = delay
return aniState
}
getRealDir(dir) {
if (dir < 5) {
return dir;
} else if (dir === 5) {
return 3;
} else if (dir === 6) {
return 2;
} else if (dir === 7) {
return 1;
} else {
return this.getRealDir(dir % 8);
}
}
getActionList() {
let actionList = []
for (let action in this.cache.config.actions) {
actionList.push(action)
}
return actionList
}
}
上面是动画控制器的代码,主要是创建动画剪辑和播放动画。
/**
* 动画资源管理器
* 功能:
* 加载动画配置文件
* 加载动画纹理
* 创建动画节点
*/
import AniCtrl from "./DemoAniCtrl"
export default class AniMgr {
// 单例
private static instance: AniMgr
private constructor() { }
static getInstance(): AniMgr {
if (!AniMgr.instance) {
AniMgr.instance = new AniMgr()
}
return this.instance
}
// 加载队列 { 路径: 回调函数列表 }
loadingQueue: { [path: string]: Function[] } = {}
// 动画缓存
aniResCache = {}
// type 取值 body weapon wing effect,动画资源按类型分目录,即目录名
loadAniNode(type, name, cb) {
let path = type + "/" + name + "/" + name
this.loadAniRes(path, (cache)=>{
if (!cb) {
return;
}
if (!cache) {
cb(null)
return
}
let node = new cc.Node()
node.name = type + "_" + name
let sprite = node.addComponent(cc.Sprite)
sprite.sizeMode = cc.Sprite.SizeMode.RAW
sprite.trim = false
node.addComponent(cc.Animation)
let ctrl = node.addComponent(AniCtrl)
ctrl.init(type, name, cache)
cb(node)
})
}
loadAniRes(path, cb) {
// 保存回调
if (!this.loadingQueue[path]) {
this.loadingQueue[path] = []
}
this.loadingQueue[path].push(cb)
let cache = this.aniResCache[path]
if(cache) {
if (cache.config) {
// 已有加载完成纹理
if (cache.index > cache.total) {
this.loadTexures(path)
} else {
// 正在加载纹理
}
} else {
// 正在加载配置文件
}
return
}
// 正在加载
this.aniResCache[path] = {};
this.loadConfig(path, (config) => {
if (!config) {
// 提前调用加载完成
this.onLoadAniRes(path)
return
}
// 新加载
this.aniResCache[path] = {
"config": config, // 配置文件
"total": config["cnt"], // 图集数量
"index": 0, // 已加载下标
"texList": [], // 动画纹理对象
}
this.loadTexures(path);
})
}
onLoadAniRes(path: string) {
let cbList = this.loadingQueue[path]
let cache = this.aniResCache[path]
if (cbList) {
for (let i = 0; i < cbList.length; ++i) {
cbList[i](cache)
}
}
delete this.loadingQueue[path]
}
loadConfig(path: string, cb) {
let url = "demo/" + path;
cc.resources.load(url, cc.JsonAsset, (error: Error, jsonAsset: cc.JsonAsset) => {
if (error) {
console.error(error.message || error);
cb(null);
return;
}
cb(jsonAsset.json);
});
}
loadTexures(path) {
// 纹理全部加载完成
let cache = this.aniResCache[path]
if (cache.index > cache.total) {
this.onLoadAniRes(path)
return
}
let url = "demo/" + path + "-" + cache.index
cc.resources.load(url, cc.Texture2D, (error: Error, texure: cc.Texture2D) => {
if (error) {
console.error(error.message || error)
}
texure.name = url;
cache.texList.push(texure)
cache.index += 1
this.loadTexures(path)
})
}
}
上面是动画资源管理器的代码,主要是负责动画资源的加载。一般游戏厉害可能由声音资源管理器,图片资源管理器等等。
下面是场景测试代码:
ndActor: cc.Node = null; // 角色节点
ctrlBody: AniCtrl = null; // 角色身体
ctrlWeapon: AniCtrl = null; // 角色武器
ctrlWing: AniCtrl = null; // 角色翅膀
dir: number = 4; // 方向
actions: string[] = [];// 动作列表
actionIndex: number = 0;// 动作下标
configMap: object = {}; // 配置文件
onLoad() {
this.ndActor = this.createActorNode();
this.createBody((node) => {
node.parent = this.ndActor;
this.ctrlBody = node.getComponent(AniCtrl);
this.onPartLoadFinish();
});
this.createWeapon((node) => {
node.parent = this.ndActor;
this.ctrlWeapon = node.getComponent(AniCtrl);
this.onPartLoadFinish();
});
this.createWing((node) => {
node.parent = this.ndActor;
this.ctrlWing = node.getComponent(AniCtrl);
this.onPartLoadFinish();
});
}
createActorNode() {
// 创建角色节点
let node = new cc.Node();
node.parent = this.node;
return node;
}
onPartLoadFinish() {
if (this.ctrlBody && this.ctrlWeapon && this.ctrlWing) {
this.actions = this.ctrlBody.getActionList()
let actionName = "idle"
this.actionIndex = this.actions.indexOf(actionName);
this.playAnimation(actionName, this.dir, cc.WrapMode.Loop);
}
}
createBody(cb) {
let part = "body";
let no = "100354";
AniMgr.getInstance().loadAniNode(part, no, cb);
}
createWeapon(cb) {
let part = "weapon";
let no = "100354";
AniMgr.getInstance().loadAniNode(part, no, cb);
}
createWing(cb) {
let part = "wing";
let no = "100307";
AniMgr.getInstance().loadAniNode(part, no, cb);
}
playAnimation(actionName, dir, loop) {
if (dir === 0 || dir === 1 || dir === 7) {
this.ctrlBody.node.zIndex = 1;
this.ctrlWeapon.node.zIndex = 2;
this.ctrlWing.node.zIndex = 3;
} else {
this.ctrlBody.node.zIndex = 2;
this.ctrlWeapon.node.zIndex = 3;
this.ctrlWing.node.zIndex = 1;
}
this.ctrlBody.play(actionName, dir, 0, loop)
this.ctrlWeapon.play(actionName, dir, 0, loop)
this.ctrlWing.play(actionName, dir, 0, loop)
}
游戏中会动态创建和删除多个角色,为了避免创建节点和释放节点的开销,使用对象池,缓存回收的节点。对象池的官方文档。抽离出一个ActorMgr,来负责这两个功能。
角色3部分动画节点组成,单可能不会全部动画节点都存在,比如npc可能没有武器和翅膀,需要抽离一个角色组件,管理这3部分动画控制器,对外提供一个动画播放节点。
为了方便演示,在界面上添加一个按钮,点击添加和删除角色。
/**
* 角色管理器
* 功能:
* 角色增删
* 内存池
*/
import AniMgr from "./DemoAniMgr"
import Actor from "./DemoActor"
export default class ActorMgr {
private static instance: ActorMgr
private constructor() { }
static getInstance(): ActorMgr {
if (!ActorMgr.instance) {
ActorMgr.instance = new ActorMgr()
}
return this.instance
}
loadingQueue = {} // 角色加载队列
// 节点池
poolMap: { [part: string]: { [no: string]: cc.NodePool } } = {}
releaseActor(ndActor) {
let actor = ndActor.getComponent(Actor)
if (actor.bodyCtrl) {
this.releasePartNode("body", actor.bodyCtrl.name, actor.bodyCtrl.node);
}
if (actor.weaponCtrl) {
this.releasePartNode("weapon", actor.weaponCtrl.name, actor.weaponCtrl.node);
}
if (actor.wingCtrl) {
this.releasePartNode("wing", actor.wingCtrl.name, actor.wingCtrl.node);
}
}
releasePartNode(part, no, node) {
node.stopAllActions();
node.getComponent(cc.Animation).stop();
let pool = this.poolMap[part][no];
pool.put(node);
}
createActor(body, weapon, wing, cb) {
let ndActor = new cc.Node()
let actor = ndActor.addComponent(Actor)
this.loadingQueue[ndActor.uuid] = {
body: body,
weapon: weapon,
wing: wing,
cb: cb,
}
this.createPartNode("body", body, (node) => {
if (node) {
node.parent = ndActor
actor.onLoadBody(node)
this.onPartLoadFinish(ndActor)
}
});
this.createPartNode("weapon", weapon, (node) => {
if (node) {
node.parent = ndActor
actor.onLoadWeapon(node)
this.onPartLoadFinish(ndActor)
}
});
this.createPartNode("wing", wing, (node) => {
if (node) {
node.parent = ndActor
actor.onLoadWing(node)
this.onPartLoadFinish(ndActor)
}
});
}
onPartLoadFinish(ndActor) {
let actor = ndActor.getComponent(Actor)
let args = this.loadingQueue[ndActor.uuid]
if (args.body && !actor.bodyCtrl) {
return
}
if (args.weapon && !actor.weaponCtrl) {
return
}
if (args.wing && !actor.wingCtrl) {
return
}
args.cb(ndActor)
delete this.loadingQueue[ndActor.uuid]
}
createPartNode(part, no, cb) {
if (!no) {
cb(null)
return
}
if (!this.poolMap[part]) {
this.poolMap[part] = {}
}
if (!this.poolMap[part][no]) {
this.poolMap[part][no] = new cc.NodePool()
}
let pool = this.poolMap[part][no];
if (pool.size() > 0) {
let node = pool.get();
cb(node);
} else {
AniMgr.getInstance().loadAniNode(part, no, cb);
}
}
}
角色管理器主要用于角色的增删,创建角色时有限从对象池中获取,释放角色时将动画结点放回对象池。
/**
* 角色组件
*/
import AniCtrl from "./DemoAniCtrl"
import DemoActorMgr from "./DemoActorMgr"
const { ccclass, property } = cc._decorator;
@ccclass
export default class Actor extends cc.Component {
public bodyCtrl: AniCtrl = null;
public weaponCtrl: AniCtrl = null;
public wingCtrl: AniCtrl = null;
onLoadBody(node: cc.Node) {
this.bodyCtrl = node.getComponent(AniCtrl)
}
onLoadWeapon(node: cc.Node) {
this.weaponCtrl = node.getComponent(AniCtrl)
}
onLoadWing(node: cc.Node) {
this.wingCtrl = node.getComponent(AniCtrl)
}
getActions() {
return this.bodyCtrl.getActionList();
}
playAnimation(actionName, dir, loop) {
if (dir === 0 || dir === 1 || dir === 7) {
this.bodyCtrl.node.zIndex = 1;
if (this.weaponCtrl) {
this.weaponCtrl.node.zIndex = 2;
}
if (this.wingCtrl) {
this.wingCtrl.node.zIndex = 3;
}
} else {
this.bodyCtrl.node.zIndex = 2;
if (this.weaponCtrl) {
this.weaponCtrl.node.zIndex = 3;
}
if (this.wingCtrl) {
this.wingCtrl.node.zIndex = 1;
}
}
this.bodyCtrl.play(actionName, dir, 0, loop)
if (this.weaponCtrl) {
this.weaponCtrl.play(actionName, dir, 0, loop)
}
if (this.wingCtrl) {
this.wingCtrl.play(actionName, dir, 0, loop)
}
}
releaseActor() {
DemoActorMgr.getInstance().releaseActor(this.node)
this.destroy()
}
}
角色组件主要是管理3个动画控制器,提供统一的动画播放接口。
都封装好之后,测试代码就非常简洁了:
actors: Actor[] = [] // 角色节点
dir: number = 4 // 方向
actions: string[] = [] // 动作列表
actionIndex: number = 0 // 动作下标
configMap: object = {} // 配置文件
// 角色参数
actorArgs = [
{
body: "100354",
weapon: "100354",
wing: "100307",
xPos: -200,
},
{
body: "100354",
weapon: "100354",
wing: null,
xPos: 0,
},
{
body: "100354",
weapon: null,
wing: null,
xPos: 200,
},
];
actorIndex = 0
onLoad() {
this.createActor(this.actorIndex)
}
createActor(index) {
let actor = this.actorArgs[index];
DemoActorMgr.getInstance().createActor(actor.body, actor.weapon, actor.wing, (node) => {
node.x = actor.xPos;
node.parent = this.node;
this.actors[index] = node.getComponent(Actor);
this.onLoadActor();
});
}
releaseActor(index) {
let actor = this.actors[index];
actor.releaseActor();
}
getOpActor() {
if (this.actorIndex < 3) {
return this.actors[this.actorIndex]
} else if (this.actorIndex === 3) {
return this.actors[1]
} else if (this.actorIndex === 4) {
return this.actors[0]
}
}
onLoadActor() {
this.actions = this.getOpActor().getActions();
let actionName = "idle";
this.actionIndex = this.actions.indexOf(actionName);
this.dir = 4;
this.getOpActor().playAnimation(actionName, this.dir, cc.WrapMode.Loop);
}
onClickActor() {
this.actorIndex += 1
if (this.actorIndex === 5) {
this.actorIndex = 1
}
if (this.actorIndex < 3) {
this.createActor(this.actorIndex)
} else if (this.actorIndex === 3) {
this.releaseActor(2)
} else if (this.actorIndex === 4) {
this.releaseActor(1)
}
}
onClickDir() {
this.dir = (this.dir + 1) % 8;
this.getOpActor().playAnimation(this.actions[this.actionIndex], this.dir, true);
}
onClickAction() {
this.actionIndex = (this.actionIndex + 1) % this.actions.length;
this.getOpActor().playAnimation(this.actions[this.actionIndex], this.dir, true);
}
}
代码中最多创建三个角色,第一个角色有用所有部分动画,第二个角色无翅膀,第三个角色无武器和翅膀。
这几个类的关系图如下:
这篇博客主要做了两个优化,合并plist和对象池。
可以看到,从帧动画的入门,到真实可商用的项目,需要经过很多步的优化,大部分都时针对资源的优化,核心点:打图集,去plist,加内存池,后续打包时,要根据不同平台,进行纹理压缩。
参考:
基于序列帧的2D角色动画
传奇cocos 基础框架
2D MMO中角色动画的优化总结
最后,我是寒风,欢迎加入Q群(830756115)讨论。