Cocos Creater 動畫animator狀態機工具及runtime

0x01 介紹

用Unity習慣了他提供了動畫狀態機animator,由於cocos creator本身沒有提供動畫狀態機工具,所以在項目製作過程中很難受,spine人物在動作多了以後手寫狀態機很要命。於是決定動手搞一個,之前看到網上有人寫過類似的工具,用Adobe air做的工具,恰好早年做過as3,於是拿到源碼修改一通,然後cocos creator實現解析runtime。

0x02 編輯器介紹

這裏寫圖片描述

打開任意一個文件spine導出的動畫json,龍骨json,或者cocos creator的anim文件都可以自動生成對應狀態

這裏寫圖片描述

在任意一個狀態上右鍵菜單可以打開一個彈窗,選擇make transition來建立調轉。
每個跳轉可以單獨編輯跳轉條件

0x03 Runtime 實現

圖中是一個spine的狀態腳本
這裏寫圖片描述

Animator For Spine組件的實現

import { AnimationController, AnimationPlayer, AnimatorStateLogicEvent} from "./AnimatorController";
import { AnimatorParams } from "./AnimatorParams";

const {ccclass, property,requireComponent} = cc._decorator;

@ccclass
@requireComponent(sp.Skeleton)
export default class Animator extends cc.Component implements AnimationPlayer, AnimatorStateLogicEvent {


    @property(
        {
            url:cc.RawAsset,
            displayName: '狀態機文件',
            tooltip: "文件名 *_anim.json",
        }
    )
    assetRawUrl:string;


    @property(
        {
            type: cc.Boolean,
            displayName: '自身觸發更新',
            tooltip: "是否由自身觸發狀態更新"
        }
    )
    autoUpdate:boolean = true;

    stateLogicEventDel:AnimatorStateLogicEvent = null;

    spine:sp.Skeleton = null;

    listeners:Array<Function>;

    onLoad():void{
        this.listeners = new Array<Function>();
        this.spine = this.getComponent(sp.Skeleton);
        if(this.assetRawUrl!=null){
            this.setUrl( this.assetRawUrl);// this.url.toString());
        }
        this.spine.setCompleteListener(this.spineAniStateEvent.bind(this));
        this.spine.setEventListener(this.spineAniEvent.bind(this));
    }

    private spineAniStateEvent(obj,trackIndex,type,event,loopCount):void
    {
        //cc.log("spineAniStateEvent");
        this.animatorController.onAnimationEvent();
    } 
    private spineAniEvent(track,event):void
    {
        // cc.log("spineAniEvent");
        for (let i = 0; i < this.listeners.length; i++) {

            this.listeners[i](track,event);
        }
    } 

    public Trigger(eventName:string):boolean{
        return this.animatorController.onTriggerEvent(eventName);
    }

    public addEventListener(cb:Function):void{
        this.listeners.push(cb);
    }


    public get curStateName():string{
        return this.animatorController.curStateName;
    }


    private animatorController:AnimationController;

    public setUrl(url:string){
        this.animatorController = new AnimationController(this,url);
    }


    public get Params():AnimatorParams{
        return this.animatorController.Params;
    }

    update(dt:number){
        if(this.autoUpdate)
            this.animatorController.update();
    }


    fixUpdate(dt:number){
        if(!this.autoUpdate)
            this.animatorController.update();
    }


    /****
     **** implements AnimationPlayer
    ****/

    PlayAnimation(aniName: string,loop?:boolean): void {
        loop = loop?loop:false;
        this.spine.setAnimation(0,aniName,loop);
    }
    ScaleTime(scale: number): void {
        if(scale>0)
            this.spine.timeScale = scale;
    }


    /****
     **** implements AnimatorStateLogicEvent
    ****/

    OnStateChange(fromState:string, toState:string):void {
        if (this.stateLogicEventDel) {
            this.stateLogicEventDel.OnStateChange(fromState, toState);
        }
    }

}

Animator For Cocos Animation組件的實現

import { AnimationController, AnimationPlayer } from "./AnimatorController";
import { AnimatorParams } from "./AnimatorParams";


const {ccclass, property,requireComponent} = cc._decorator;

@ccclass
@requireComponent(cc.Animation)
export default class AnimatorCocos extends cc.Component implements AnimationPlayer{


    @property(
        {
            url:cc.RawAsset,
            displayName: '狀態機文件',
            tooltip: "文件名 *_anim.json",
        }
    )
    assetRawUrl:string;


    @property(
        {
            type: cc.Boolean,
            displayName: '自身觸發更新',
            tooltip: "是否由自身觸發狀態更新"
        }
    )
    autoUpdate:boolean = true;



