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