SDL编程入门(28)每像素碰撞检测
每像素碰撞检测
一旦你知道如何检查两个矩形之间的碰撞,你可以检查任何两个图像之间的碰撞,因为所有的图像都是由矩形构成的。
在电子游戏中,所有的东西都可以用矩形来做,甚至这个点:
没看到?我们把它放大:
还没看到?那现在呢?:
图像由正方形的像素组成,正方形的像素为矩形。 要进行每个像素的碰撞检测,我们要做的就是让每个对象都有一组碰撞框,并检查一组碰撞框与另一组碰撞框的碰撞,如下所示:
//在屏幕上移动的点
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
推荐阅读