Cocos 2.x 动画 DragonBones和Spine

2021-10-28  本文已影响0人  合肥黑
一、动画系统

参考动画系统

注意:Cocos Creator 自带的动画编辑器适用于制作一些不太复杂的、需要与逻辑进行联动的动画,例如 UI 动画。如果要制作复杂的特效、角色动画、嵌套动画,可以考虑改用 Spine 或者 DragonBones 进行制作。

二、DragonBones

参考
DragonBones 组件参考
example-cases 范例中的 DragonBonesGitHub | Gitee
Laya 动画系列三 骨骼动画

1.换装示例

通过替换插槽的显示对象,将下图绿色框中的武器替换为红色框中的刀。


image.png
@ccclass
export default class Helloworld extends cc.Component {

    @property(cc.Label)
    label: cc.Label = null;

    @property
    text: string = 'hello';

    @property(dragonBones.ArmatureDisplay)
    public robot: dragonBones.ArmatureDisplay = null;

    @property(dragonBones.ArmatureDisplay)
    public knife: dragonBones.ArmatureDisplay = null;

    private _rightDisplayIndex = 0;
    private _rightDisplayNames = ["weapon_1004_r", "weapon_1004d_r"];
    private _rightDisplayOffset = [{ x: 0, y: 0 }, { x: -60, y: 100 }];

    start() {
        this.knife.node.active = false;
    }

    left() {
        let armature = this.robot.armature();
        let slot = armature.getSlot("weapon_hand_l");
        slot.displayIndex = slot.displayIndex == 0 ? 4 : 0;
    }

    right() {
        this._rightDisplayIndex++;
        this._rightDisplayIndex %= this._rightDisplayNames.length;
        let armature = this.robot.armature();
        let slot = armature.getSlot("weapon_hand_r");
        const displayName = this._rightDisplayNames[this._rightDisplayIndex];
        let factory = dragonBones.CCFactory.getInstance();
        factory.replaceSlotDisplay(this.knife.getArmatureKey(), "weapon", "weapon_r", displayName, slot);

        let offset = this._rightDisplayOffset[this._rightDisplayIndex];
        slot.parent.offset.x = offset.x;
        slot.parent.offset.y = offset.y;
        armature.invalidUpdate();
    }
}
2.至2021年7月6号,最新的版本已经三年没更新了

参考https://docs.egret.com/dragonbones/cn/docs/update/update561

版本:5.6.1
发布日期:2018-8-22

三、Spine

参考
Spine 组件参考
Spine API
官方 Spine 用户指南
http://zh.esotericsoftware.com/spine-api-reference

版本参考http://zh.esotericsoftware.com/spine-changelog,至2021年7月6号最新的版本是

## 4.0.02
2 Jul 2021

Creator 中的骨骼动画资源是由 Spine 编辑器 导出的,目前支持 JSON二进制 两种数据格式。Creator2.3版本以上,支持Spine3.8以上版本。

Spine可以导出为JSON或者二进制格式。JSON可读性强、便于检查和修改但体积大;二进制可读性差,修改成本高,但文件体积小,加载速度快。

1.spine相关API

参考ULUA中SPINE动画的切换以及委托的使用方法
官方提供的连续播放SPINE动画方法

//先开始播放一个动画,然后使用AddAnimation往后面去添加动画
OBJ.AnimationState:SetAnimation(0, "animation1", false)
OBJ.AnimationState:AddAnimation(0, "animation2", false, 0)

关于SetAnimation 的三个参数,可以看下原型实现

public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) 

第二个参数是动画的名字,第三个参数是动画是否循环,很好理解。
第一个参数trackIndex是指通道序号。所谓通道Track,就是把动画分层,让角色在同一时间可以播放几个Spine动画。高层级的通道会覆盖低层级的Track:越大的通道数字就拥有越高的优先级。我觉得和PS里面的图层概念很像。

为了方便理解这个通道的概念,可以修改上面的代码。

