RSS
 

Inside Ruby: Eigenclass

03 Feb

在《Inside Ruby: Object Model》中提到,object拥有method,但是method并不存在于object中,而是在class中,这样同一个class的不同实例可以共享method。

我们知道在Ruby中,可以定义singleton method,这种method只针对某个特定的object定位,而其它的object则没有该方法。如下面的片段:

obj = Object.new
def obj.my_singleton_method
  puts 'one singleton method'
end

obj.my_singleton_method            # => "one singleton method"
Object.new.my_singleton_method     # => NoMethodError

那么对于singleton method如何用上述理论来解释呢?

my_singleton_method不能存在于obj中,因为obj不是class,它也不能存在于Object这个class中,因为如果那样的话,所有的Object实例都会有这个方法,不会抛出异常。

对class method也可以做同样的分析,因为不同的类实际是Class的不同对象,从属于某个类的class method,其它类是不会有该方法的。

实际上,对每个object(class也是一种对象),它还可以有一个特殊的隐藏的class,这就是Eigenclass(也叫Singleton class,Meta class)。

class Object
  def eigenclass
    class << self
      self
    end
  end
end

class A
  class << self
    def a_class_method; end
  end
end

obj = A.new
class << obj
  def a_singleton_method; end
end

obj.eigenclass                                         # => Class
obj.class                                              # => A
obj.eigenclass.superclass                              # => A
obj.eigenclass.instance_methods().grep(/a_sin/)        # => [:a_singleton_method]

上面的代码片段中,首先在class Object上加入一个eigenclass方法,用于返回被隐藏的eigenclass,然后给class A定义一个class method,给class A的实例obj定义一个singleton method。从运行的结果可以看出:

  • ruby的class并没有告诉我们“真相”,obj的class方法返回结果实际上应该是eigenclass而不是A。
  • obj的method存在于它的eigenclass的instance method中,这就回答了最开始提出的那个singleton method到底存在何处的问题

再看下面的片段

class B < A ; end

B.methods.grep(/a_/)                                   # => [:a_class_method]
B.superclass.eigenclass.instance_methods.grep(/a_/)    # => [:a_class_method]

这里想要说的是当向上寻找一个class method的时候,实际上是沿着eigenclass的super class来寻找的,也就是说一个类的eigenclass的superclass是它的superclass的eigenclass。

Metaprogramming Ruby》一书对这些有非常详细的解释,推荐参考。

 
 

Inside Ruby: Object Model

02 Feb

今天来重新了解一下ruby的Object Model,之所以是重新,因为是从内部来看,而不是从外部的使用上。

1. object

object = instance variables + methods(包括一个指向所属class的方法)。使用object.instance_variables 和object.methods可以查看对应的信息, 区别在于前者存在于object本身,而method存在于object的class中,这些method在class中被称作instance method,这也是为什么同一个class不同object可以共享方法,但是不能共享instance variable。

2. class

class也是一个object,是Class的实例,拥有instance methods和指向父类的方法superclass。Class是Module的子类,所以一个class也是一个module。

3. module

module与class没有根本差别,因为class本身就是module的子类,但是引入module与class的目的是不同的。通常来说,class用来实例化和继承,而module用来mix in或者作为namespace。

下面的代码片段展示了部分上述内容

class MyClass
  def my_method
    @v = 'instance variable'
  end
end

obj = MyClass.new
obj.instance_variables    # => []
obj.my_method
obj.instance_variables    # => [:@v]
obj.methods == MyClass.instance_methods    # => true
MyClass.class        # => Class
Class.superclass     # => Module
Module.superclass    # => Object
MyClass.superclass   # => Object

4. include

先看下面的代码

module A; end
module B; end

class BaseClass; end
class MyClass < BaseClass
  include A
  include B
end

MyClass.ancestors   # => [MyClass, B, A, BaseClass, Object, Kernel, BasicObject]
MyClass.superclass  # => BaseClass

在面向对象的语言中,当我们调用方法的时候,首先会在当前类中寻找,如果找不到,则会去父类中,然后是父类的父类。Ruby中提供了一个superclass方法,顾名思义是返回父类,但是Ruby并不是按照superclass的返回结果层层向上寻找方法。与Java和C#一样,Ruby不允许多继承,但是Module的引入使得它有所不同,不同在于,Ruby是按照ancestors的返回结果来寻找,这个ancestor tree中包含了class和module。

