游戏角色动画:从入门到商用(二)

接上一节:游戏角色动画:从入门到商用(一)

七 合并多图集的plist文件

一般游戏做到上面一步就可以了,如果要进一步的优化,会发现每个图集都会产生 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) 在左上角,其中保存了每张图片的:

  • frame:当前帧在图集上的矩形区域,{{坐标},{宽高}}
  • offset:偏移
  • rotated:是否旋转
  • sourceColorRect:当前帧在原始图片上的矩形区域,{{坐标},{宽高}}
  • sourceSize:原始尺寸

再看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 文件,生成上一个上述格式的配置文件。导出来的配置文件内容如下:
游戏角色动画:从入门到商用(二)_第1张图片

将所有 plist 合并成一个配置文件之后,角色动画的创建步骤:

  1. 加载配置文件
  2. 加载动画纹理
  3. 创建精灵帧
  4. 创建动画剪辑
  5. 播放动画
    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;
    }

代码中,分别创建身体,武器,翅膀的动画,都加载完成之后,再播放。效果如图:
游戏角色动画:从入门到商用(二)_第2张图片

九 抽离动画管理器和控制器

动画资源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);
    }
}

代码中最多创建三个角色,第一个角色有用所有部分动画,第二个角色无翅膀,第三个角色无武器和翅膀。
游戏角色动画:从入门到商用(二)_第3张图片
这几个类的关系图如下:
游戏角色动画:从入门到商用(二)_第4张图片

这篇博客主要做了两个优化,合并plist和对象池。

总结

可以看到,从帧动画的入门,到真实可商用的项目,需要经过很多步的优化,大部分都时针对资源的优化,核心点:打图集,去plist,加内存池,后续打包时,要根据不同平台,进行纹理压缩。

参考:
基于序列帧的2D角色动画
传奇cocos 基础框架
2D MMO中角色动画的优化总结


最后,我是寒风,欢迎加入Q群(830756115)讨论。
qq

你可能感兴趣的:(游戏开发)