叶落为重生每片落下的叶子都是为了下一次的涅槃...^_^

逐帧动画 and 有限状态机(fsm)

【逐帧动画】

其实就canvas而言,和flash有太多相似的地方。最直接的一点:比如把一个object从a点移动到b点。并不是直接去改变object的位置,而是把a点的object擦掉,在b点重新画一个。这其实和我们儿时喜爱的动画原理一致的。电视机里播放的好看的动画,早期都是由我们的动画制作者一帧一帧画出来的。一秒30帧或者其他数。快速的转变欺骗我们的眼睛。

如今的web动画,尤其是web Game一块,因为大量复杂绚丽的原因,简单依靠底层矢量的绘制基本已经不能满足需求。更多的,或者说更实际可用的是怎么把一张张图片资源连接到一起变成动画的方法。

dom里面background 本身支持background-position,canvas就更不用说了,drawImage本身也就支持针对图片区域绘制。如此以来,一张张图片的切换就可以换成一张图片不同位置的切换。

假如我们预先设定好一个数据格式,那么动画帧的制作就会慢慢开始变得简单起来了,意味着只需要按照固定的格式填写每一帧的具体信息即可以完成一个动画过程。

/**
 * Animation
 * {Class}
 * 动画类
 */
 
Laro.register('.action', function (La) {
 
    var Class = La.Class || La.base.Class,
        extend = La.extend;
 
    /**
     * @param anim {Object} 从json配置里面获取的anim 配置
            {
                "nbrOfFrames": 73,
                "name": "TimeTrap",
                "atlas": "atlas/game/timetrap",
                "type": "animation",
                "image": "anims/timetrap.png",
                "pivoty": 128,
                "framerate": 30,
                "pivotx": 256,
                "events": []
            }
     * @param frames {Array} 从json配置文件里面获得的每帧的位置信息
     */
    var Animation = Class(function (anim, frames) {
        extend(this, anim);
 
        this.frames = frames;
        if (anim.framerate == undefined) anim.framerate = 20;
        // 这个动画执行需要的时间
        this.animationLength = frames.length / anim.framerate;
    }).methods({
        // 获取动画时间内指定时间段[from, to]中插入的事件
        getEvents: function (from, to) {
            var events = [];
            for (var e = 0; e < this.events.length; e ++) {
                var evt = this.events[e];
                if (evt.time >= from && evt.time < to) {
                    events.push(evt.name);
                }
            }
            return events;
        },
        // 获取下一个动画内(指定时间段内)插入事件的触发具体时间
        getTimeForNextEvent: function (from, to) {
            var first = -1;
            for (var e = 0; e < this.events.length; e ++) {
                var evt = this.events[e];
                if (evt.time > from && evt.time < to) {
                    if (first != -1) return first;
                    first = evt.time;
                }
            }
            return first;
        },
        // 给定两个时间区间,如果有交集,交集中的事件push两次
        getEventsSlow: function (from, to, start, end, dt) {
            var events = [],
                e,
                evt;
            for (e = 0; e < this.events.length; e++) {
                evt = this.events[e];
                if (evt.time >= from && evt.time < end) {
                    events.push(evt.name);
                }
            }
 
            for (e = 0; e < this.events.length; e ++) {
                evt = this.events[e];
                if (evt.time >= start && evt.time < to) {
                    events.push(evt.name);
                }
            }
 
            return events;
        }
    });
 
    this.Animation = Animation;
    Laro.extend(this);
         
})
/**
 * AnimationHandle
 * {Class}
 * 管理驱动动画
 */
 
