SDL程式設計入門(28)每畫素碰撞檢測

血色v殘陽發表於2020-12-30

每畫素碰撞檢測

一旦你知道如何檢查兩個矩形之間的碰撞,你可以檢查任何兩個影像之間的碰撞,因為所有的影像都是由矩形構成的。

在電子遊戲中,所有的東西都可以用矩形來做,甚至這個點:

沒看到?我們把它放大:

還沒看到?那現在呢?:

影像由正方形的畫素組成,正方形的畫素為矩形。 要進行每個畫素的碰撞檢測,我們要做的就是讓每個物件都有一組碰撞框,並檢查一組碰撞框與另一組碰撞框的碰撞,如下所示:

//在螢幕上移動的點
class Dot
{
    public:
        //The dimensions of the dot
        static const int DOT_WIDTH = 20;
        static const int DOT_HEIGHT = 20;

        //Maximum axis velocity of the dot
        static const int DOT_VEL = 1;

        //Initializes the variables
        Dot( int x, int y );

        //Takes key presses and adjusts the dot's velocity
        void handleEvent( SDL_Event& e );

        //Moves the dot and checks collision
        void move( std::vector<SDL_Rect>& otherColliders );

        //Shows the dot on the screen
        void render();

        //獲取碰撞框
        std::vector<SDL_Rect>& getColliders();

    private:
        //The X and Y offsets of the dot
        int mPosX, mPosY;

        //The velocity of the dot
        int mVelX, mVelY;
        
        //點的碰撞框
        std::vector<SDL_Rect> mColliders;

        //移動碰撞框相對於點的偏移
        void shiftColliders();
};

這是我們的點,現在有了每畫素碰撞檢測。它的速度減少到每幀1畫素,使碰撞更容易看到。move函式現在接受了一個碰撞框的向量,所以我們可以對照檢查兩組碰撞。由於我們將有兩個點碰撞,我們需要能夠得到碰撞器,所以我們有一個函式來處理。

我們沒有一個單一的碰撞框,而是有一個碰撞器的向量。我們也有一個內部函式來移動碰撞器以匹配點的位置。

//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

//Frees media and shuts down SDL
void close();

//Box set collision detector
bool checkCollision( std::vector<SDL_Rect>& a, std::vector<SDL_Rect>& b );

在這裡,我們有了新的碰撞檢測器,它可以相互檢查碰撞框的集合。

Dot::Dot( int x, int y )
{
    //初始化偏移量
    mPosX = x;
    mPosY = y;

    //建立必要的SDL_Rects
    mColliders.resize( 11 );

    //初始化速度
    mVelX = 0;
    mVelY = 0;

    //初始化碰撞框的寬度和高度。
    mColliders[ 0 ].w = 6;
    mColliders[ 0 ].h = 1;

    mColliders[ 1 ].w = 10;
    mColliders[ 1 ].h = 1;

    mColliders[ 2 ].w = 14;
    mColliders[ 2 ].h = 1;

    mColliders[ 3 ].w = 16;
    mColliders[ 3 ].h = 2;

    mColliders[ 4 ].w = 18;
    mColliders[ 4 ].h = 2;

    mColliders[ 5 ].w = 20;
    mColliders[ 5 ].h = 6;

    mColliders[ 6 ].w = 18;
    mColliders[ 6 ].h = 2;

    mColliders[ 7 ].w = 16;
    mColliders[ 7 ].h = 2;

    mColliders[ 8 ].w = 14;
    mColliders[ 8 ].h = 1;

    mColliders[ 9 ].w = 10;
    mColliders[ 9 ].h = 1;

    mColliders[ 10 ].w = 6;
    mColliders[ 10 ].h = 1;

    //初始化相對於位置的碰撞器
    shiftColliders();
}

就像之前一樣,我們必須在建構函式中設定碰撞器的尺寸。這裡唯一不同的是,我們有多個碰撞框需要設定。

void Dot::move( std::vector<SDL_Rect>& otherColliders ){
    //Move the dot left or right
    mPosX += mVelX;
    shiftColliders();

    //If the dot collided or went too far to the left or right
    if( ( mPosX < 0 ) || ( mPosX + DOT_WIDTH > SCREEN_WIDTH ) || checkCollision( mColliders, otherColliders ) )
    {
        //Move back
        mPosX -= mVelX;
        shiftColliders();
    }

    //Move the dot up or down
    mPosY += mVelY;
    shiftColliders();

    //If the dot collided or went too far up or down
    if( ( mPosY < 0 ) || ( mPosY + DOT_HEIGHT > SCREEN_HEIGHT ) || checkCollision( mColliders, otherColliders ) )
    {
        //Move back
        mPosY -= mVelY;
        shiftColliders();
    }
}

