四叉樹之碰撞檢測(梁王的代碼解剖室)

前言

這篇文章會(huì)簡單介紹一下四叉樹的基本思想,然后會(huì)對(duì)timohausmann/quadtree-js進(jìn)行代碼解析。

預(yù)備理論

什么是四叉樹

為什么需要四叉樹

怎么通過四叉樹進(jìn)行碰撞檢測

代碼解析

構(gòu)造函數(shù)

function Quadtree( bounds, max_objects, max_levels, level ) {
    
    this.max_objects    = max_objects || 10; //每個(gè)區(qū)域可以容納的最大對(duì)象數(shù),超過就需要?jiǎng)澐?    this.max_levels     = max_levels || 4; //最多劃幾層四叉樹
    
    this.level      = level || 0; //當(dāng)前樹或子樹的層,根為0
    this.bounds         = bounds; //bounds就是對(duì)象集,每個(gè)點(diǎn)包括x,y,width,height(有些實(shí)現(xiàn)是圓形,這個(gè)是矩形)
    
    this.objects        = []; //屬于當(dāng)前節(jié)點(diǎn)的對(duì)象,不包括子節(jié)點(diǎn)的對(duì)象
    this.nodes      = []; //屬于當(dāng)前樹的節(jié)點(diǎn)
};

劃分

當(dāng)調(diào)用Insert函數(shù)向樹中插入對(duì)象的時(shí)候,如果當(dāng)前節(jié)點(diǎn)沒有被劃分過的時(shí)候,會(huì)判斷節(jié)點(diǎn)的對(duì)象數(shù)是否超過的限制的max_objects,如果超過了的話當(dāng)前節(jié)點(diǎn)就會(huì)調(diào)用這個(gè)split方法。

Quadtree.prototype.split = function() {
    
    var nextLevel = this.level + 1;
    var subWidth = Math.round( this.bounds.width / 2 );
    var subHeight = Math.round( this.bounds.height / 2 );
    var x = Math.round( this.bounds.x );
    var y = Math.round( this.bounds.y );
 
    //第一象限,和數(shù)學(xué)里的坐標(biāo)軸一樣,不過起點(diǎn)變了而已
    this.nodes[0] = new Quadtree({
        x   : x + subWidth, 
        y   : y, 
        width   : subWidth, 
        height  : subHeight
    }, this.max_objects, this.max_levels, nextLevel);
    
    //第二象限
    this.nodes[1] = new Quadtree({
        x   : x, 
        y   : y, 
        width   : subWidth, 
        height  : subHeight
    }, this.max_objects, this.max_levels, nextLevel);
    
    //第三象限
    this.nodes[2] = new Quadtree({
        x   : x, 
        y   : y + subHeight, 
        width   : subWidth, 
        height  : subHeight
    }, this.max_objects, this.max_levels, nextLevel);
    
    //第四象限
    this.nodes[3] = new Quadtree({
        x   : x + subWidth, 
        y   : y + subHeight, 
        width   : subWidth, 
        height  : subHeight
    }, this.max_objects, this.max_levels, nextLevel);
};

基本是切割,沒什么太值得一說的,劃分后的節(jié)點(diǎn)level + 1了

查找對(duì)象

插入節(jié)點(diǎn)和碰撞檢測的時(shí)候我們需要先知道對(duì)象在這個(gè)節(jié)點(diǎn)所在的象限,這個(gè)函數(shù)輸入一個(gè)有x,y,width,height的對(duì)象,并判斷應(yīng)該屬于這個(gè)節(jié)點(diǎn)的哪個(gè)象限。

Quadtree.prototype.getIndex = function( pRect ) {
    
     var     index           = -1,
        verticalMidpoint    = this.bounds.x + (this.bounds.width / 2),
        horizontalMidpoint  = this.bounds.y + (this.bounds.height / 2),
 
        topQuadrant = (pRect.y < horizontalMidpoint && pRect.y + pRect.height < horizontalMidpoint),
        
        //pRect can completely fit within the bottom quadrants
        bottomQuadrant = (pRect.y > horizontalMidpoint);
     
    if( pRect.x < verticalMidpoint && pRect.x + pRect.width < verticalMidpoint ) {
        if( topQuadrant ) {
            index = 1;
        } else if( bottomQuadrant ) {
            index = 2;
        }
        
    } else if( pRect.x > verticalMidpoint ) {
        if( topQuadrant ) {
            index = 0;
        } else if( bottomQuadrant ) {
            index = 3;
        }
    }
 
    return index;
};

比較簡單的數(shù)學(xué),不過值得注意一點(diǎn)的是,如果一個(gè)對(duì)象是跨象限的,那么在它會(huì)返回-1

插入對(duì)象到節(jié)點(diǎn)