Laro.register('.action', function (La) {
     
    var Class = La.Class || La.base.Class;
 
    var AnimationHandle = Class(function (animation, callback, mirrored) {
        if (animation instanceof AnimationHandle) {
            animation = animation.animation;
        }
        if (mirrored == undefined) {
            mirrored = false;
        }
 
        this.animation = animation;
        this.callback = callback == undefined ? null : callback;
        this.currentFrame = 0;
        this.time = 0;
        this.renderMirrored = mirrored;
 
        this.speed = 1;
        this.start = 0;
        this.end = 1;
 
        this.playTo = -1;
        this.loop = true;
        this.playing = false;
 
    }).methods({
        clone: function () {
            var clone = new AnimationHandle(this.animation, this.callback, this.renderMirrored);
            clone.start = this.start;
            clone.end = this.end;
            clone.time = this.time;
            return clone;
        },
        update: function (dt) {
            if (!this.playing) {
                this.currentFrame = Math.floor(this.time * this.animation.framerate) % this.animation.nbrOfFrames;
                return;
            }
 
            var oldTime = this.time;
            this.time += this.speed * dt;
 
            var halfFrame = 0.5/this.animation.framerate;
            var animationLength = this.animation.animationLength;
             
            // 循环
            if (this.loop) {
                if (this.speed > 0) {
                    this.time = this.time >= animationLength * this.end ? this.start * animationLength : this.time;
                } else {
                    this.time = this.time <= animationLength * this.start ? this.end * animationLength - halfFrame : this.time;
                }
            } else {
                var tempPlayTo;
                if (this.speed > 0) {
                    tempPlayTo = this.playTo >= 0 ? this.playTo : this.end;
                    if (this.time >= animationLength * tempPlayTo) {
                        this.time = animationLength * tempPlayTo - halfFrame;
                        this.playing = false;
                    }
                } else {
                    tempPlayTo = this.playTo >= 0 ? this.playTo : this.start;
                    if (this.time <= animationLength * tempPlayTo) {
                        this.time = animationLength * tempPlayTo + halfFrame;
                        this.playing = false;
                    }
                }
            }
 
            this.time = Math.max(this.time, 0);
            this.currentFrame = Math.floor(this.time * this.animation.framerate) % this.animation.nbrOfFrames;
 
            if (this.callback != null) {
                var timeToCheck = this.playing ? this.time : (this.speed > 0 ? animationLength * this.end : this.animation.animationLength * this.start);
                var evts;
                if (oldTime < timeToCheck) {
                    evts = this.animation.getEvents(oldTime, timeToCheck);
                } else {
                    evts = this.animation.getEventsSlow(oldTime, timeToCheck, animationLength * this.start, animationLength * this.end, dt);
                }
 
                if (evts.length >= 2) {
                    this.time = this.animation.getTimeForNextEvent(oldTime, timeToCheck);
                    evts = [evts[0]];
                }
 
                for (var e = 0; e < evts.length; e++) {
                    this.callback(evts[e], this);
                }
 
                if (!this.playing) {
                    this.callback('stopped', this);
                }
            }
        },
 
        draw : function (render, x, y, angle, alpha, tint) {
            var image = this.animation.frames[this.currentFrame];
            var baseX = this.renderMirrored ? x - (image.textureWidth - this.animation.pivotx) : x - this.animation.pivotx;
            render.drawImage(image, baseX, y - this.animation.pivoty, angle, false, alpha, tint, this.renderMirrored);
        },
        mirror: function () {
            this.renderMirrored = !this.renderMirrored;    
        },
        // play animation
        play: function (loop) {
            this.playTo = -1;
            if (loop == undefined) {
                loop = true;
            }
            if (this.time >= this.end * this.animation.animationLength - 0.5 / this.animation.framerate) {
                this.time = this.start * this.animation.animationLength;
            }
            this.loop = loop;
            this.playing = true;
        },
        // play 到指定时间点
        playToTime: function (t) {
            this.playTo = t;
            if (this.time >= this.playTo * this.animation.animationLength - 0.5 / this.animation.framerate) {
                this.time = this.start * this.animation.animationLength;
            }
            this.playing = true;
        },
        // play 到指定 event位置
        playToEvent: function (name) {
            for (var i = 0; i < this.animation.events.length; i ++) {
                var e = this.animation.events[i];
                if (e.name == name) {
                    this.playToTime(e.time / this.animation.animationLength);
                    break;
                }
            }           
        },
        // stop
        stop: function () {
            this.playing = false;    
        },
        // 返回动画初始位置
        rewind: function () {
            this.time = this.start * this.animation.animationLength;       
        },
        // 跳到指定位置
        gotoTime: function (time) {
            this.time = time * this.animation.animationLength;       
        },
        // 跳到指定事件位置
        gotoEvent: function (evt) {
            for (var i = 0; i < this.animation.events.length; i ++) {
                var e = this.animation.events[i];
                if (e.name == evt) {
                    this.time = e.time;
                    break;
                }
            }         
        },
        //跳到动画结束位置
        gotoEnd: function () {
            var halfFrame = 0.5 / this.animation.framerate;
            this.time = (this.end - halfFrame) * this.animation.animationLength;
        },
        // 指定动画播放的区间
        setRange: function (s, e) {
            this.start = s;
            this.end = e;
 
            var length = this.animation.animationLength;
            if (this.time < s * length) this.time = s * length;
            if (this.time > e * length) this.time = e * length;
        },
        // 播放速度
        setSpeed: function (s) {
            this.speed = s;      
        },
        // 获取用于播放的动画长度
        getLength: function () {
            return this.animation.animationLength * (this.end - this.start);          
        },
        // 获取当前位置
        getCurrentPosition: function () {
            return this.time;                  
        },
        // 是否停止
        isStopped: function () {
            return !this.playing;         
        },
        setCallback: function (cb) {
            this.callback = cb;         
        }
    });
 
    this.AnimationHandle = AnimationHandle;
    Laro.extend(this);
         
})

