Hello World
Spiga

基于Jscex.Async的JavaScript动画/游戏

2010-12-16 01:03 by 老赵, 4473 visits

首先和大家宣布一个消息,Jscex的代码已经提交至Github上了,感兴趣的朋友下载来Dog Fooding一把,并欢迎提出反馈意见。Jscex受到F#计算表达式的启发,是一个面向JavaScript语言的monadic扩展,最常见的用途便是编写异步程序,尤其是逻辑复杂的异步程序。不过除此之外,使用这套异步库来编写动画或是游戏也是十分容易的事情。例如,一个人物的走动或是爆炸效果,其实可以视为一个贴图随时间不断变化的过程。这个变化的过程是异步的,但是有了Jscex.Async,我们只需使用最直接的同步形式编写代码就行了。

例如这里有个我用一个多小时编写的示例(需要支持canvas的浏览器,例如IE 9,FireFox,Chrome,Safari等等),类似于“是男人就撑过20秒”,其中有这么一段代码:

BulletGame.prototype._bulletFlyAsync = eval(Jscex.compile("$async", function (f) {
    var options = this._options;
    var bullet = {pos: {x: 0, y: 0}};

    this._addBullet(bullet);
    
    for (var t = 0; this._playing && this._inArea(bullet.pos); t += 20) {
        $await(this._timer.setTimeoutAsync(20));
        this._tryHit(bullet.pos); // 判断是否击中
        bullet.pos = f(t); // 改变子弹位置
    }

    this._removeBullet(bullet);
}));

这段代码是一个异步程序,表示一颗子弹的飞行过程。子弹的飞行轨迹(某一时刻的位置)由函数f来决定,因此我们只需要写一个for循环,并且在循环内部“暂停”一段时间就行了,就这么简单。在循环之前,我们将子弹添加到一个容器里,这样在一个不断循环的paint方法里就会把这颗子弹绘制在canvas上面。循环结束后,我们将子弹从容器中移出。这一切都十分顺其自然,虽然这段代码的执行过程完全是异步的。

Jscex可以使用各种方式与现有代码组合至一块儿,例如这里我为BulletGame的prototype对象扩展了一个方法,为此我晚上还改进了一下Jscex的编译逻辑,以及Jscex.Async的实现,其目的就是在_bulletFlyAsync这样的函数内部保留this对象在JavaScript语义上的正确性。JavaScript语言中this引用的动态特征是种很强大的特性,但是对于“语言改造者”来说,在一个充满回调的过程中保持语义正确并不是件十分容易的事情(当然,也不算十分困难,只要“想明白”即可)。上面的代码经过编译之后,就类似于我们直接编写了这样的代码(暂时如此,未来会有修改和优化):

BulletGame.prototype._bulletFlyAsync = function (f) {
    return $async.Start(this, function () {
        var options = this._options;
        var bullet = { pos: { x: 0, y: 0} };
        this._addBullet(bullet);
        return $async.Combine(
            $async.Delay(function () {
                var t = 0;
                return $async.Loop(
                    function () { return this._playing && this._inArea(bullet.pos); },
                    function () { t += 20; },
                    $async.Delay(function () {
                        return $async.Bind(this._timer.setTimeoutAsync(20), function () {
                            this._tryHit(bullet.pos);
                            bullet.pos = f(t);
                            return $async.Return();
                        });
                    })
                );
            }),
            $async.Delay(function () {
                this._removeBullet(bullet);
                return $async.Return();
            })
        );
    });
};

此外,JavaScript是一个很容易编写此类动画或是游戏的平台,因为它所有的代码都在UI线程上执行,因此不会有任何多线程方面的问题。例如上面的代码,我们可以放心地向容器里添加或删除“子弹”对象,试想在一个并发环境里构造一个线程安全的双向链表是件多么麻烦的事情。