所以,Ruby查找方法的顺序为:当前类 -> include的module的逆序,-> 继承的父类。原因在于 ,Ruby中,当在一个class中include一个module时,它会创建一个匿名类,包装那个module,并且将这个匿名类插入祖先树中,仅仅在当前类之上。而superclass对此一无所知。

Metaprogramming Ruby》一书对这些有非常详细的解释,推荐参考。

 
 

Feature Toggle

29 Jan

每个迭代(两周)发布一次,所有功能必须完整可用。这对项目的计划,story的划分都提出了很高的要求。然而有的功能很难在一个迭代内完成,例如某个story在临近迭代结束的时候开始,或者某个系统的某个特性需要持续若干迭代的开发, 在整体完成之前不能出现在产品中。那么如何控制未完成的功能不出现在产品中而又不影响新的代码开发呢?这时就需要引入Feature Toggle。

在Rails中Feature Toggle可以分为三个部分:

  • 定位配置文件:比如toggle的名称,开关属性,使用环境等,下面的文件中表示有个show_user_name的feature,只在qa和staging环境中打开
    show_user_name:
      switch: on
      when: [qa,staging]
  • 实现解析配置文件的Ruby文件:这个文件最主要是在Object对象上实现一个方法,用于判断配置文件中定义的标志位的状态。
    class Object
      def show_feature? feature_name
        feature_toggle.active? feature_name
      end
    
      ...
    
    end
  • 使用:调用show_feature?方法,并传入toggle的名称,以此来控制是否执行相关代码。

从使用角度来说,并没有多大的难度,但下面两点必须要注意,否则会有很大的可能产生问题:

  • Feature Toggle的应只针对尚未完成的功能,而不是作为选择功能的控制器。
    功能完成之后,就立刻删除与该feature相关的所有控制代码。特别要摒弃的思想是保留已经完成的功能,但是把该功能关掉,以备将来使用。这完全可以从源码控制工具中轻松得到,而一旦保留在代码中,就需要增加额外的维护成本。
  • 测试。
    对使用Feature Toggle的每个功能都需要测试打开与关闭两种场景,因为两种条件下可能都会对其他功能产生影响。我们的项目中曾出现过一个功能完成之后,就一直关闭,很长时间以后在产品环境中打开,因为觉得那个功能对其他都没有影响,但是却导致了系统异常。

Feature Toggle是实现持续发布的重要手段,但当控制的条件较多时,就一定是什么地方出了问题,好用但不能滥用。

下面的文章有有更加深入的解释:http://martinfowler.com/bliki/FeatureToggle.html

 
 

软件发布实战 — 沟通

04 Nov

沟通在任何一项团队活动中都非常重要, 下面的一个例子发生在我们的一次发布中.

这次我们对数据库进行了重构, 将原来的一个大表拆分成若干小表, 那么除了做表结构的迁移,还需要做数据迁移, 因为在产品环境中已经存在了一些数据.

产品环境的数据还算比较干净, 针对这种理想状态数据的迁移, 我们使用存储过程很快实现. 但是在QA环境和Staging环境中, 由于有各种测试, 而且这些数据从几个月前很早的版本中就写入了数据库. 为了让迁移脚本更加强壮,保证产品环境万无一失, 又经过反复的修改, 终于可以随意的迁移,回滚,再迁移.

但是事情没有想象的那么顺利. 在上线的时候发现 产品的数据库不支持新建和执行存储过程. 这是我们从来没有考虑过的.一切的基础设施能力都是我们自己的感觉. 现在只有两种方案:

1. 修复产品数据库存储过程的问题. 这不是我们可控的, 而且对ops团队来说, 这件事优先级可能会非常低, 需要极大的推动. 而且这次肯定无法上线

2. 丢弃我们的产品数据. 听起来很荒谬, 但是去问了producer, 却得到了非常轻松的答案, 我们产品两个月后上线真正开始销售时会把整个遗留数据都清除一边. 所以现在可以不用保留. 这样的答案听起来轻松的一点是解决了所有的问题, 但汗颜的是我们花了几天的时间做了一个完全无用的工作, 如果在做之前询问一下是否需要保留这些数据, 那么前面的工作就会轻松许多.

