laya2.0 box2d系列二 碰撞体

参考
官方文档 2D物理系统概述

一、基础概念

1.刚体rigidbody :刚体是指在运动中和受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体。可参考 laya2.0 box2d系列一 基础概念和刚体

2.碰撞体collider:碰撞体是给物体加一个判定框,当碰撞框重叠的时候,两物体发生碰撞。碰撞体是检测物理碰撞的框架,他永远跟随物体的刚体移动,不会产生偏差。碰撞体有四种:矩形碰撞体,圆形碰撞体,线形碰撞体,多边形碰撞体。每个碰撞体都是继承自碰撞体基类。

  • PolygonCollider->ColliderBase->Component 2D多边形碰撞体
  • CircleCollider->ColliderBase->Component 2D圆形碰撞体
  • ChainCollider->ColliderBase->Component 2D线形碰撞体
  • BoxCollider->ColliderBase->Component 2D矩形碰撞体
  • 二、矩形碰撞体例子

    1.施加力
    laya2.0 box2d系列一 基础概念和刚体 中介绍了刚体的许多特性,如果想用示例来演示,比如放一个静态的Sprite地面(设置一下texture,然后添加内部刚体类型static),然后一个sprite矩形盒子(设置一下texture,然后添加内部刚体类型dynamic,受重力影响)从空中落下停在地面上,如果不添加碰撞体,盒子会直接穿过地面,坠出屏幕(如果先添加碰撞体,IDE会自动添加刚体)。添加矩形刚体,IDE会把宽高设定为自动与父容器相同,然后就能看到盒子稳稳地停在地面上了。

    private onKeyDown(e: Laya.Event): void {
        var rig: Laya.RigidBody = this.dropBox.getComponent(Laya.RigidBody);
        switch (e.keyCode) {
            case Laya.Keyboard.LEFT:
                // rig.setVelocity({ x: -1, y: 0 });
                //报错:this._body.applyForceToCenter is not a function
                //rig.applyForceToCenter({ x: -1, y: 0 });
                rig.applyForce(rig.body.GetWorldCenter(),{x:-1,y:0});
    

    这里setVelocity是可以的,但是applyForceToCenter报错、applyForce无效,原因不明。applyLinearImpulseToCenter也是失效的。使用原生方式var box2d:any = window['box2d'];rig.body.ApplyForce(new box2d.b2Vec2(-1,0),rig.body.GetWorldCenter());也是不行。

    2.改变重心,做出类似不倒翁的效果。这里laya没有封装SetMassData方法,需要自己写

    var box2d: any = window['box2d'];
    var rigid: Laya.RigidBody = this.dropBox.getComponent(Laya.RigidBody);
    var md: any = new box2d.b2MassData();
    rigid.body.GetMassData(md);
    //重新设置重心
    md.center.Set(1, 1.25);
    rigid.body.SetMassData(md);
    

    设置重心时,如果过低时,SetMassData会报错:0<a.I&&!this.m_flag_fixedRotationFlag&&(this.m_I=a.I-this.m_mass*box2d.b2Dot_V2_V2(a.center,a.center)这里如果a.I小于0,就会出问题。可以打开e_centerOfMassBit配置来观察重心的位置。

    switch (e.keyCode) {
        case Laya.Keyboard.LEFT:
        rig.applyLinearImpulse({ x: 50, y: 50 }, { x: -1, y: 0 });
    

    按下左箭头,就能看到不倒翁效果了。

    三、laya封装

    1.ColliderBase.as

    /**@private 获取碰撞体信息*/
    protected function getDef():* {
        if (!_def) {
            var def:* = new window.box2d.b2FixtureDef();
            def.density = density;
            def.friction = friction;
            def.isSensor = isSensor;
            def.restitution = restitution;
            def.shape = _shape;
            _def = def;
        return _def;
     * @private
     * 碰撞体参数发生变化后,刷新物理世界碰撞信息
    public function refresh():void {
        if (enabled && rigidBody) {
            var body:* = rigidBody.body;
            if (fixture) {
                //trace(fixture);
                if (fixture.GetBody() == rigidBody.body) {
                    rigidBody.body.DestroyFixture(fixture);
                fixture.Destroy();
                fixture = null;
            var def:* = getDef();
            def.filter.groupIndex = rigidBody.group;
            def.filter.categoryBits = rigidBody.category;
            def.filter.maskBits = rigidBody.mask;
            fixture = body.CreateFixture(def);
            fixture.collider = this;
    override protected function _onEnable():void {
        rigidBody || Laya.systemTimer.callLater(this, _checkRigidBody);
    private function _checkRigidBody():void {
        if (!rigidBody) {
            var comp:RigidBody = owner.getComponent(RigidBody);
            if (comp) {
                this.rigidBody = comp;
                refresh();
    

    可以看到碰撞体和刚体之间的关联,是通过owner.getComponent来获得引用的。def.shape = _shape;则用来处理形状的。

    2.BoxColider.as
    这里显示是要覆盖上面提到的_shape了。使用了_shape.SetAsBox(_width / 2 / Physics.PIXEL_RATIO, _height / 2 / Physics.PIXEL_RATIO, new window.box2d.b2Vec2((_width / 2 + _x) / Physics.PIXEL_RATIO, (_height / 2 + _y) / Physics.PIXEL_RATIO));矩形只是多边形类的特殊情况,注意SetASBox方法传入的是半宽和半高,需要原始的width或height/2

    override protected function getDef():* {
        if (!_shape) {
            _shape = _temp || (_temp = new window.box2d.b2PolygonShape());
            _setShape(false);
        this.label = (this.label || "BoxCollider");
        return super.getDef();
    

    IDE上的fitsize按钮,点一下这个按钮,碰撞体的大小就会自适应为节点宽高。

    这里测试一下在HTML的CANVAS中,SetASBox和坐标的关系:

    var fixDef = new b2FixtureDef;
    fixDef.density = 1.0;//密度
    fixDef.friction = 0.5;//摩擦
    fixDef.restitution = 0.2;//弹性
    fixDef.shape = new b2PolygonShape;//矩形
    fixDef.shape.SetAsBox(2, 2);//宽高           
    //创建一个矩形地板刚体
    var bodyDef = new b2BodyDef;                 
    bodyDef.type = b2Body.b2_staticBody;//静态的
    bodyDef.position.x = 2;    //X轴
    bodyDef.position.y = 2;    //Y轴     
    //世界中添加一个刚体
    world.CreateBody(bodyDef).CreateFixture(fixDef);
    

    现在能看到一个矩形刚体,宽高应该都是4,然后位于坐标系的左上角,这时候position的x,y都是2。可以改改position的x,y就知道这个坐标对应的是矩形的中心坐标,并不是左上角。LAYA在封装时,就把这个偏移累加上去了:_shape.SetAsBox(_width / 2 / Physics.PIXEL_RATIO, _height / 2 / Physics.PIXEL_RATIO, new window.box2d.b2Vec2((_width / 2 + _x) / Physics.PIXEL_RATIO, (_height / 2 + _y) / Physics.PIXEL_RATIO));

    3.CircleColider.as

    override protected function getDef():* {
        if (!_shape) {
            _shape = _temp || (_temp = new window.box2d.b2CircleShape());
            _setShape(false);
        this.label = (this.label || "CircleCollider");
        return super.getDef();
    private function _setShape(re:Boolean = true):void {
        _shape.m_radius = _radius / Physics.PIXEL_RATIO;
        _shape.m_p.Set((_radius + _x) / Physics.PIXEL_RATIO, (_radius + _y) / Physics.PIXEL_RATIO);
        if (re) refresh();
    

    同上面的矩形,也可以看看在HTML的CANVAS中圆心和坐标系的关系:

    var fixDef2 = new b2FixtureDef;
    fixDef2.density = 1.0;
    fixDef2.friction = 0.5;
    fixDef2.restitution = 0.2;
    fixDef2.shape = new b2CircleShape(1);   
    var bodyDef2 = new b2BodyDef;                
    bodyDef2.type = b2Body.b2_dynamicBody;
    bodyDef2.position.x = 1;    
    bodyDef2.position.y = 0;    
    world.CreateBody(bodyDef2).CreateFixture(fixDef2);
    

    圆的半径是1,如果将圆心的x设置为0,就只能看到一半的圆了。只有将x设置为1,才恰好看到整个圆。这也说明,和矩形一样,postion的坐标对应的是圆心坐标。LAYA和矩形一样,也进行了处理:_shape.m_p.Set((_radius + _x) / Physics.PIXEL_RATIO, (_radius + _y) / Physics.PIXEL_RATIO);,也就是与编辑器中的_x,_y保持了一致,以左上角为0,0点。

    4.ChainColider.as

    /**用逗号隔开的点的集合,格式:x,y,x,y ...*/
    private var _points:String = "0,0,100,0";
    /**是否是闭环,注意不要有自相交的链接形状,它可能不能正常工作*/
    private var _loop:Boolean = false;
    override protected function getDef():* {
        if (!_shape) {
            _shape = _temp || (_temp = new window.box2d.b2ChainShape());
            _setShape(false);
        this.label = (this.label || "ChainCollider");
        return super.getDef();
    private function _setShape(re:Boolean = true):void {
        var arr:Array = _points.split(",");
        var len:int = arr.length;
        if (len % 2 == 1) throw "ChainCollider points lenth must a multiplier of 2";
        var ps:Array = [];
        for (var i:int = 0, n:int = len; i < n; i += 2) {
            ps.push(new window.box2d.b2Vec2((_x + parseInt(arr[i])) / Physics.PIXEL_RATIO,
             (_y + parseInt(arr[i + 1])) / Physics.PIXEL_RATIO));
        _loop ? _shape.CreateLoop(ps, len / 2) : _shape.CreateChain(ps, len / 2);
        if (re) refresh();
    

    5.PolygonCollider.as
    2D多边形碰撞体,暂时不支持凹多边形,如果是凹多边形,先手动拆分为多个凸多边形。节点个数最多是b2_maxPolygonVertices,这数值默认是8,所以点的数量不建议超过8个,也不能小于3个。

    /**用逗号隔开的点的集合,格式:x,y,x,y ...*/
    private var _points:String = "50,0,100,100,0,100";
    override protected function getDef():* {
        if (!_shape) {
            _shape = _temp || (_temp = new window.box2d.b2PolygonShape());
            _setShape(false);
        this.label = (this.label || "PolygonCollider");
        return super.getDef();
    private function _setShape(re:Boolean = true):void {
        var arr:Array = _points.split(",");
        var len:int = arr.length;
        if (len < 6) throw "PolygonCollider points must be greater than 3";
        if (len % 2 == 1) throw "PolygonCollider points lenth must a multiplier of 2";
        var ps:Array = [];
        for (var i:int = 0, n:int = len; i < n; i += 2) {
            ps.push(new window.box2d.b2Vec2((_x + parseInt(arr[i])) / Physics.PIXEL_RATIO,
                (_y + parseInt(arr[i + 1])) / Physics.PIXEL_RATIO));
        _shape.Set(ps, len / 2);
        if (re) refresh();
    

    注意在IDE中设置时,是顺时针方向旋转的。在编辑器中,在线上单击左键增加一个点,点可以拖拽,双击点会删除这个点。这里最后一个点会自动连接到起始点,形成一个封闭的多边形。

    这里使用的Set方法是有两个限制的:

    (1)凹多边形(相邻两个边之间夹角有一个大于180度)在进行碰撞模拟时会出现不可预知的错误。有一些工具类如b2Separator类可以处理,在拉小登博客 运行时创建多边形刚体有提到。

    (2)在向vertices数组中添加顶点坐标时,要保证顶点的顺序是顺时针的。否则也会在碰撞中出现未知错误。

    关于坐标,这里在HTML的CANVAS中再做个测试:

    var fixDef3 = new b2FixtureDef;
    var shape = new b2PolygonShape();
    var arr = [];
    arr.push(new b2Vec2(5,0));
    arr.push(new b2Vec2(10,5));
    arr.push(new b2Vec2(0,5));
    shape.SetAsVector(arr,3);
    fixDef3.shape = shape;
    var bodyDef3 = new b2BodyDef;
    bodyDef3.type = b2Body.b2_staticBody;//静态的
    bodyDef3.position.x = 0;//X轴
    bodyDef3.position.y = 10;//Y轴
    world.CreateBody(bodyDef3).CreateFixture(fixDef3);
    

    这里position.x=0,可以看到三角形在最左侧。也就是position对应的是多边形的最左侧,这与LAYA中的坐标系是一致的,所以不需要再对中心点,只加上偏移的_x,_y即可:ps.push(new window.box2d.b2Vec2((_x + parseInt(arr[i])) / Physics.PIXEL_RATIO, (_y + parseInt(arr[i + 1])) / Physics.PIXEL_RATIO));

    6.SetAsOrientedBox
    拉小登博客 信手绘制线条刚体中,是可以创建未封闭的多边形:

    for each(var segment:Segment in segmentsArray) {
        var shapeBoxRequest:b2PolygonShape = new b2PolygonShape();
        shapeBoxRequest.SetAsOrientedBox(segment.length /2/ 30, 2 / 30, 
        new b2Vec2(segment.centerX/30, segment.centerY/30), segment.rotation);
        fixtureRequest.shape = shapeBoxRequest;
        body.CreateFixture(fixtureRequest);
    

    使用的SetAsOrientedBox方法,可以在拉小登博客 Box2D多边形刚体中看到使用方式,参考API可知,四个参数分别是宽、高、偏移向量、旋转角度。但是在laya封装的physics.js中没有找到SetAsOrientedBox方法

    1.密度density:number
    注意不要为了实现刚体不受重力影响,将密度设为0。系统会强制把0变成1.0。
    2.摩擦力friction
    和linearDamping有些相似,friction决定了两个刚体之间接触时所产生的摩擦力大小,而linearDamping影响的是刚体自由运动时速度的衰减,类似空气阻力。friction可以实现冰面、沙滩、玻璃等表面摩擦力不同的材质。
    3.弹性restitution
    反弹力,可以模拟橡胶、木板、钢铁等不同材质
    4.碰撞筛选group,category,mask
    box2d默认所有刚体进行碰撞模拟,filter就是将刚体分门别类,比如游戏中敌我之间进行碰撞,而敌人与敌人之间不进行碰撞检测。在源码中,会有一个shouldCollide函数来返回一个Boolean来确定是否碰撞:

    var f1:b2FilterData = fixtureA.GetFilterData();
    var f2:b2FilterData = fixtureB.GetFilterData
    if(f1.groupIndex == f2.groupIndex && f1.groupIndex != 0){
        return f1.groupIndex > 0;
    var collide:boolean = (f1.maskBits & f2.categoryBits) != 0 
    && (f1.categoryBits & f2.maskBits) != 0;
    

    参考源码,当groupIndex相等时,如果大于0,则会碰撞;小于0,则永远不会碰撞;剩余情况就看mask和category了:在laya中,group默认为0,category默认为1,即0x0001;mask默认为-1,转二进制取反加1,也就是0xFFFF;

    观察(f1.maskBits & f2.categoryBits) != 0 && (f1.categoryBits & f2.maskBits) != 0;简称前面的条件为a,后面的条件为b.因为&&的意义,想要碰撞,就要求a和b都不为0。

    设置好filter属性后,如果想清除筛选条件,需要使用新建一个b2FilterData作为属性,直接置null会报错。laya是在refresh中刷新属性的:

    var def:* = getDef();
    def.filter.groupIndex = rigidBody.group;
    def.filter.categoryBits = rigidBody.category;