最大的问题其实是浏览器环境里的性能问题,不过我对此并不怎么担心,因为V8已经给我们开了个好头,并且还在不断前进。此外对于动画和游戏来说,最大的性能问题更可能是canvas的绘图性能。现在这个简单的例子性能自然不会有太大问题,不过一旦加上贴图和素材,性能问题就会凸现出来了。对于目前的示例,我在安装Windows 7的台式机里试验了多种浏览器,帧数(fps)各有高低:

  • Firefox:110左右
  • Chrome:210左右
  • IE 9:300至800不等,比较常见的是500左右

而在我的MBP上:

  • Firefox:70左右
  • Safari:60左右
  • Chrome:190左右

此外还有同事测试了Safari最新的Nightly Build,帧数与Chrome不相上下。从中可以看出,IE 9的表现最为出色,可见它的GPU加速的确不是在吹牛的。值得一提的是,如果图像上没有子弹,那么帧数大约只有60出头,一旦同时出现了几十颗子弹,帧数变会飙升至500以上,峰值甚至可以到800。由此推测,IE 9也是在“按需”使用GPU,十分周全。

Jscex的源码及示例都在Github上,欢迎尝试。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

40 条回复

  1. amenamen
    114.251.186.*
    链接

    amenamen 2010-12-16 09:59:06

    赞一个…………

  2. infinte
    218.104.71.*
    链接

    infinte 2010-12-16 10:06:29

    第一次玩的时候我还以为是上下左右移动 shift 减速…… 东方玩多了……

  3. stephenhuang
    113.106.106.*
    链接

    stephenhuang 2010-12-16 10:23:10

    提两个问题:

    1. 示例代码好多this,不知道this在什么时候指向哪个object,好容易造成混乱。用that,或者像jQeury那样用一个jQuery的对象感觉就好多了。
    2. 函数嵌套太多,回调里面还有各种回调,看着好晕啊。
    3. $async和$await、XXXAsync(如setTimeoutAsync)跟C#那个两个有啥异同?$async.Combine、$async.Delay、$async.Bind。。。都是干什么用的?
  4. Cnhitman
    125.69.76.*
    链接

    Cnhitman 2010-12-16 10:47:38

    很好,学习了.....

  5. 老赵
    admin
    链接

    老赵 2010-12-16 10:50:50

    @stephenhuang

    没听懂,示例代码this和普通写JavaScript程序时的用法有什么区别吗?this的语义和普通JavaScript代码一模一样的,怎么会混乱……还有就是,我只是帮你搞定this的问题(因为作为一个通用类库,我得满足各种需求),如果你觉得普通JavaScript里面的this用起来不爽,那么也可以像就jQuery那样不用this。

    还有,其实你不用关心最终生成的代码,Jscex就是让人爽快地写代码……当然如果你想知道这些的话,可以从F#的计算表达式了解起……

  6. 掌握星光
    114.250.169.*
    链接

    掌握星光 2010-12-16 14:44:26

    关于示例代码我也有点疑惑。

    SoftTimer = function () { };
    

    下边有一个

    var _this = this;
    

    有一段这个代码,然后是这句

    _this._update();
    

    这个应该相当于:this._update() 吧。但下边的_update()在

    SoftTimer.prototype._update
    

    能访问到吗?汗,糊涂了

  7. biubiu
    58.210.35.*
    链接

    biubiu 2010-12-16 14:44:59

    22533ms hoho

  8. 老赵
    admin
    链接

    老赵 2010-12-16 15:00:46

    @掌握星光

    学习一下基本的JavaScript编程吧……

  9. Code之行人
    123.89.249.*
    链接

    Code之行人 2010-12-16 17:19:49

    对脚本不是很熟悉的,看起来比较费劲啊

  10. 老赵
    admin
    链接

    老赵 2010-12-16 19:56:37

    @Code之行人

    其实就是最最普通的JavaScript……

  11. apoclast
    71.6.173.*
    链接

    apoclast 2010-12-17 13:44:02

    js应该加个类似F#里头reference cell的东西

  12. 老赵
    admin
    链接

    老赵 2010-12-17 14:00:31

    @apoclast

    JavaScript里不需要这个东西啊,JavaScript天生是mutable的。

  13. 链接

    景良 2010-12-18 05:54:11

    下载来玩玩,JS异步编程我来了

  14. 风牧
    122.235.247.*
    链接

    风牧 2010-12-18 19:16:30

    老赵,刚刚在D2看到你,真的非常佩服啊。我觉得应该让你去作嘉宾才好,淘宝的技术含量没有多少。今天在D2最让我高兴的是看到谷歌工程师和你的演讲了。什么时候D2可以都有这中水平也可以在国际上占有一席之地了

  15. ONEBOYS
    58.100.80.*
    链接

    ONEBOYS 2010-12-18 21:06:17

    很给力。对于动画控制可以很简单了

  16. superman
    122.235.163.*
    链接

    superman 2010-12-18 22:58:42

    老赵,你这个是不是相当于一个控制动画的队列呢,你的源码没看懂!

  17. 老赵
    admin
    链接

    老赵 2010-12-18 23:34:01

    @superman

    和队列没有什么关系了。

    话说各位,有没有什么射击游戏的素材吗?我想写个简单地游戏案例玩玩。

  18. 老赵
    admin
    链接

    老赵 2010-12-18 23:35:07

    @风牧

    多谢支持,明年我会努力搞这个东西的,呵呵。

  19. superman
    121.0.29.*
    链接

    superman 2010-12-19 11:42:31

    看你写动画的那些代码,我觉得不比jQuery的动画功能简单啊~~jQuery队列控制动画也是非常方便的. 提个想法:可以开发一个javascript版<<愤怒的小鸟>>

  20. 老赵
    admin
    链接

    老赵 2010-12-19 14:26:43

    @superman

    怎么可能啊,我这个就是最普通基本的JavaScript,没法再简单和直接了。jQuery的话,只是提供一些现成的动画,加上队列,能做的基本就是动画的顺序执行或累加。你可以试着加上循环和判断。

    愤怒的小鸟困难之处还是在于物理引擎,相比来说对异步的要求到不高,我想搞个逻辑简单的……

  21. Mr.司马
    58.61.131.*
    链接

    Mr.司马 2010-12-20 08:52:05

    你说射击游戏,我脑海中立马出现了“魂斗罗”。

  22. 老赵
    admin
    链接

    老赵 2010-12-20 10:12:41

    @Mr.司马

    关键还是缺素材啊……

  23. 链接

    ericzhang.buaa 2010-12-20 14:19:01

    那些个飞来飞去的红色点点。。。有点恶心的说,嘿嘿

  24. xxiu
    124.127.114.*
    链接

    xxiu 2010-12-31 08:45:45

    不错啊 。。。。IE9真猛

  25. kai
    125.115.75.*
    链接

    kai 2011-01-01 23:23:12

    你好,我是一名编程初学者。自学C++ 和 PHP。感觉自学还是有点迷茫,不知道哪里是方向。希望赵老师能指点下。 —— (我热爱编程,我热爱这个职业)

  26. 助平君
    205.156.160.*
    链接

    助平君 2011-01-13 14:28:56

    哈哈,让子弹飞。这个项目,我看行。

  27. vvii
    119.84.184.*
    链接

    vvii 2011-02-05 12:19:19

    concurrent.thread提供了一套API,使得JavaScript的线程编程模型得以实现。 但是和这种事件驱动的模型的代码风格还是有一定的区别。

    始终觉得,本来是一套完整的流程,非要不断的将代码分割缩进,这样的代码看起来并不直观。

    PS: 说到魂斗罗,可以访问这个地址,他在8月实现了一个JS版的NEC模拟器,还有一个相关的教程

  28. 老赵
    admin
    链接

    老赵 2011-02-06 15:35:15

    @vvii

    所以Jscex很重要啊。

  29. soloist
    114.113.197.*
    链接

    soloist 2011-02-10 15:10:30

    对于游戏这种实时交互式应用来说,最终的自变量有两类:时间和用户输入。就子弹飞的动画控制来说,它的位置不看成一个普通的值,而看成一个在时间轴上的采样函数,把这种采样函数当成first-class object,为它们定义合适的operator,会提供更简洁、更自然、更functional的表达方式。或许你已经明白了我的意思,没错,说的就是发源自Haskell社区的Functional Reactive Programming。

  30. soloist
    114.113.197.*
    链接

    soloist 2011-02-10 15:45:06

    var mouseMove = GetMouseMove();
    var mouseDiff = mouseMove.Zip(mouseMove.Skip(1), (prev, curr) =>
        new
        {
            PrevPos = new Point(prev.EventArgs.X, prev.EventArgs.Y),
            CurrPos = new Point(curr.EventArgs.X, curr.EventArgs.Y)
        });
    
    
    var mouseDrag = from _ in GetMouseDown()
                    from diff in mouseDiff.TakeUntil(GetMouseUp())
                    select diff;
    
    mouseDrag.Subscribe(diff => DrawLine(diff.PrevPos, diff.CurrPos));
    

    看了你的这个Reactive框架的例子,对离散事件流的抽象和FRP已经非常象了。想想觉得这也再正常不过了,谁让Reactive的设计师Eric Meijer也是Haskell背后的重量级人物呢。我刚才还在想这里面支不支持FRP里的Behaviour,就是随时间变化的量,作为first-class object。想想其实也只需要增加一个IBehaviour的接口就好了,里面有一个sample(t)的方法,能获取behaviour在当前时刻的值。C#里应该是有操作符重载的吧?如果我有两个会动的圆圈,他们的位置分别用两个behaviour表示:b1,b2。要画一根线,从他们圆心连线的中点到某个固定点就可以这样写:drawLine(b1 + (b2-b1)/2, fixPoint); 因为b1、b2都是变化的,所以画出来的线也就是自动跟着变化的。这样类似的事情在Reactive框架里可以做到的吧?

  31. 老赵
    admin
    链接

    老赵 2011-02-10 16:11:46

    @soloist

    能接受FP,就用Rx。不能接受的话,估计还是得用Jscex这种命令式的了……

  32. 小林
    123.114.45.*
    链接

    小林 2011-02-14 15:04:22

    移动的效果非常棒,想知道如何做, div淡入淡出的效果。期待

  33. Jason
    70.255.231.*
    链接

    Jason 2011-02-20 07:57:29

    I, for one, find it very exciting that you are attempting to bring computation expressions to javascript. I wish I could communicate in languages other than English to interact with you more on this topic and understand your current writing on it

    Bottom line, I think this is the best approach to cracking the async problem on the browser that is out there today. A lot of work needs to be done, but the foundation is exceptionally solid from a theoretical computer science perspective

    Thanks for the interesting project

  34. 呵呵
    220.249.92.*
    链接

    呵呵 2011-03-27 17:35:28

    希望老赵有空能研究一下JS里面对FLASH多时间轴的模拟(movieclip)

  35. 老赵
    admin
    链接

    老赵 2011-03-28 11:36:34

    @呵呵

    我不懂Flash和动画的……

  36. 晨祷
    221.208.90.*
    链接

    晨祷 2011-04-22 15:21:53

    网站好慢。,。,。

  37. 老赵
    admin
    链接

    老赵 2011-04-22 18:51:03

    @晨祷

    下午在捣鼓数据库,现在好了。

  38. hxz
    222.240.187.*
    链接

    hxz 2011-10-10 10:27:48

    Disruptor您可以分析一下不?有一个.net的开源版

    http://blog.codeaholics.org/2011/disruptor-net/

  39. Jam
    58.246.144.*
    链接

    Jam 2012-02-16 09:47:19

    你能不能也帖上this._timer.setTimeoutAsync方法的函数签名,这个方法的实现? 谢谢

  40. zoroseo2020
    35.215.141.*
    链接

    zoroseo2020 2022-11-07 16:17:41

    因为只有那时才会引起聚合器的变动(这与之前元素在子集合之间移动的性质相同)。要知道分组字段改变次数相对会少很多,因此在大部分情况里我们只需要使用字段的更新前后的值即可。 幸运飞艇走势图 福彩双色球走势图 幸运时时彩走势图

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我