    animation:cc.Animation = null;

    listeners:Array<Function>;

    onLoad():void{
        this.listeners = new Array<Function>();
        this.animation = this.getComponent(cc.Animation);
        if(this.assetRawUrl!=null){
            this.setUrl(this.assetRawUrl);
        }

        this.animation.on('finished',  this.onFinished,    this);
    }

    private onFinished():void
    {
        this.animatorController.onAnimationEvent();
    } 


    public Trigger(eventName:string):boolean{
        return this.animatorController.onTriggerEvent(eventName);
    }

    public addEventListener(cb:Function):void{
        this.listeners.push(cb);
    }


    public get curStateName():string{
        return this.animatorController.curStateName;
    }


    private animatorController:AnimationController;

    public setUrl(url:string){
        this.animatorController = new AnimationController(this,url);
    }


    public get Params():AnimatorParams{
        return this.animatorController.Params;
    }

    update(dt:number){
        if(this.autoUpdate)
            this.animatorController.update();
    }


    fixUpdate(dt:number){
        if(!this.autoUpdate)
            this.animatorController.update();
    }

    private animState:cc.AnimationState;

    PlayAnimation(aniName: string,loop?:boolean): void {
        loop = loop?loop:false;

        this.animState = this.animation.play(aniName);
        this.animState.wrapMode = loop?cc.WrapMode.Loop:cc.WrapMode.Default;
    }
    ScaleTime(scale: number): void {
        if(scale>0 &&  this.animState)
            this.animState.speed = scale;
    }

}

AnimatorCondition 條件實現

import { AnimationController } from "./AnimatorController";



export class AnimatorCondition {

    public static LOGIC_EQUAL: number = 0;
    public static LOGIC_GREATER: number = 1;
    public static LOGIC_LESS: number = 2;
    public static LOGIC_NOTEQUAL: number = 3;


    public static TYPE_COMPLETE: number = 0;
    public static TYPE_BOOL: number = 1;
    public static TYPE_NUMBER: number = 2;
    public static TYPE_TRIGGER: number = 3;


    public static CHECK_ON_UPDATE: number = 1;
    public static CHECK_ON_COMPLETE: number = 2;
    public static CHECK_ON_TRIGGER: number = 3;

    ac: AnimationController;
    value: number = 0;// type == 1;0爲false,1爲true。  type ==2 爲值
    logic: number = 0;//大於 小於 等於
    id: string = "";
    type: number = 3;// 1:真假 2:數值比較 3:觸發器

    constructor(data: any, ac: AnimationController) {
        this.ac = ac;
        this.value = data.value;
        this.logic = data.logic;
        this.id = data.id;
        this.type = data.type;
    }

    public check(checkType:number,triggerName?:string): boolean {
        if (this.type == AnimatorCondition.TYPE_BOOL) {
            // cc.log("con:"+this.id +"["+this.value+"/"+this.ac.Params.getPropertyBool(this.id)+"]");
            return this.ac.Params.getPropertyBool(this.id)==(this.value==0?false:true);
        } else if (this.type == AnimatorCondition.TYPE_NUMBER) {
            let value: number = this.ac.Params.getPropertyValue(this.id);
            // cc.log("con:"+this.id +"["+this.value+"/"+value+"]");
            if (this.logic == AnimatorCondition.LOGIC_EQUAL) {
                return value == this.value;
            } else if (this.logic == AnimatorCondition.LOGIC_GREATER) {
                return value > this.value;
            } else if (this.logic == AnimatorCondition.LOGIC_LESS) {
                return value < this.value;
            } else if (this.logic == AnimatorCondition.LOGIC_NOTEQUAL) {
                return value != this.value;
            } else {
                return false;
            }
        } else if (this.type == AnimatorCondition.TYPE_COMPLETE) {
            if(checkType == AnimatorCondition.CHECK_ON_COMPLETE)
                return true;
            else
                return false;
        } else if (this.type == AnimatorCondition.TYPE_TRIGGER) {
            // if ("jumpPress" == triggerName) {
            //     cc.log("==== jumpPress this.id " + this.id);
            // }

            if(checkType == AnimatorCondition.CHECK_ON_TRIGGER)
                return this.id == triggerName;
            else
                return false;
        }
        else if (this.type == 0) {
            return false;
        }
    }
}

AnimatorParams 參數實現

export class AnimatorParams{

    private property:any = {};

    public setPropertyValue(key:string,value:number):void{
        // if(this.property[key]!=null)
            this.property[key] =value;
    }

    public setPropertyBool(key:string,value:boolean):void{
        // if(this.property[key]!=null )
            this.property[key] =value;
    }