OBJ.AnimationState:SetAnimation(0, “animation1”, false) 
OBJ.AnimationState:SetAnimation(1, “animation2”, false) 

这个时候就看不到播放的animation1了,这样写两个动画会同时播放,动画2的层级较高,会直接覆盖掉动画1。

可以用通道实现动画的叠加:比如我们有一个跑步的动画,依靠通道,我们可以实现在跑步的同时,让人能够挥手,或者扭头,用高级别的通道去覆盖低级别上的动画。

//利用不同的通道,边跑边射击
    run () {
        this.spine.setAnimation(0, 'run', true);
        this._hasStop = false;
    },
    shoot () {
        this.spine.setAnimation(1, 'shoot', false);
    },
2.官方文档的格布林换装示例

这个比dragonbones要简单一些:

    start() {
        this._skinIdx = 0;
        this.parts = ["left-arm", "left-hand", "left-shoulder"];
        this.parts = ["left-arm"];
    },

    change() {
        if (this._skinIdx == 0) {
            console.log("换女孩的蓝手");
            this._skinIdx = 1;
            for (let i = 0; i < this.parts.length; i++) {
                // 通过名称查找 slot。这里对每个 slot 的名称进行了比较。
                // 返回一个 sp.spine.Slot 对象。
                let goblin = this.goblin.findSlot(this.parts[i]);
                let goblingirl = this.goblingirl.findSlot(this.parts[i]);
                // 通过 slot 和 attachment 的名字来设置 attachment。 
                // Skeleton 优先查找它的皮肤,然后才是 Skeleton Data 中默认的皮肤。
                let attachment = goblingirl.getAttachment();
                goblin.setAttachment(attachment);
            }
        } else if (this._skinIdx == 1) {
            console.log("换成自己的手");
            this._skinIdx = 0;
            // 按名称查找皮肤,激活该皮肤。这里对每个皮肤的名称进行了比较。
            // 注意:设置皮肤不会改变 attachment 的可见性。
            // 返回一个 sp.spine.Skin 对象。
            this.goblin.setSkin('goblin');
            //设置 slot 到起始动作
            this.goblin.setSlotsToSetupPose();
        }
    }
//spine.js L6440
    var Slot = (function () {
        function Slot(data, bone) {
            this.deform = new Array();
            if (data == null)
                throw new Error("data cannot be null.");
            if (bone == null)
                throw new Error("bone cannot be null.");
            this.data = data;
            this.bone = bone;
            this.color = new spine.Color();
            this.darkColor = data.darkColor == null ? null : new spine.Color();
            this.setToSetupPose();
        }
        Slot.prototype.getSkeleton = function () {
            return this.bone.skeleton;
        };
        Slot.prototype.getAttachment = function () {
            return this.attachment;
        };
        Slot.prototype.setAttachment = function (attachment) {
            if (this.attachment == attachment)
                return;
            this.attachment = attachment;
            this.attachmentTime = this.bone.skeleton.time;
            this.deform.length = 0;
        };
        Slot.prototype.setAttachmentTime = function (time) {
            this.attachmentTime = this.bone.skeleton.time - time;
        };
        Slot.prototype.getAttachmentTime = function () {
            return this.bone.skeleton.time - this.attachmentTime;
        };
        Slot.prototype.setToSetupPose = function () {
            this.color.setFromColor(this.data.color);
            if (this.darkColor != null)
                this.darkColor.setFromColor(this.data.darkColor);
            if (this.data.attachmentName == null)
                this.attachment = null;
            else {
                this.attachment = null;
                this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName));
            }
        };
        return Slot;
    }());
    spine.Slot = Slot;
3.上面换装示例使用素材的goblins.json

参考
Spine2D-JSON数据格式

