小球移动过程中,可能会碰到食物、其他玩家和病毒,如果碰到食物,则吞食食物,质量增加;如果碰到其他玩家,体积大的吃掉体积小的,如果吞食病毒,分身解体。tickPlayer中有一段遍历所有cell的代码,它处理了游戏中的碰撞事件。
for(var z=0; z<currentPlayer.cells.length; z++) {
……
}
代码中定义了一个SAT.Circle类型的playerCircle,它指的是以currentCell.x和currentCell.y为圆心,currentCell.radius为半径的圆。后续将会用这个圆形去和场景中的物体做碰撞检测。
var V = SAT.Vector; //一开始定义
var C = SAT.Circle;
var playerCircle = new C(
new V(currentCell.x, currentCell.y),
currentCell.radius
);
吞食食物
吞食食物的代码如下所示,foodEaten表示被吃掉的食物列表,程序对food列表的所有食物执行funcFood方法,即是使用 SAT.pointInCircle看看食物是不是被包含在玩家的面积之内。然后再对每个foodEaten执行deleteFood方法,即删除掉这个食物。food.map(funcFood)表示对food数组的每个元素传递给指定的函数,并返回一个数组,该数组由函数的返回值构成。funcFood返回的是玩家是否吞食了食物,形成true/false的列表。reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的一个数组逐项处理方法。针对map(funcFood)返回的true/false列表,如果该食物被包含(为true),则将它添加到返回值中。
var foodEaten = food.map(funcFood)
.reduce( function(a, b, c) { return b ? a.concat(c) : a; }, []);
foodEaten.forEach(deleteFood);
function funcFood(f) {
return SAT.pointInCircle(new V(f.x, f.y), playerCircle);
}
function deleteFood(f) {
food[f] = {};
food.splice(f, 1);
}
看到这里作者还是比较失望的,因为本来期待有更好的方法,减少计算量。像这样两两判断谁不会啊!
吞食massFood
massFood是玩家喷射出的“质量”处理过程与吞食食物类似,获取被吃掉的mass的列表massEaten,然后从massFood列表中删掉它。
var massEaten = massFood.map(eatMass)
.reduce(function(a, b, c) {return b ? a.concat(c) : a; }, []);
……
var masaGanada = 0;
for(var m=0; m<massEaten.length; m++) {
masaGanada += massFood[massEaten[m]].masa;
massFood[massEaten[m]] = {};
massFood.splice(massEaten[m],1);
for(var n=0; n<massEaten.length; n++) {
if(massEaten[m] < massEaten[n]) {
massEaten[n]--;
}
}
}
吞食病毒
如果不小心吞食了病毒,玩家会被迫分身,代码如下所示。
var virusCollision = virus.map(funcFood)
.reduce( function(a, b, c) { return b ? a.concat(c) : a; }, []);
if(virusCollision > 0 && currentCell.mass > virus[virusCollision].mass) {
sockets[currentPlayer.id].emit('virusSplit', z);
}
下图为吞食病毒导致的分身前后,绿色圆形为病毒,大球aa吞食病毒后,立即分解为两个小球。
增加质量
如果玩家吞食了食物或massfood,小球会变大,相关代码如下。
if(typeof(currentCell.speed) == "undefined")
currentCell.speed = 6.25;
masaGanada += (foodEaten.length * c.foodMass);
currentCell.mass += masaGanada;
currentPlayer.massTotal += masaGanada;
currentCell.radius = util.massToRadius(currentCell.mass);
playerCircle.r = currentCell.radius;
吞食其他玩家
接下来是使用四叉树计算玩家之间的碰撞,笔者就在想,前面都用了那么多个for循环了,这可是每个玩家都对food,massfood,病毒都for一次啊。这里用四叉树意义很大么?为什么不一开始就都用呢?
先使用tree.put构建四叉树,四叉树可以把判断的范围变小,把每个玩家都放进去,然后通过tree.get(currentPlayer, check)获取发生碰撞的玩家。最后再对每个可能发生碰撞的玩家执行collisionCheck。
tree.clear();
users.forEach(tree.put);
var playerCollisions = [];
var otherUsers = tree.get(currentPlayer, check);
playerCollisions.forEach(collisionCheck);
接下来看看check,它遍历玩家身上每个cells,然后使用SAT.testCircleCircle测试是否圆在圆内,如果是的话返回一个response结构,该结构里面包含对方玩家的id、name、坐标等信息。然后构建playerCollisions数组。
function check(user) {
for(var i=0; i<user.cells.length; i++) {
if(user.cells[i].mass > 10 && user.id !== currentPlayer.id) {
var response = new SAT.Response();
var collided = SAT.testCircleCircle(playerCircle,
new C(new V(user.cells[i].x, user.cells[i].y), user.cells[i].radius),
response);
if (collided) {
response.aUser = currentCell;
response.bUser = {
id: user.id,
name: user.name,
x: user.cells[i].x,
y: user.cells[i].y,
num: i,
mass: user.cells[i].mass
};
playerCollisions.push(response);
}
}
}
return true;
}
然后是对发生碰撞的玩家执行逻辑,把它吃掉。
function collisionCheck(collision) {
if (collision.aUser.mass > collision.bUser.mass * 1.1 && collision.aUser.radius > Math.sqrt(Math.pow(collision.aUser.x - collision.bUser.x, 2) + Math.pow(collision.aUser.y - collision.bUser.y, 2))*1.75) {
console.log('[DEBUG] Killing user: ' + collision.bUser.id);
console.log('[DEBUG] Collision info:');
console.log(collision);
var numUser = util.findIndex(users, collision.bUser.id);
if (numUser > -1) {
if(users[numUser].cells.length > 1) {
users[numUser].massTotal -= collision.bUser.mass;
users[numUser].cells.splice(collision.bUser.num, 1);
} else {
users.splice(numUser, 1);
io.emit('playerDied', { name: collision.bUser.name });
sockets[collision.bUser.id].emit('RIP');
}
}
currentPlayer.massTotal += collision.bUser.mass;
collision.aUser.mass += collision.bUser.mass;
}
}
这里是笔者看不懂还是四叉树没啥作用呢?在这里用四叉树和直接两次循环有区别么?check是固定返回true的啊!!!!!下面的四叉树说明,可以证明这里用四叉树是无效的。
四叉树
四叉树空间索引原理及其实现 - 心如止水-GISer的成长之路 - CSDN博客
四叉树索引的基本思想是将地理空间递归划分为不同层次的树结构。它将已知范围的空间等分成四个相等的子空间,如此递归下去,直至树的层次达到一定深度或者满足某种要求后停止分割。四叉树的结构比较简单,并且当空间数据对象分布比较均匀时,具有比较高的空间数据插入和查询效率,因此四叉树是GIS中常用的空间索引之一。常规四叉树的结构如图所示,地理空间对象都存储在叶子节点上,中间节点以及根节点不存储地理空间对象。
四叉树对于区域查询,效率比较高。但如果空间对象分布不均匀,随着地理空间对象的不断插入,四叉树的层次会不断地加深,将形成一棵严重不平衡的四叉树,那么每次查询的深度将大大的增多,从而导致查询效率的急剧下降。
nodejs的 simple-quadtree介绍
代码中的tree.get、tree.put等方法用到了nodejs的simple-quadtree库,这里做个简单介绍。
simple-quadtree
simple-quadtree是一套小型的四叉树实现,每棵树支持 put、 get、remove 和 clear四种操作。四叉树的节点对象必须包含x,y坐标,以及长度宽度w、h。
Put方法
Put方法可以将节点放入四叉树里面,例如:
qt.put({x: 5, y: 5, w: 0, h: 0, string: 'test'});
Get方法
Get方法会迭代取出四叉树节点,然后调用回调函数,如下所示。
qt.get({x:0, y: 0, w: 10, h: 10}, function(obj) {
// obj == {x: 5, y: 5, w: 0, h: 0, string: 'test'}
});
如果回调函数返回true,迭代会一直进行下去,如果回调函数返回false,则迭代停止。由于源码中的check方法总是返回true,所以这里使用四叉树并没能减少计算量,相反比for循环多了构建树的计算。没什么用!