PIXI.js源码解析(7)- shape对象

pixi有很多shape对象,类似Circle,Rectangle。这些不是真正用来显示到界面上的对象,只是代表对应的数学上图形,主要作用是辅助运算,以及保存一些数据。基本上所有ui框架中都有类似的类,改一下语法就可以放到别的语言别的库里使用。我写过一个java的渲染库,用的shape就是直接把pixi的这组类改了一下语法放进去就可以直接用了。

pixi的这一组shape对象方法较少,很适合学习,对2D图形的基本操作也足够了。

不知道为什么pixi的这些对象没有继承结构,都是单独的类,每个类大致只有contains,clone,getbounds三个方法

我们看最简单的circle,它代表一个圆,其他的shape对象都是类似的。

构造函数

 function Circle() {
                    var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
                    var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
                    var radius = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;

                    _classCallCheck(this, Circle);
//省略
}

上一篇说过,前面三句是为了实现构造器重载,分别支持传入参数为0个,1个,2个,3个的情况,根据参数长度判断,如果没有传入则默认值为0。(上一篇也说过我很讨厌js这种的这种重载写法,因为js语言本身问题,ide的代码提示功能提示不出来各个重载,所以要多记一些东西,如果给我来写这个类,我就不会重载,每次都强制用户填完三个参数,这其实没什么。非要重载的话,写一个circle工厂,三个不同名的方法,用来对应三个构造器生成circle对象。我实在是不推荐直接在函数内部根据参数长度和类型来隐式的重载,这等于就是逼用户记三个重载,比每次逼用户填完三个参数还要麻烦)

然后_classcallcheck的功能,就是为了防止构造函数被错误的当做普通函数调用,上一篇已经详细的分析过了。

圆形的属性有x,y坐标和半径。有这三个属性就足够在一个2D坐标系表示一个圆了。

clone就是创建一个新对象返回,没有使用缓存池之类的策略。然后因为这些shape对象类并没有引用类型的字段,所以不用考虑深复制的问题,直接传一遍参数就搞定了,其他的shape对象的clone语句都类似。

  Circle.prototype.clone = function clone() {
                    return new Circle(this.x, this.y, this.radius);
                };

 

contains测试一个点是否在该圆的范围内。无论是游戏还是其他交互应用都需要大量的碰撞测试,例如点击事件,肯定要经过一连串的测试才能知道到底是点在了哪个对象上,即判断哪个对象发生了事件。点碰撞是相对耗费资源较少的一种,圆形的点碰撞实际代码实现就是如下,就是简单的勾股定理判断距离。

Circle.prototype.contains = function contains(x, y) {
                    if (this.radius <= 0) {
                        return false;
                    }

                    var r2 = this.radius * this.radius;
                    var dx = this.x - x;
                    var dy = this.y - y;

                    dx *= dx;
                    dy *= dy;

                    return dx + dy <= r2;
                };

比较诡异的就是函数开头判断了半径不能小于0,其实我觉得这种东西不应该写在这种,构造器里面就该判断了,然后使用object.defineproperty封装x,y属性,在set里面再来一个判断就足够了。写在这里明显会导致重复判断。

 

接下来是getBounds方法,这个方法返回一个矩形的边界框 参数分别是x,y,width,height,因为很多时候需要计算一个对象的宽度和长度,像圆形仅仅知道半径是不行的,所以还要进行转化。circle的实现也非常简单。因为circle的x,y代表的是圆形,所以要取到圆左上角的坐标需要圆形坐标减掉半径,而宽高则是半径*2,这样就返回了一个完全包围圆的bound。

 Circle.prototype.getBounds = function getBounds() {
                    return new _Rectangle2.default(this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2);
                };

接下来看一下其他shape的实现

椭圆的contains,先压缩回一个单位圆,然后再用勾股定理判断

  Ellipse.prototype.contains = function contains(x, y) {
                    if (this.width <= 0 || this.height <= 0) {
                        return false;
                    }

                    // 归一化向量
                    var normx = (x - this.x) / this.width;
                    var normy = (y - this.y) / this.height;

                    normx *= normx;
                    normy *= normy;

                    return normx + normy <= 1;
                };