所以软件发布中也需要足够的沟通, 除了BA, Dev , OPs, 还有producer.

 
No Comments

Posted in Work

 

软件发布实战 — 状态检查

16 Oct

当前项目的发布周期是两个星期,指的是总共6,7个项目一起发布。从第一天code cut,经历如下的过程:
总共三个阶段,每次发布上去以后各个项目都要进行回归测试,发现bug就需要打tag,重新经过QA环境。

如果每次环境保持比较稳定,比如发布的操作系统,部署方式都没有改变,那基于已有稳定cloud环境保证的基础上,不会打太多tag。

但是我们就经历了上面两种方式的变化。系统从Debian改为CentOS,部署方式从同一个appliction变为两个application,同一台VM, 进而变为两个application,两个VM。这一次,在很短的时间里tag的数量就飙升到20。

在如此紧密的发布,这么多的小版本中,如何快速完成下面两个检测呢?

  • 快速查看发布了正确的版本。可以点击查看当前版本中更新的内容,但显然这需要记住每个tag更改的内容,在tag较多团队太多时不实际。
  • 快速查看基础架构(比如数据库连接)运行良好。运行测试可以做到,但依然效率太低。

Live Version和Health Check可以实现这一点。

1、Live Version

也就是显示当前运行的版本。Rails中默认显示index页面,我们可以将这个页面稍作改变,以支持版本显示。

#live_version.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta name="package-git-sha" content="<%= package_git_sha %>">
    <title>Version: <%= package_version %>, <%= package_git_sha %></title>
  </head>
  <body><%= package_version %></body>
</html>

#version task
task :version do
  write_version_file("live_version.html.erb", "public/live_version.html")
  write_version_file("live_version.yml.erb", "live_version.yml")
end

#get version from jenkins
def package_version_label
  build_id = ENV['BUILD_NUMBER'] || 'dev'
  "1.#{build_id}"
end

最上面是显示版本的页面模板, 默认的index链接到这个模板。下面是rake task, 当package整个应用时将给version具体的信息。其中用到了Jenkins Parameter的属性。这样当访问应用的根目录时会显示程序的版本信息, 实现中, 如果是普通提交构建,版本会使用时间戳,如果是Tag版本构建,会显示Git tag+ Build number+ App name的格式。

2、Health Check

它的作用在于快速显示程序的基本状态,比如数据库链接是否正常。我们使用了heartbeat和status两种:

  • Heartbeat: 当一切正常返回OK。通常在发布过程中发布了一个应用后都会去检查Hearbeat是否正常。如果异常,将使用status来查看细节。
  • Status:显示每个连接的具体状态,仅在Heartbeat异常时帮助定位。
发布的过程并不需要每个分支去测试,因为基本的功能实现有各种单元测试和集成测试,所以这种状态信息的快速反馈十分必要,这是加快发布的重要一环。
 
No Comments

Posted in Work

 

软件发布实战 — 集成

16 Oct

作为一个程序员, 每天开心的新程序,让所有的测试通,最后打浏览器,输入“localhost/newapp”, 看到开发的功能展现在眼前, 那种愉悦和满足不言而喻。

但是有没有想过如何发布让其他人共享你的成果呢?

当然!使用heroku,输入下面的命令就可以轻松的帮你完成。

git push heroku master

那如果同时要发布6 ,7个项目呢?项目之间可能共用数据库,共享若干代码。情况似乎就不那么简单。

需要考虑的因素很多,其中集成无疑是非常重要的,下面两点,可见一斑。

1、 与其他项目的集成

一个很简单的例子。我们的项目的唯一入口是另一个项目中一个连接

old link: http://host/show?name=jiangpeng
new link: http://host/show

需求是把old link改成new link,以前name参数是必传的,否则就会失败,现在是可选择的。
我们对自己的代码库非常熟悉,很快将验证必选的逻辑去掉。接下来就要去修改另一个代码库,那是个陈旧的用Perl完成的代码,修改完成之后甚至不知道怎么运行测试,而且CI还是红的,没法build出package,也没法提交。
过了两天才顺利的做完,build出最新的版本,deploy到cloud上,功能完美,QA竖起大拇指说做的不错,没有bug!

