Engine implementation - NULL-HDU/Barrage_Frontend GitHub Wiki
By Arthury on 30th December 2016 @NULL TEAM
Introduction
Engine 控制 Socket 的解析与 View 的渲染的速度比例。
Engine 进行 bullet 碰撞检查。
Engine 接受用户的输入,包括键盘输入,鼠标左键输入以及鼠标轨迹的输入,用来控制飞机的技能发动,移动以及方向的变化。
Engine 对失效的子弹进行回收。
Implement
Rate Control
Engine 的循环作为总循环控制 Socket 的解析以及 View 的渲染
let engine = () => {
let socketCount = 0;
let socketCountMax = global.SOCKET_LOOP_INTERVAL / global.GAME_LOOP_INTERVAL;
let viewCount = 0;
let viewCountMax = Math.floor(global.VIEW_LOOP_INTERVAL / global.GAME_LOOP_INTERVAL);
looper(() => {
if (++socketCount >= socketCountMax) {
socketCount = 0;
playgroundInfo();
}
if (++viewCount >= viewCountMax) {
viewCount = 0;
loopRender();
}
}, global.GAME_LOOP_INTERVAL);
};
Collision Detect
使用四叉树空间检索算法进行碰撞检测:
- 1.将要检测碰撞的球体加入四叉树
- 2.对每个球体进行碰撞检测,检测到的就进行标记
- 3.碰撞效果和伤害检测处理之后清空四叉树,进行下一轮碰撞检测
let collisionDetection = () => {
if (data.airPlane === undefined) {
return;
}
let selfBullets = data.bullet.concat(data.airPlane);
let enemyBullets = backendData.bullet.concat(backendData.airPlane);
let bulletsBank = selfBullets.concat(enemyBullets);
let i, j;
quad.clear();
for (i = 0; i < bulletsBank.length; i++) {
quad.insert(bulletsBank[i]);
}
for (i = 0; i < selfBullets.length; i++) {
let collidors = quad.retrieve(selfBullets[i]);
for (j = 0; j < collidors.length; j++) {
// 1 check camp
// 2 check type
// 3 check distance
if (collidors[j].camp === selfBullets[i].camp) {
continue;
}
if (collidors[j].ballType === AIRPLANE && selfBullets[i].ballType === AIRPLANE) {
continue;
}
let a = selfBullets[i].locationCurrent;
let b = collidors[j].locationCurrent;
let distance = PVector.dist(a, b);
if (distance <= collidors[j].radius + selfBullets[i].radius) {
let damageInformation = {
collision1: [selfBullets[i].userId, selfBullets[i].id],
collision2: [collidors[j].userId, collidors[j].id],
damageValue: [selfBullets[i].hp, collidors[j].hp],
state: []
};
// bullet <-> bullet
if (collidors[j].ballType === selfBullets[i].ballType) {
let chp = collidors[j].hp,
shp = selfBullets[i].hp;
collidors[j].hp -= shp;
selfBullets[i].hp -= chp;
} else {
// TODO: There is no method to check airPlane colliding with food.
collidors[j].hp = 0;
selfBullets[i].hp = 0;
}
if (collidors[j].hp <= 0) collidors[j].state = DEAD;
if (selfBullets[i].hp <= 0) selfBullets[i].state = DEAD;
damageInformation.state[0] = selfBullets[i].state;
damageInformation.state[1] = collidors[j].state;
gamemodel.socketCache.damageInformation.push(damageInformation);
//只判断两个相撞
break;
}
}
}
};
Useless Bullet Collect
Engine 对 hp 归0的子弹、超出射程范围的子弹、超出边界的子弹以及 hp 归0的飞机进行回收。
let uselessBulletsCollect = () => {
if (data.airPlane && data.airPlane.state === DEAD) {
gamemodel.deadCache.push(data.airPlane);
data.airPlane = undefined;
data.bullet.forEach((bullet) => {
bullet.state = DISAPPEAR;
});
}
backendData.airPlane = backendData.airPlane.filter((airPlane) => {
if (airPlane.state === DEAD) {
gamemodel.deadCache.push(airPlane);
return false;
}
return true;
});
data.bullet = data.bullet.filter((bullet) => {
if (bullet.state === DEAD) {
gamemodel.deadCache.push(bullet);
gamemodel.socketCache.disapperBulletInformation.push(bullet.id);
return false;
}
if (bullet.state === DISAPPEAR) {
gamemodel.disappearCache.push(bullet);
gamemodel.socketCache.disapperBulletInformation.push(bullet.id);
return false;
}
return true;
});
backendData.bullet = backendData.bullet.filter((bullet) => {
if (bullet.state === DEAD) {
gamemodel.deadCache.push(bullet);
return false;
}
if (bullet.state === DISAPPEAR) {
gamemodel.disappearCache.push(bullet);
return false;
}
return true;
});
};
Handle user input
在用户输入用户名结束之后会对时间监听进行重新绑定,键盘敲击的时间绑定在 window 下,鼠标的点击事件绑定在 canvas 下
window.addEventListener("keydown", key.downHandler.bind(key), false);
window.addEventListener("keyup", key.upHandler.bind(key), false);
export const configCanvasEventListen = () => {
//屏蔽右键菜单
document.addEventListener("contextmenu", (e) => {
e.preventDefault();
}, false);
let canvas = document.getElementById("canvas");
canvas.addEventListener("mousedown", mousePress);
canvas.addEventListener("mouseup", mouseRelease);
canvas.addEventListener("mousemove", mouseMove);
};
对键盘事件进行绑定
let keyboard = (keyCode) => {
let key = {};
key.code = keyCode;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
//The `downHandler`
key.downHandler = (event) => {
if (event.keyCode === key.code) {
if (key.isUp && key.press) key.press();
key.isDown = true;
key.isUp = false;
}
event.preventDefault();
};
//The `upHandler`
key.upHandler = (event) => {
if (event.keyCode === key.code) {
if (key.isDown && key.release) key.release();
key.isDown = false;
key.isUp = true;
event.preventDefault();
}
};
let keydownTuple = ["keydown", key.downHandler.bind(key), false];
let keyupTuple = ["keyup", key.upHandler.bind(key), false];
//Attach event listeners
window.addEventListener(...keydownTuple);
window.addEventListener(...keyupTuple);
windowEventTuples.push(keydownTuple);
windowEventTuples.push(keyupTuple);
return key;
};
Quadtree
简介:四叉树索引的基本思想是将地理空间递归划分为不同层次的树结构;它将已知范围的空间等分成四个相等的子空间,如此递归下去,直至树的层次达到一定深度或者满足某种要求后停止分割。四叉树的结构比较简单,并且当空间数据对象分布比较均匀时,具有比较高的空间数据插入和查询效率,因此四叉树是GIS中常用的空间索引之一。
插入:从根节点开始,判断你的数据点属于哪个象限。递归到相应的节点,重复步骤,直到到达叶节点,然后将该点加入节点的索引点列表中。如果列表中的元素个数超出了预设的最大数目,则将节点分裂,将其中的索引点移动到相应的子节点中去。
查询:查询四叉树时从根节点开始,检查每个子节点看是否与查询的区域相交。如果是,则递归进入该子节点。当到达叶节点时,检查点列表中的每一个项看是否与查询区域相交,如果是则返回此项。
缺点:四叉树对于区域查询,效率比较高。但如果空间对象分布不均匀,随着地理空间对象的不断插入,四叉树的层次会不断地加深,将形成一棵严重不平衡的四叉树,那么每次查询的深度将大大的增多,从而导致查询效率的急剧下降。
/*
* Split the node into 4 subnodes
*/
Quadtree.prototype.split = function() {
var nextLevel = this.level + 1,
subWidth = Math.round(this.bounds.width / 2),
subHeight = Math.round(this.bounds.height / 2),
x = Math.round(this.bounds.x),
y = Math.round(this.bounds.y);
//top right node
this.nodes[0] = new Quadtree({
x: x + subWidth,
y: y,
width: this.bounds.width - subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
//top left node
this.nodes[1] = new Quadtree({
x: x,
y: y,
width: subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
//bottom left node
this.nodes[2] = new Quadtree({
x: x,
y: y + subHeight,
width: subWidth,
height: this.bounds.height - subHeight
}, this.max_objects, this.max_levels, nextLevel);
//bottom right node
this.nodes[3] = new Quadtree({
x: x + subWidth,
y: y + subHeight,
width: this.bounds.width - subWidth,
height: this.bounds.height - subHeight
}, this.max_objects, this.max_levels, nextLevel);
};
/*
* Determine which node the object belongs to
* @param Object pRect bounds of the area to be checked, with x, y, width, height
* @return Integer index of the subnode (0-3), or -1 if pRect cannot completely fit within a subnode and is part of the parent node
*/
Quadtree.prototype.getIndex = function(pRect) {
var index = -1,
// |
verticalMidpoint = this.bounds.x + (this.bounds.width / 2),
// 一
horizontalMidpoint = this.bounds.y + (this.bounds.height / 2),
// TODO: change radius to bound border length.
//pRect can completely fit within the top quadrants
topQuadrant = (pRect.locationCurrent.y < horizontalMidpoint && pRect.locationCurrent.y + pRect.radius <= horizontalMidpoint),
//pRect can completely fit within the bottom quadrants
bottomQuadrant = (pRect.locationCurrent.y > horizontalMidpoint && pRect.locationCurrent.y - pRect.radius >= horizontalMidpoint);
//pRect can completely fit within the left quadrants
if (pRect.locationCurrent.x < verticalMidpoint && pRect.locationCurrent.x + pRect.radius <= verticalMidpoint) {
if (topQuadrant) {
index = 1;
} else if (bottomQuadrant) {
index = 2;
}
//pRect can completely fit within the right quadrants
} else if (pRect.locationCurrent.x > verticalMidpoint && pRect.locationCurrent.x - pRect.radius >= verticalMidpoint) {
if (topQuadrant) {
index = 0;
} else if (bottomQuadrant) {
index = 3;
}
}
return index;
};
/*
* Insert the object into the node. If the node
* exceeds the capacity, it will split and add all
* objects to their corresponding subnodes.
* @param Object pRect bounds of the object to be added, with x, y, width, height
*/
Quadtree.prototype.insert = function(pRect) {
var i = 0,
index;
//if we have subnodes ...
if (this.nodes[0] !== null) {
index = this.getIndex(pRect);
if (index !== -1) {
this.nodes[index].insert(pRect);
return;
}
}
this.objects.push(pRect);
if (this.objects.length <= this.max_objects || this.level >= this.max_levels) {
return;
}
//split if we don't already have subnodes
if (this.nodes[0] === null) {
this.split();
}
//add all objects to there corresponding subnodes
while (i < this.objects.length) {
index = this.getIndex(this.objects[i]);
if (index !== -1) {
this.nodes[index].insert(this.objects.splice(i, 1)[0]);
} else {
i = i + 1;
}
}
};