getbounds和circle是一样的,xy坐标减去二分之一宽高

  Ellipse.prototype.getBounds = function getBounds() {
                    return new _Rectangle2.default(this.x - this.width, this.y - this.height, this.width, this.height);
                };

这里之所以之间减去width是因为ellipse的这个属性本来就是半宽,这在构造器的注释里有说明,但是我觉得很容易让人混淆,不知道pixi为什么要这样写。特别是到rectangle里面width意思又变成全宽而不是半宽了

  * @param {number} [width=0] - The half width of this ellipse
     * @param {number} [height=0] - The half height of this ellipse
 

RoundedRectangle代表一个圆角矩形,他的contains代码略长,但也不难,只要分解为矩形和1/4圆来分别判断就可以,如图所示

pixi的实现如下

    if (this.width <= 0 || this.height <= 0)
        {
            return false;
        }
        //外层两个if是先判断大矩形的
        //这里注意的是遇到这种嵌套if判断的情况
        //简单快速的判断要写在外面,这样就可以先过滤掉一些,然后复杂的判断写在里面的if,这样做可以提高性能
        if (x >= this.x && x <= this.x + this.width)
        {
            if (y >= this.y && y <= this.y + this.height)
            {
                //判断是否在分解出来的内部矩形
                if ((y >= this.y + this.radius && y <= this.y + this.height - this.radius)
                || (x >= this.x + this.radius && x <= this.x + this.width - this.radius))
                {
                    return true;
                }
                
                let dx = x - (this.x + this.radius);
                let dy = y - (this.y + this.radius);
                const radius2 = this.radius * this.radius;

                //接下来四个判断分别对应四个1/4圆
                if ((dx * dx) + (dy * dy) <= radius2)
                {
                    return true;
                }
                dx = x - (this.x + this.width - this.radius);
                if ((dx * dx) + (dy * dy) <= radius2)
                {
                    return true;
                }
                dy = y - (this.y + this.height - this.radius);
                if ((dx * dx) + (dy * dy) <= radius2)
                {
                    return true;
                }
                dx = x - (this.x + this.radius);
                if ((dx * dx) + (dy * dy) <= radius2)
                {
                    return true;
                }
            }
        }

        return false;

 

Polygon则代表多边形,保存了顶点组,他的contains判断就是比较经典的Ray-casting 算法,网上都能找到,不多说了。

 

想要继续深入学习的,可以看一下java一些ui库的实现,没记错的话swt,swing,javafx都分别实现了这样一组对象,除了上述的contain和getbound方法之外,还支持高级的图形裁剪,图形叠加返回新图形,直接图形碰撞(不像pixi这样只能测试点),用线来截断图形等等,其涉及的数学运算也相当复杂,可能需要一定的计算几何基础。

关于碰撞测试,因为我自己也写过类似的简单库,所以有一点经验。一般来说如果只需要知道两个图形是否碰撞,不要求很精确的话,那么只要图形支持点碰撞就完全足够了。举个例子,像游戏人物的碰撞盒,不求精确的话直接用一个长方形框住就可以了,地图不求精确的话直接用N个长方形拼装起来。碰撞时人物的碰撞盒取四个顶点,遍历组成地图碰撞盒的长方形,返回两者是否有重叠就可以了,只需要有一个长方形对象,以及他的contains方法能测试一个点是否在他内部就够了。当然,最后还要做一些算法优化。

大多数情况下这种碰撞测试完全足够,支持《冒险岛》这样的大型2D游戏也不成问题,实际上《冒险岛》还有其他一些横轴游戏的碰撞也仅仅只支持到这个程度。

但是如果要写一个物理游戏,像愤怒的小鸟,就需要更精确的碰撞测试,想知道这种复杂碰撞检测怎么实现的可以去看box2d的源码。

-------------------------

我的github

https://github.com/luckyCatMiao

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值