等到发布的时候只要把最新的包部署到production上面。这样就可以了么?
不行!

当前对于两个工程的修改在发布时是有依赖的。如果Perl工程先发布,就必然导致在我们工程发布之前的这个时间段,我们的功能将无法访问。通常,发布会持续两三天,也就是说最坏的情况,三天之内我们的应用都不可用。 这对于要求7*24小时在线的应用来说是不可接受的。
其实这个问题是可以避免,在cloud上我们有E2E的测试,但是由于要在那个环境上做BAT,所以恰好关掉了cloud的自动部署。
虽然只是这么微小的改变都有可能导致产品无法访问的巨大后果,这就是集成考虑不全面的后果。

2、与数据库的集成
发布从大的方面可以分为数据库与代码库两个部分,数据库先于代码库发布,如果发布持续3天,通常第一天第一步就是应用所有的数据库修改,然后陆续的发布不同项目的代码。
如果只是增加新列或表,这个过程没有问题。如果是修改列名呢?
按照前面的发布顺序,数据库发布之后,代码发布之前,应用必然异常,会提示数据库列不存在。以下的几个方案可以考虑实施:

  • 不要在最开始就执行数据库的修改,可以将修改列名这一个操作放在发布对应代码之前。这种方案其实就是缩短两者之间的时间差,期望在这个时间段内没有用户访问。
  • 修改表名时采用增加一列的方式,在下一次发布时删除原来的列。这种方式需要在下一次发布时候做数据迁移,因为在第2步将新产生的数据写入旧的列中。 从实现的角度来说最为复杂。但是可以保证产品不下线并且不出异常。
  • 与Business的人商量,如果允许产品下线,那么采用步骤1即可。通常Business的人对这种要求都会比较谨慎的思考,所以提出这样的建议前,也需要想清楚可否避免。

与发布相关的集成点有的时候需要dev在开发结阶段不要仅仅局限于代码功能完成本身,而需要从发布的角度仔细想想才会避免. 当然更加科学的做法是通过足够的自动化测试来保证,纯粹靠程序员的思考,很难保证每一次都不出问题。

软件开发沉思录–ThoughtWorks文集》中的第一章:Solving the Business Software “Last Mile”,更加详细和理论的阐述了大型项目发布的问题,不论是否参与过这样的发布, 都值得学习一下。

 
2 Comments

Posted in Work

 

Express Js

04 Jul

Javascript通常运行于客户端浏览器,可以方便的操作HTML中的DOM元素,另外,提供也提供了一些事件响应机制,将表单验证等很多功能置于前台完成,降低前端与服务器端的交互次数,提高用户体验。在服务器端,有很多的框架可以选择,包括SSH,Spring MVC,ASP.Net MVC等,这些框架提供了良好的结构,可以创建强健的Web应用。

然而,前端与后端的割裂在某种程度上限制了Web开发人员的全面发展,而且我们往往需要可以快速的创建我们的应用系统,在这一点上,上面的框架显得过于笨重。如果可以让前后台可以无缝连接,那么将会大为简化系统的开发,基于Node.js的Express就是这样一种框架。

谈到Express就必须要了解Node.js,Wikipedia对Nodejs的定义是:

Node.js is an event-driven I/O server-side JavaScript (on V8 JavaScript engine) environment for Unix-like platforms. It is intended for writing scalable network programs such as web servers. It was created by Ryan Dahl in 2009, and its growth is sponsored by Joyent, which employs Dahl.

Node.js is similar in purpose to Twisted for Python, Perl Object Environment for Perl, libevent for C and EventMachine for Ruby. Unlike most JavaScript, it is not executed in a web browser, but is instead a form of server-side JavaScript. Node.js implements some CommonJS specifications. Node.js includes a REPL environment for interactive testing.