這個功能和之前的差不多。每當我們移動點,我們就移動碰撞器。在我們移動點之後,我們檢查它是否離開了螢幕或撞到了什麼東西。如果是這樣,我們就把點移回來,並把它的碰撞器也一起移動。

void Dot::shiftColliders(){
    //行偏移量
    int r = 0;

    //通過點的碰撞框
    for( int set = 0; set < mColliders.size(); ++set )
    {
        //將碰撞框居中
        mColliders[ set ].x = mPosX + ( DOT_WIDTH - mColliders[ set ].w ) / 2;

        //在它的行偏移處設定碰撞框
        mColliders[ set ].y = mPosY + r;

        //將行的偏移量向下移動到碰撞框的高度。
        r += mColliders[ set ].h;
    }
}

std::vector<SDL_Rect>& Dot::getColliders(){
    return mColliders;
}

不要太擔心shiftColliders的工作原理。它是mColliders[ 0 ].x = …,mColliders[ 1 ].x = …等的簡便方法,它適用於這種特定情況。對於自己的每一個畫素物件,你會有自己的放置函式。

而在shiftColliders之後,要有一個獲取colliders的訪問函式。

bool checkCollision( std::vector<SDL_Rect>& a, std::vector<SDL_Rect>& b ){
    //矩形的邊框
    int leftA, leftB;
    int rightA, rightB;
    int topA, topB;
    int bottomA, bottomB;

    //通過A框
    for( int Abox = 0; Abox < a.size(); Abox++ )
    {
        //計算矩形A的邊長
        leftA = a[ Abox ].x;
        rightA = a[ Abox ].x + a[ Abox ].w;
        topA = a[ Abox ].y;
        bottomA = a[ Abox ].y + a[ Abox ].h;
        //通過B框
        for( int Bbox = 0; Bbox < b.size(); Bbox++ )
        {
            //計算矩形B的邊長
            leftB = b[ Bbox ].x;
            rightB = b[ Bbox ].x + b[ Bbox ].w;
            topB = b[ Bbox ].y;
            bottomB = b[ Bbox ].y + b[ Bbox ].h;

            //如果A的任何邊都不在B的外面
            if( ( ( bottomA <= topB ) || ( topA >= bottomB ) || ( rightA <= leftB ) || ( leftA >= rightB ) ) == false )
            {
                //檢測到碰撞
                return true;
            }
        }
    }

    //如果兩組碰撞框都沒有接觸
    return false;
}

在我們的碰撞檢測函式中,我們有一個for迴圈,計算物件a中每個碰撞框的頂部/底部/左側/右側。

然後我們計算物件b中每個碰撞框的上/下/左/右,然後檢查是否沒有分離軸。如果沒有分離軸,我們返回true。如果我們通過這兩個集合而沒有碰撞,我們返回false。

            //Main loop flag
            bool quit = false;

            //Event handler
            SDL_Event e;

            //將在螢幕上移動的點
            Dot dot( 0, 0 );
            
            //將要碰撞的點
            Dot otherDot( SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4 );

在進入主迴圈之前,我們先宣告我們的點和我們要碰撞的另一個點。

             //While application is running
            while( !quit )
            {
                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }

                    //Handle input for the dot
                    dot.handleEvent( e );
                }

                //移動點並檢查碰撞
                dot.move( otherDot.getColliders() );

                //Clear screen
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );
                
                //Render dots
                dot.render();
                otherDot.render();

                //Update screen
                SDL_RenderPresent( gRenderer );
            }

再次在主迴圈中,為點處理事件,對點進行碰撞檢查,然後最後我們渲染我們的物件。

我經常被問到的一個問題是,如何製作一個載入影像並自動生成每畫素碰撞檢測的碰撞框集的函式。答案很簡單。

別這樣做

在大多數遊戲中,你不希望100%的準確性。碰撞框越多,你的碰撞檢查就越多,速度也越慢。大多數遊戲追求的是足夠接近,比如在《街頭霸王》中:

結果雖然不是畫素完美,但已經很接近了。

另外,我們還可以在這裡做一個優化。我們可以為點設定一個邊界框,封裝所有其他的碰撞框,然後在進入每個畫素的碰撞框之前先檢查這一個。這樣做確實會多增加一次碰撞檢測,但由於兩個物體不碰撞的可能性更大,所以更可能為我們節省額外的碰撞檢測。在遊戲中,通常使用具有不同細節級別的樹結構來完成此操作,以便儘早使用,以防止在每個畫素級別進行不必要的檢查。和之前的教程一樣,樹形結構不在這些教程的範圍內。

這裡下載本教程的媒體和原始碼。

原文連結

關注我的公眾號:程式設計之路從0到1
程式設計之路從0到1

相關文章