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