当前web的应用的现状:很多应用(如聊天室,网络游戏等)都希望在连接建立以后始终保持这个连接,随时与发送请求获取响应。传统的服务器,为每个请求单独开始新的线程,在客户并发请求数量巨大的情况下,使用传统的方式实现资源的合理配置并不容易。而Node.js使用事件驱动,通过这种方式以单线程的方式实现并发的快速响应,所有的方法都通过回掉方式进行处理,之后回到事件循环。而在实践循环的时间,可以随时接受新的事件,CPU不会有任何的消耗。

本文不是要详细介绍Node.js,而是Express框架,希望通过Route,View,Database这三个方面的实现来与传统的web框架作比较,来分享服务器端的实现的简洁与强大。

  • Route
    app.get('/user/:id', function(req, res){
        res.send('user ' + req.params.id);
    });

上面代码在当用户访问”http://server:port/user/1″这样的地址时触发,执行上面所定义的匿名函数,参数1会放入req.params.id中。这里还支持使用正则表达式匹配更多的情况。

Middleware

Express中,可以对某个请求进行顺序的若干处理,比如从数据库加载用户信息,判断用户权限,然后操作数据。这可以使用Middleware完成。Middleware与普通的回掉函数不同的地方在有另一个函数作为参数,通常叫做next()。当在某个Middleware中调用next()时,系统会自动调用下一个匹配的路由进行处理。

Route Param Pre-conditions

app.param('userId', function(req, res, next, id){
  User.get(id, function(err, user){
    if (err) return next(err);
    if (!user) return next(new Error('failed to find user'));
    req.user = user;
    next();
  });
});

app.get('/user/:userId', function(req, res){
  res.send('user ' + req.user.name);
});

上面的代码中,当请求该路由时,会先执行param,这里通常可以进行一些校验,从数据库读取数据等等任务,之后基于Middleware的功能,调用next(),转回主回掉函数。

Struts使用极为复杂的配置来实现路由功能,Spring MVC加入了annotation,在代码可读性上有了较大提高,但依然没有这里的直接。同时Middleware,特别是Route Param Pre-conditions类似于AOP,可以在真正的业务逻辑处理前进行预处理工作,使代码更清晰简洁。

  • View

Express支持Haml, Jade, EJS, CoffeeKup,jQuery等视图。

可通过app.set(‘view engine’, ‘jade’)加载并激活对应的template,Jade是Express中默认的template。下面代码实现了在render页面以及页面与后台的数据绑定(使用hmal作为view engine)

%h1= title
%form{ method: 'post' }
  %div
    %div
      %span Title :
      %input{ type: 'text', name: 'title', id: 'editArticleTitle' }
    %div
      %span Body :
      %textarea{ name: 'body', rows: 20, id: 'editArticleBody' }
    %div#editArticleSubmit
      %input{ type: 'submit', value: 'Send' }
get('/blog/new', function(){
  this.render('blog_new.html.haml', {
    locals: {
      title: 'New Post'
    }
  });
});

post('/blog/new', function(){
  var self = this;
  articleProvider.save({
    title: this.param('title'),
    body: this.param('body')
  }, function(error, docs) {
    self.redirect('/')
  });
});
  • Database

由于JSON是天然的javascript数据传递格式,可以看到在路由定义中render view使用的变量定义也是通过JSON的格式进行,所以Express自然对基于document的数据库提供了良好的支持,也就是Mongo DB。它的driver是node-mongodb-native,通过下面的代码就可以使用mongo。

var mongo= require('mongodb').Db
db= new Db('db-name', new Server(host, port, {auto_reconnect: true}, {}));
db.open(function(){});
 
 

Box2D-Post Collision Detection

01 Jun

在检测到碰撞后,我们往往需要进行一些处理,比如在Angry Birds中当小鸟撞击到障碍或者击中猪后,会有碰撞的声音,破碎的效果等,这些都是在碰撞检测后进行处理的。

如上图,坦克发出的炮弹击中了空中飞行的物体,之后炮弹消失,物体旋转下落。

实现的代码如下

var contactListener = new Box2D.Dynamics.b2ContactListener;
contactListener.BeginContact = function(contact) {
   var bullet = contact.GetFixtureB().GetBody();
   var fly = contact.GetFixtureA().GetBody()
   var fly_data = fly.GetUserData()
   if(bullet.GetUserData() == "bullet" && fly_data != null && fly_data.indexOf("fly") != -1){
       trace("bullet collision detected");
       bullet.SetUserData("dead")
       fly.SetLinearVelocity(new b2Vec2(0, 3))
       fly.SetAngularVelocity(2)
    }
 };