按照既定的格式填写我们每一帧动画数据,动画跑起来就变得容易。 至于resource图片的拼接,和具体坐标的计算,不妨写个脚本自动生成。那么一切就变得简单了。

animation demo 1 animation demo 2 (demo 运行于现代浏览器) 

 

【有限状态机 FSM】

事件驱动的代码组织。以状态为标志进行进程或场景间的转换。简单的说,可以看成 switch case的进阶版。以状态为单位对每个场景进行模块化,以状态顺序为时间轴辅以事件相应进行不同状态间的切换。软件技术里很古老的编码模式,却又很经典。适用于强烈依赖事件驱动的程序或者 需按照既定顺序转换的多个场景切换。比如开场动画,或者游戏。

/**
 * fsm
 * 有限状态机
 */
 
Laro.register('.game', function (La) {
         
    var Class = La.Class || La.base.Class,
        SimpleState = La.SimpleState || La.game.SimpleState,
        BaseState = La.BaseState || La.game.BaseState;
 
    var FSM = Class(function (host, states, onStateChange){
        if (host == undefined) return;
        this.host = host;
        this.onStateChange = onStateChange;
        this.stateArray = [];
 
        // states list 中,俩个元素为一组,分别是state标识{Int,标识状态顺序},和对应的state 类
        for (var i = 0; i < states.length; i += 2) {
            var stateId = states[i],
                stateClass = states[i + 1];
 
            if (stateClass instanceof SimpleState) {
                this.stateArray[stateId] = stateClass;
            } else {
                this.stateArray[stateId] = new stateClass(host, this, stateId);
            }
        }
 
        this.currentState = FSM.kNoState;
        this.numSuspended = 0;
        this.suspendedArray = [];
        this.numPreloaded = 0;
        this.preloadedArray = [];
        this.numStates = this.stateArray.length;
 
    }).methods({
        // 进入某一个state
        enter: function (startState, message) {
            this.setState(startState, message);
        },
        // 退出state,回到初始化状态
        leave: function () {
            this.setState(FSM.kNoState);
        },
        // 每帧状态机更新
        update: function (dt) {
            for (var i = 0; i < this.numSuspended; i ++) {
                this.stateArray[this.suspendedArray[i]].suspended(dt);
            }
 
            if (this.currentState != FSM.kNoState) {
                this.stateArray[this.currentState].update(dt);
                // update 之后再判断transition
                if (this.currentState != FSM.kNoState) {
                    this.stateArray[this.currentState].transition();
                }
            }
        },
        // 发出消息
        message: function (msg) {
            this.currentState != FSM.kNoState && this.stateArray[this.currentState].message(msg);   
        },
        // 消息挂起
        messageSuspended: function (msg) {
            for (var i = 0; i < this.numSuspended; i ++) {
                this.stateArray[this.suspendedArray[i]].message(msg);
            }                
        },
        // 改变状态,根据一个判断来决定是否改变状态进入下一个状态
        // 返回boolean值表示尝试改变是否成功
        tryChangeState: function (condition, toState, msg, reEnter, suspendedCurrent) {
            if (reEnter == undefined) { reEnter = true } //重进当前状态
            if (suspendedCurrent == undefined) { suspendedCurrent = true }
            if (toState == FSM.kNextState) { toState = this.currentState + 1 }
 
            if (condition
                && (toState != this.currentState || reEnter)) { console.log(toState)
                this.setState(toState, msg, suspendedCurrent);
                return true;
            }
 
            return false;
        },
        // 设置状态
        setState: function (state, msg, suspendedCurrent) {
            if (state = FSM.kNextState) {
                state = this.currentState + 1;
            }
 
            if (state == FSM.kNoState) {
                // 当前挂起的状态全部推出
                for ( ; this.numSuspended > 0; this.numSuspended --) {
                    this.stateArray[this.suspendedArray[this.numSuspended - 1]].leave();
                    this.stateArray[this.suspendedArray[this.numSuspended - 1]].isSuspended = false;
                }
                // 等待中的状态也全部终止
                for ( ; this.numPreloaded > 0; this.numPreloaded --) {
                    this.stateArray[this.preloadedArray[this.numPreloaded - 1]].cancelPreload();
                }
            } else {
                if (suspendedCurrent) { // 需要挂起当前的状态
                    this.stateArray[this.currentState].suspended();
                    this.stateArray[this.currentState].isSuspended = true;
                    this.suspendedArray[this.numSuspended ++] = this.currentState;
                } else {
                    // 推出当前状态,进入指定状态
                    if (this.currentState != FSM.kNoState) {
                        this.stateArray[this.currentState].leave();
                    }
                    // 如果指定状态并没有挂起的话,需要把所有挂起的状态退出
                    if (!this.stateArray[state].isSuspended) {
                        for ( ; this.numSuspended > 0; this.numSuspended --) {
                            this.stateArray[this.suspendedArray[this.numSuspended - 1]].leave();
                            this.stateArray[this.suspendedArray[this.numSuspended - 1]].isSuspended = false;
                        }
                    }
                }
            }
 
            // 处理等待中的状态,如果不是指定状态,都取消
            for (var p = 0; p < this.numPreloaded; p++) {
                this.preloadedArray[p] != state && this.stateArray[this.preloadedArray[p]].cancelPreload();
            }
            this.numPreloaded = 0;
            // 从当前状态到指定状态
            this.onStateChange != undefined && this.onStateChange(this.currentState, state, msg);
 
            var lastState = this.currentState;
            this.currentState = state;
 
            if (this.currentState != FSM.kNoState) {
                if (this.stateArray[this.currentState].isSuspended) {
                    // 状态转变后,如果状态是挂起的,不能是最后一个
                    this.stateArray[this.currentState].resume(msg, lastState);
                    this.stateArray[this.currentState].isSuspended = false;
                    -- this.numSuspended;
                } else {
                    // 进入指定状态
                    this.stateArray[this.currentState].enter(msg, lastState);
                }
            }
        },
        // 获取当前状态
        getCurrentState: function () {
            if (this.currentState == FSM.kNoState) return null;
            return this.stateArray[this.currentState];
        },
        preload: function (state) {
            this.preloadedArray[this.numPreloaded ++] = state;      
        },
        isSuspended: function (state) {
            return this.stateArray[state].isSuspended;          
        }
     
    }).statics({
        kNoState : -1, // 默认初始化状态码
        kNextState: -2 // 默认进入下一个状态的状态码
    });
 
    /**
     * app 的 FSM
     * 添加一些简单的交互处理
     */
    var AppFSM = FSM.extend().methods({
        draw: function (render) {
            // 如果状态类支持 draw,调用draw来绘制相关内容
            for (var i = 0; i < this.numSuspended; i ++) {
                this.stateArray[this.suspendedArray[i]].draw(render);
            }
            var s = this.getCurrentState();
            !!s && s.draw(render);
        },
        onMouse: function (x, y, left, leftPressed) {
            // 鼠标事件
            var s = this.getCurrentState();
            !!s && s.onMouse(x, y, left, leftPressed);
        }
    })
 
    this.FSM = FSM;
    this.AppFSM = AppFSM;
    Laro.extend(this);
         
})

 FSM animation demo (需现代浏览器)

=============================
这一阵事情稍微有点多,写blog的时间都快没有了

posted on   岑安  阅读(4105)  评论(5编辑  收藏  举报

编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· SQL Server 2025 AI相关能力初探
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库

导航

统计信息

点击右上角即可分享
微信分享提示