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;
        }
    }
};