Box2D中Body拥有一个userData属性,可以在里面存储任何数据。当引擎检测到碰撞时,就会回掉上面的函数,此时可以使用userData中存储的数据来判断碰撞的对象。对于上面的效果,首先根据userData判断出子弹和物理,然后设置炮弹的userData为dead,以及给飞行物体设置新的线速度和角速度。这样飞行物体就会改变飞行状态,而在下一次的界面update操作中可以destroy所有dead的物体。

这里需要注意的是不能直接在回掉函数中destroy body,box2D不允许这样做,原因是body可能用于与其他物体的碰撞检测中。所以只能在回掉函数中记录需要destroy的物体并且在更新函数中销毁。

 
 

Box2D-Collision Detection

01 Jun

碰撞检测是物理引擎中非常重要的部分,一般分为两种:

  • Discrete Collision Detection: 离散碰撞检测。
    从实现的角度来说,就是在每TimeStep时刻计算所有当前物体的Contact,由于Box2D处理的都是刚体,这样如果在计算的结果中有overlap的刚体存在,那么这些物体之间必然存在碰撞关系。
  • CCD(Continuos Collision Detection): 连续碰撞检测。
    与离散检测不同,并不是只在某些时刻检测碰撞情况,而是根据物理学的相关知识,通过当前的速度,加速度,位置,方向等信息计算在每个离散采样时间间隔内的运动轨迹,以轨迹来判断是否存在碰撞。

 

既然两种方式存在,那么如何选择,他们之间有怎么样的区别呢?

Tunneling

下面的图可以用来揭示部分原因

第一张图中,假设一个物体以恒定速度从左向右运动,物理引擎分别在t1,t2,t3时刻采样,这样在t2时刻物体处于障碍物前方,而t3时刻位于障碍物右方,这就好像物理穿过了障碍物。这种现象叫做Tunneling,也是离散碰撞检测带来的问题。同样的现象如第二张图,坦克发射的炮弹落在了障碍物的中间,这是因为穿过了前面的物体,而恰好没有穿过后一个。CCD由于计算了物理的运动轨迹,它与障碍物之间就会有交叉,所以不会产生Tunneling现象。

Box2D中,static与dynamic物体之间的以及kinematic与dynamic物体之间都是使用CCD,所以这两类物体之间不会错过任何碰撞。但是dynamic物体之间默认采用离散检测,而将与CCD的控制转换通过Body的bullet属性交给设计者。当此属性为true的时候,就对该物体使用CCD。

除了使用CCD可以消除Tunneling想象外,也可以通过提高采样频率来降低它发生的概率,例如上面的途中,如果将采样频率提高一倍,就恰好可以检测到碰撞。

无论使用何种方式当检测到碰撞后,都需要计算物体发生碰撞的时刻,因为刚体不允许出现overlap,所以需要将物体恢复到发生碰撞的时刻,等待下一次界面更新,渲染碰撞效果。这个第一次碰撞的时间叫做TOI(Time of Impact)。

两种碰撞检测方式的取舍就是性能与精确度的权衡,一般来说可以从下面的角度来考虑:

  • CCD非常昂贵。相对于只有固定间隔的离散检测来说,时间变量的引入使引擎的计算工作加大,当物体较多时,影响更为明显。
  • CCD应该只用于高速运动的关键物体。比如Angry Birds中发射的小鸟,玩家绝对不能接受小鸟穿过障碍物。
  • 不是所有高速物体都要使用CCD,因为往往当物体速度非常块时,我们是希望忽略它的碰撞关系的。
  • 不要将CCD应用于在初始位置已经接触的物体。
 
 

Box2D–Physics Engine

17 Apr

Box2D是一个强大的物理引擎(Physics Engine),有c++, java, js等多种版本。当前流行的Angry birds游戏就使用它作为物理引擎。Wikipedia中给出的定义是:

physics engine is computer software that provides an approximate simulation of certain simple physical systems, such as rigid body dynamics(including collision detection), soft body dynamics, and fluid dynamics, of use in the domains of computer graphicsvideo games and film. Their main uses are in video games (typically as middleware), in which case the simulations are in real-time. The term is sometimes used more generally to describe any software system for simulating physical phenomena, such as high-performance scientific simulation.

近期为了给Tankcraft做spike,实现了一个简单的坦克,包括的功能有:

  • 左右键控制坦克前后移动
  • 上下键调整炮筒的角度
  • 空格键发射炮弹,根据炮筒角度不同炮弹运行的抛物线也不一样
  • 当运行到有坡度的地面时,坦克整体布局不发生改变

其中关键技术点包括:

Body:

在Box2D中,整个界面成为一个World,World中可以创建多个不同的Body,Body可以具有质量,摩擦力,位置,形状等等属性。例如上图中坦克的底座,操作舱,轮子,炮筒,炮弹,斜坡,四周方框都是Body。Body共有三种:

  • staticBody:不能有任何移动和变化。例如,四周方框和斜坡
  • dynamicBody: 可以任意移动变化。除过方框和斜坡外的其余部分都是这种类型
  • kinematicBody:通常用于给定初始速度后可以移动的物体

Body Joint:

多个Body之间可能毫无关系,也可能紧密结合,Body Joint就是用来处理不同Body之间关系的一个对象。共有9种Joint:

  • Distance Joint
  • Friction Joint
  • Gear Joint
  • Line Joint
  • Mouse Joint
  • Prismatic Joint
  • Pulley Joint
  • Revolute Joint
  • Weld Joint

有一篇blog对joint讲解的非常详细:http://blog.allanbishop.com/box2d-2-1a-tutorial-part-2-joints/,这里不再详述每种joint的适用场景。只简单列举用到的两种:

  • Weld Joint: 这应该是最简单最直接的一种连接,就是将两个Body绑定在某个点上。例如操作舱和坦克底部,它们彼此绑定而且没有相对移动。
    joint = new b2WeldJointDef;
    var car_body_position = car_body.GetPosition()
    joint.Initialize(car_body, car_head, new b2Vec2(car_body_position.x - 50/30, car_body_position.y));
    world.CreateJoint(joint)
  • Revolute Joint:这种方式是用来处理两个Body之间旋转的一种连接,例如车轮和底座之间,以及炮筒与操作舱之间。
    var joint = new b2RevoluteJointDef;
    var joint_point = new b2Vec2(car_head.GetWorldCenter().x + 25/30, car_head.GetWorldCenter().y)
    joint.Initialize(gun, car_head, joint_point);
    joint.enableLimit = true;
    
    joint.referenceAngle = 20 * Math.PI / 180
    gun_joint = world.CreateJoint(joint)

    这里定义了在Joint时两个Body的初始角度差。另外要特别注意的就是,定义的joint_point是car_head的最后端,而gun的Position一定要根据car_head的Position设置正确,保证gun的最左端与joint_point重合,否则无法实现合理的绕joint_point旋转的效果!

  • ApplyImpulse: 这个用来给发出的炮弹初始速度以及角度。由于炮弹必须要沿着炮筒的方向,所以需要理解这个方法的参数含义,并且考虑几个关键点坐标来计算。
    var bulletDef = new b2BodyDef;
    bulletDef.type = b2Body.b2_dynamicBody;
    fixDef.shape = new b2CircleShape(
        4/30
    );
    fixDef.friction = 4
    fixDef.density = 2
    fixDef.filter.groupIndex = 1;
    var gun_joint_position = gun_joint.GetAnchorA()
    bulletDef.position = new b2Vec2(2 * gun.GetPosition().x - gun_joint_position.x,  2 * gun.GetPosition().y -  gun_joint_position.y);
    var bullet=world.CreateBody(bulletDef);
    bullet.CreateFixture(fixDef);
    
    bullet.ApplyImpulse(new b2Vec2(gun.GetPosition().x - gun_joint_position.x, gun.GetPosition().y - gun_joint_position.y), gun_joint_position)

以下是一些有用的链接:

http://www.box2dflash.org/docs/2.1a/reference/
http://www.box2d.org/manual.html
http://blog.allanbishop.com/box2d-2-1a-tutorial-part-2-joints/
http://www.box2d.org/index.html