"bones": [
    { "name": "root" },
    { "name": "hip", "parent": "root", "x": 0.65, "y": 114.41, "color": "ffcf00ff" },
    {
        "name": "torso",
        "parent": "hip",
        "length": 85.83,
        "rotation": 93.93,
        "x": -6.42,
        "y": 1.98,
        "color": "ffcf00ff"
    },
    {
        "name": "neck",
        "parent": "torso",
        "length": 18.38,
        "rotation": -1.52,
        "x": 81.68,
        "y": -6.35,
        "color": "ffcf00ff"
    },
...
"slots": [
    { "name": "left-shoulder", "bone": "left-shoulder", "attachment": "left-shoulder" },
    { "name": "left-arm", "bone": "left-arm", "attachment": "left-arm" },
    { "name": "left-hand-item", "bone": "left-hand", "attachment": "spear" },
    { "name": "left-hand", "bone": "left-hand", "attachment": "left-hand" },
    { "name": "left-foot", "bone": "left-foot", "attachment": "left-foot" },
    { "name": "left-lower-leg", "bone": "left-lower-leg", "attachment": "left-lower-leg" },
    { "name": "left-upper-leg", "bone": "left-upper-leg", "attachment": "left-upper-leg" },
    { "name": "neck", "bone": "neck", "attachment": "neck" },
    { "name": "torso", "bone": "torso", "attachment": "torso" },
    { "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" },
    { "name": "right-foot", "bone": "right-foot", "attachment": "right-foot" },
    { "name": "right-lower-leg", "bone": "right-lower-leg", "attachment": "right-lower-leg" },
    { "name": "undie-straps", "bone": "pelvis", "attachment": "undie-straps" },
    { "name": "undies", "bone": "pelvis", "attachment": "undies" },
    { "name": "right-upper-leg", "bone": "right-upper-leg", "attachment": "right-upper-leg" },
    { "name": "head", "bone": "head", "attachment": "head" },
    { "name": "eyes", "bone": "head" },
    { "name": "right-shoulder", "bone": "right-shoulder", "attachment": "right-shoulder" },
    { "name": "right-arm", "bone": "right-arm", "attachment": "right-arm" },
    { "name": "right-hand-thumb", "bone": "right-hand", "attachment": "right-hand-thumb" },
    { "name": "right-hand-item", "bone": "right-hand", "attachment": "dagger" },
    { "name": "right-hand", "bone": "right-hand", "attachment": "right-hand" },
    { "name": "right-hand-item2", "bone": "right-hand", "attachment": "shield" }
],

attachment:附件名称


image.png
4.渲染模式示例SpineBatch

渲染模式,默认 REALTIME 模式。(v2.0.9 中新增)

let mode = dragonBones.ArmatureDisplay.AnimationCacheMode.SHARED_CACHE;
if (!this.isCache) mode = dragonBones.ArmatureDisplay.AnimationCacheMode.REALTIME;
this.sp0.setAnimationCacheMode(mode);
变灰:
let material = this.grayMaterial;
if (!this.isGray) {
    material = this.normalMaterial;
}
this.sp0.setMaterial(0, material);
this.sp0.markForRender(true);
Use Tint 是否开启染色效果,默认关闭。(v2.0.9 中新增)
this.sp0.useTint = this.isTint;

没搞懂染色是啥意思……

Enable Batch 是否开启动画合批,默认关闭。(v2.0.9 中新增)

开启时,能减少 Drawcall,适用于大量且简单动画同时播放的情况。
关闭时,Drawcall 会上升,但能减少 CPU 的运算负担,适用于复杂的动画。

this.sp0.enableBatch = this.isBatch;

开启合批,如果渲染大量相同纹理,且结构简单的骨骼动画,开启合批可以降低drawcall,否则请不要开启,cpu消耗会上升。

5.顶点效果示例SpineMesh

https://docs.cocos.com/creator/api/zh/classes/VertexEffectDelegate.html

    start () {
        this._swirlTime = 0;
        this._maxEffect = 3;
        this._index = 0;
        this._bound = cc.size(this.skeleton.node.width, this.skeleton.node.height);

        this._swirlEffect = new sp.VertexEffectDelegate();
        this._swirlEffect.initSwirlWithPowOut(0, 2);

        this._jitterEffect = new sp.VertexEffectDelegate();
        this._jitterEffect.initJitter(20, 20);
    },

    switchEffect () {
        this._index++;
        if (this._index >= this._maxEffect) {
            this._index = 0;
        }

        switch (this._index) {
            case 0:
                this.skeleton.setVertexEffectDelegate(null);
                break;
            case 1:
                this.skeleton.setVertexEffectDelegate(this._jitterEffect);
                break;
            case 2:
                this.skeleton.setVertexEffectDelegate(this._swirlEffect);
                break;
        }
    },

    update (dt) {
        if (this._index == 2) {
            this._swirlTime += dt;
            let percent = this._swirlTime % 2;
            if (percent > 1) percent = 1 - (percent -1 );

            let bound = this._bound;
            let swirlEffect = this._swirlEffect.getSwirlVertexEffect();
            swirlEffect.angle = 360 * percent;
            swirlEffect.centerX = bound.width * 0.5;
            swirlEffect.centerY = bound.height * 0.5;

            swirlEffect.radius = percent * Math.sqrt(bound.width * bound.width + bound.height * bound.height);
        }    
    } 
6.挂点示例SpineAttach
image.png

点击 生成挂点 按钮后,层级管理器 中 Spine 组件所在节点的下方,会以节点树的形式生成所有骨骼。

注意:Spine 挂点完成后,即可删除 层级管理器 中无用的骨骼节点,以减少运行时的计算开销。注意目标骨骼节点的父节点都不可删。

    generateSomeNodes () {
        let attachUtil = this.skeleton.attachUtil;
        let boneNodes = attachUtil.generateAttachedNodes(this.greenBoneName);
        let boneNode = boneNodes[0];
        if (boneNode) {
            let targetNode = cc.instantiate(this.targetPrefab);
            targetNode.color = cc.color(0, 255, 0);
            boneNode.addChild(targetNode);
        }
        // console.log(attachUtil._attachedNodeArray);
        // console.log(attachUtil._boneIndexToNode);
    },

    destroySomeNodes () {
        let attachUtil = this.skeleton.attachUtil;
        attachUtil.destroyAttachedNodes(this.greenBoneName);
        // console.log(attachUtil._attachedNodeArray);
        // console.log(attachUtil._boneIndexToNode);
    },

注:这里如果TS中this.skeleton.attachUtil报错,可以在其上面一行添加//@ts-ignore进行忽略。

7.碰撞检测示例SpineCollider

也是利用挂点,添加碰撞盒实现的。

关于polygonCollider,点击Editor按钮后进入编辑模式:


image.png
8.控制示例SpineBoy
spineBoy.setAnimation(0, 'walk', true);//trues时为一直循环播放
spineBoy.setMix('walk', 'jump', 0.2);//从walk转换到run动画的过度时间

关于setMix可以参考spine动画融合与动画叠加

下图为动作切换时的情况(setAnimation),左红线为切换动作时刻,两个红线之间为动作切换时间,这之间动画播放为A与B的混合,A和B所占的权重是不断变化的,从A占的权重从100到0,B占的权重从0到100(alpha值变化)。右红线之后为动作B完全播放。


image.png

事件:

this.spine.setCompleteListener(this._spineEventHandler);

    private _spineEventHandler(trackEntry: any) {
        var animationName = trackEntry.animation ? trackEntry.animation.name : "";
        console.log("_animationEventHandler", trackEntry, animationName);
        if (animationName === 'shoot') {
            // this.spine.clearTrack(1);
        }
    }
image.png
四、扩展阅读

2d手游美术实现方案分析
我所理解的打击感 逐帧分析过几十款游戏的开发者经验分享

上一篇 下一篇

猜你喜欢

热点阅读