    public getPropertyValue(key:string,defaultValue:number= 0):number{
        if(this.property[key]!=null  && typeof(this.property[key]) == "number")
            return this.property[key];
        else
            return defaultValue;
    }


    public getPropertyBool(key:string):boolean{
        if(this.property[key]!=null && typeof(this.property[key]) == "boolean")
            return this.property[key];
        else
            return false;
    }

    public Trigger(key:string){

    }
}

AnimatorState 狀態實現

import { AnimatorTransition } from "./AnimatorTransition";
import { Dictionary } from "../Base/Dictionary";
import { AnimationController } from "./AnimatorController";
import { AnimatorCondition } from "./AnimatorCondition";

export class AnimatorState{
    name:string = "";
    animation:string;
    loop:boolean = false;

    speed:number = 1;
    multi:string = "";

    transitions:Dictionary<AnimatorTransition>;
    default:boolean= false;

    ac:AnimationController;
    constructor(data:any,ac:AnimationController){
        this.name = data.state;
        this.animation = data.animation;
        if(data.default)
            this.default = true;
        this.transitions = new Dictionary<AnimatorTransition>();
        this.ac = ac;
        this.loop = data.loop;

        this.speed = (data.speed==undefined || data.speed==null)?1:data.speed;
        this.multi = data.multi?data.multi:"None";

        for (let i = 0; i < data.transition.length; i++) {
            let transition:AnimatorTransition = new AnimatorTransition(data.transition[i],ac) ;
            this.transitions.Add(transition.toStateName,transition);
        }
    }

    public update():void{
        let playspeed = this.speed;
        if(this.multi != "None"){
            playspeed = playspeed * this.ac.Params.getPropertyValue(this.multi,1);
        }
        this.ac.ScaleTime(playspeed);

        for (let i = 0; i < this.transitions.values().length; i++) {
            let transition:AnimatorTransition = this.transitions.values()[i];
            if(transition.can(AnimatorCondition.CHECK_ON_UPDATE)){
                transition.doTrans();
                return;
            }
        }
    }


    public onComplete():void{
        for (let i = 0; i < this.transitions.values().length; i++) {
            let transition:AnimatorTransition = this.transitions.values()[i];
            if(transition.can(AnimatorCondition.CHECK_ON_COMPLETE)){
                transition.doTrans();
                return;
            }
        }
    }

    public onTrigger(triggName:string):boolean{
        for (let i = 0; i < this.transitions.values().length; i++) {
            let transition:AnimatorTransition = this.transitions.values()[i];

            if(transition.can(AnimatorCondition.CHECK_ON_TRIGGER,triggName)){
                transition.doTrans();
                return true;
            }
        }
        return false;
    }
}

AnimatorTransition 跳轉實現

import { AnimatorCondition } from "./AnimatorCondition";
import { AnimationController } from "./AnimatorController";

export class AnimatorTransition{
    toStateName:string;
    conditons:Array<AnimatorCondition>;
    ac:AnimationController;
    constructor(data:any,ac:AnimationController){
        this.toStateName = data.nextState;
        this.conditons = new Array<AnimatorCondition>();
        this.ac = ac;
        for (let i = 0; i < data.condition.length; i++) {
            let condition:AnimatorCondition = new AnimatorCondition(data.condition[i],ac) ;
            this.conditons.push(condition);
        }
    }

    public can(checkType:number,triggerName?:string):boolean{
        if(this.toStateName == this.ac.curStateName){
            return false;
        }

        let cando:boolean = true;
        for (let i = 0; i < this.conditons.length; i++) {
            cando = (cando && this.conditons[i].check(checkType,triggerName));
        }
        return cando;
    }
    public doTrans():void{
        this.ac.ChangeState(this.toStateName);
    }


}

AnimationController 核心實現

import { AnimatorState } from "./AnimatorState";
import { Dictionary } from "../Base/Dictionary";
import { AnimatorParams } from "./AnimatorParams";
import Animator from "./Animator";

export interface AnimationPlayer{
    PlayAnimation(aniName:string,loop?:boolean):void;
    ScaleTime(scale:number):void;

}

//狀態機邏輯事件處理的對外接口
//!!! AnimationPlayer 與  StateLogicEvent 觸發條件很類似,後期需要優化成一個接口或者一個基類
export interface AnimatorStateLogicEvent {
    OnStateChange(fromState:string, toState:string):void;
}

export class AnimationController{
    private m_loaded:boolean = false;
    private m_stateData:any;

    private m_states:Dictionary<AnimatorState>;

    private m_curState:AnimatorState;

    private m_param:AnimatorParams;
    private m_player:AnimationPlayer;
    private m_anyState:AnimatorState;