Quadtree.prototype.insert = function( pRect ) {
    
    var i = 0,
    var index;
    
    // 如果當(dāng)前節(jié)點(diǎn)已經(jīng)劃分過了,就查找對(duì)象所屬象限,遞歸調(diào)用
    if( typeof this.nodes[0] !== 'undefined' ) {
        index = this.getIndex( pRect );
 
        if( index !== -1 ) {
            this.nodes[index].insert( pRect );   
            return;
        }
    }
 
    this.objects.push( pRect );
    
    // 如果節(jié)點(diǎn)對(duì)象超過設(shè)置值,而且還能繼續(xù)劃分(level沒到上限)的時(shí)候
    if( this.objects.length > this.max_objects && this.level < this.max_levels ) {
        
        // 先劃分節(jié)點(diǎn)
        if( typeof this.nodes[0] === 'undefined' ) {
            this.split();
        }
        
        // 把對(duì)象加入對(duì)應(yīng)子節(jié)點(diǎn)
        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;
            }
        }
    }
};

這里值得注意一點(diǎn)的是,如果一個(gè)對(duì)象是跨象限的,這種時(shí)候怎么處理??创a段

if( index !== -1 ) {                    
    this.nodes[index].insert( this.objects.splice(i, 1)[0] );
} else {
    i = i + 1;
}

之前getIndex的時(shí)候我們就說過,如果一個(gè)對(duì)象是跨象限的,getIndex會(huì)返回-1,從代碼來看,跨象限的對(duì)象會(huì)被放在當(dāng)前節(jié)點(diǎn)的objects里面而不會(huì)被劃給子節(jié)點(diǎn)。這一點(diǎn)很有必要,因?yàn)閎labla(待補(bǔ)充)

返回碰撞候選列表

四叉樹最核心的一部分就是要過濾掉一些根本不可能碰撞的對(duì)象,避免對(duì)比全部的對(duì)象以此來提高效率,這個(gè)函數(shù)輸入一個(gè)包含x,y,width,height的對(duì)象,返回一個(gè)集合,是經(jīng)過過濾后的可能和輸入對(duì)象發(fā)生碰撞的候選對(duì)對(duì)象。

Quadtree.prototype.retrieve = function( pRect ) {
    
    var     index = this.getIndex( pRect ),
        returnObjects = this.objects;
        
    //if we have subnodes ...
    if( typeof this.nodes[0] !== 'undefined' ) {
        
        //if pRect fits into a subnode ..
        if( index !== -1 ) {
            returnObjects = returnObjects.concat( this.nodes[index].retrieve( pRect ) );
            
        //if pRect does not fit into a subnode, check it against all subnodes
        } else {
            for( var i=0; i < this.nodes.length; i=i+1 ) {
                returnObjects = returnObjects.concat( this.nodes[i].retrieve( pRect ) );
            }
        }
    }
 
    return returnObjects;
};

首先我們先確立一下,對(duì)于一個(gè)對(duì)象來說,什么樣的對(duì)象才算是可能和它發(fā)生碰撞的候選對(duì)象?,代碼里面主要分兩部分來考慮
一個(gè)是對(duì)象就在這個(gè)節(jié)點(diǎn)的某個(gè)象限里面,這個(gè)時(shí)候看代碼段

        if( index !== -1 ) {
            returnObjects = returnObjects.concat( this.nodes[index].retrieve( pRect ) );
            
        //if pRect does not fit into a subnode, check it against all subnodes
        } 

如果一個(gè)對(duì)象在當(dāng)前節(jié)點(diǎn)的某個(gè)象限里面,則其碰撞候選是當(dāng)前節(jié)點(diǎn)的對(duì)象加上那個(gè)象限里面調(diào)用retrieve的節(jié)點(diǎn)。這里也解釋了為什么跨象限的節(jié)點(diǎn)的對(duì)象就放在當(dāng)前節(jié)點(diǎn)上,因?yàn)榭缦笙薜墓?jié)點(diǎn)也有可能和某個(gè)象限內(nèi)的節(jié)點(diǎn)發(fā)生碰撞,所以需要把這些對(duì)象都加入到候選對(duì)象里面(盡管可能這個(gè)對(duì)象在第四象限,而跨象限的對(duì)象在第一象限和第二象限之間)

其次是這個(gè)對(duì)象本身就是跨象限的,這個(gè)時(shí)候看代碼段

        else {
            for( var i=0; i < this.nodes.length; i=i+1 ) {
                returnObjects = returnObjects.concat( this.nodes[i].retrieve( pRect ) );
            }
        }

就是直接把這個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)遞歸把結(jié)果集合到一起,然后根據(jù)上面retrieve的內(nèi)容我們知道,如果輸入的對(duì)象不在節(jié)點(diǎn)的任意象限內(nèi),則返回節(jié)點(diǎn)上的對(duì)象
總結(jié)一下就是,當(dāng)這個(gè)對(duì)象是個(gè)跨象限的對(duì)象的時(shí)候
可能發(fā)生碰撞的是所屬節(jié)點(diǎn)所有跨象限的對(duì)象加上所跨象限的所有內(nèi)容,具體可以在網(wǎng)站上自己試一下simple demo

總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容