    public get Params():AnimatorParams{
        return this.m_param;
    }

    constructor(player:AnimationPlayer,resUrl:string){
        this.m_player = player;
        this.m_states = new Dictionary<AnimatorState>();
        this.m_param = new AnimatorParams();
        cc.loader.load(resUrl, function(err,res){  
            if (err) {  
                cc.log(err);  
            }else{  
                this.m_stateData=res;
                this.m_loaded = true;
                this.onLoad(res);
            }  
        }.bind(this));  
    }

    private onLoad(res:any):void{
        if(res.state!=0){
            let defaultState:string;
            for (let i = 0; i < res.state.length; i++) {
                let state:AnimatorState = new AnimatorState(res.state[i],this);
                this.m_states.Add(state.name,state);
                if(state.default){
                    defaultState = state.name;
                }

                if(state.name == "anyState"){
                    this.m_anyState = state;
                }
            }
            this.ChangeState(defaultState);

        }
    }



    private onAnimationComplete():void{
        this.m_curState.onComplete();
        if(this.m_curState!=this.m_anyState && this.m_anyState!=null)
            this.m_anyState.onComplete();
    }

    public onAnimationEvent():void{
        this.onAnimationComplete();
    }
    public onTriggerEvent(eventName:string):boolean{
        if(this.m_curState.onTrigger(eventName)){
            return true;
        }

        if(this.m_curState!=this.m_anyState && this.m_anyState!=null){
            return this.m_anyState.onTrigger(eventName);
        }

        return false;
    }


    public PlayAnimation(aniName:string):void{
        this.m_player.PlayAnimation(aniName);
    }

    public ScaleTime(scale:number):void{
        this.m_player.ScaleTime(scale);
    }

    public ChangeState(stateName:string):void{
        // if (this.m_curState != null) {
        //     cc.log("==== m_curState " + this.m_curState.name + "  newState  " + stateName);
        // }

        if( this.m_states.ContainsKey(stateName) && (this.m_curState==null || this.m_curState.name!=stateName )){
            // if(this.m_curState!=null)
            //     cc.log("ChangeState:{"+this.m_curState.name+"===>"+stateName+"}");
            // else 
            //     cc.log("ChangeState:{ Empty ===>"+stateName+"}");
            let oldState = this.m_curState;
            this.m_curState = this.m_states[stateName];

            if(this.m_curState.animation && this.m_curState.animation!=""){
               this.m_player.PlayAnimation(this.m_curState.animation,this.m_curState.loop);
                if (this.m_player instanceof Animator) {
                    let oldStateName = "";
                    if (oldState) {
                        oldStateName = oldState.name;
                    }
                    this.m_player.OnStateChange(oldStateName, this.m_curState.name);
                }

            }

            this.m_curState.update();
            if(this.m_curState!=this.m_anyState && this.m_anyState!=null)
                this.m_anyState.update();
        }
    }
    public get curStateName():string{
        return this.m_curState.name;
    }

    public update():void{
        if(!this.m_loaded) return;
        this.m_curState.update();
        if(this.m_curState!=this.m_anyState && this.m_anyState!=null)
            this.m_anyState.update();
    }
}

其中用到的數據結構封裝Dictionary的Typescript實現

export interface IDictionary<K>{
    Add(key: string, value: K): void;
    Remove(key: string): void;
    ContainsKey(key: string): boolean;
    keys(): string[];
    values(): K[];
}
export class Dictionary<K> implements IDictionary<K>{

    _keys: Array<string> = new Array<string>();
    _values: Array<K> = new Array<K>();

    constructor(init: { key: string; value: K; }[] = null) {
        if(init==null)
            return;
        for (var x = 0; x < init.length; x++) {
                    this[init[x].key] = init[x].value;
                    this._keys.push(init[x].key);
                    this._values.push(init[x].value);
        }
    }


    private  HashCode():void{

    }


    Add(key: string, value: K) {
                this[key] = value;
                this._keys.push(key);
                this._values.push(value);
    }

    Remove(key: string) {
                var index = this._keys.indexOf(key, 0);
                this._keys.splice(index, 1);
                this._values.splice(index, 1);

                delete this[key];
    }

    keys(): string[] {
                return this._keys;
    }

    values(): K[] {
        return this._values;
    }

    ContainsKey(key: string) {
        if (typeof this[key] === "undefined") {
            return false;
         }

        return true;
    }

    toLookup(): IDictionary<K> {
        return this;
    }


    get Count(): number {
        return this._values.length;
    }
}

編輯器百度網盤地址
鏈接: https://pan.baidu.com/s/1l8lSntbhYe_mnlq0_o5bPg 密碼: 9y1s

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章