tirsdag 29. april 2014

[ SDL2 - Part 5 ] Collision detection and our first game!

Collision detection


Collision detection is one of the key aspects of game programming. For all non-rectangular objects it gets harder and harder the more accurate you want it to be. But today we're gonna continue just using rectangles. This part describes a simple algorithm to check for intersections.


After we've created a functions that does rectangle-rectangle collision detections, we'll use all of what we've learned so far and make a simple game!

Rectangle - rectangle collisions


Instead of checking if two rectangles are inside or touching each other, we're gonna check for the opposite; if they are outside of each other. The algorithm to do so consists of two steps.

  1. Find the x coordinate of the left and right and the y coordinate for top and bottom.


    Say we have two rectangles, rect1, and rect2.
    SDL_Rect rect1;
    SDL_Rect rect2;
    Finding the x/y of left/right/top/bottom is very easy.
    int left1 = rect1.x;
    int right1 = rect1.x + rect2.w;
    int top1 = rect1.y;
    int bottom1 = rect1.y + rect2.h;
    The process is exactly the same for the second rect.

  2. Use the 8 edges to check if they are not colliding


    Take a look at the below drawing

    As you can see, the red rectangle is farther to the right than the blue one. But how do can we easily check that? Simply by checking if redLeft is further to the right than blueRight. If it is ( like it is in our case ) there is no collision. Then it's just a simple matter of repeating it for the other edges. So we end up with something like this :

    SDL_Rect rect1;
    SDL_Rect rect2;

    // Find edges of rect1
    int left1 = rect1.x;
    int right1 = rect1.x + rect1.w;
    int top1 = rect1.y;
    int bottom1 = rect1.y + rect1.h;

    // Find edges of rect2
    int left2 = rect2.x;
    int right2 = rect2.x + rect2.w;
    int top2 = rect2.y;
    int bottom2 = rect2.y + rect2.h;

    // Check edges
    if ( left1 > right2 )// Left 1 is right of right 2
        return false; // No collision

    if ( right1 < left2 ) // Right 1 is left of left 2
        return false; // No collision

    if ( top1 > bottom2 ) // Top 1 is below bottom 2
        return false; // No collision

    if ( bottom1 < top2 ) // Bottom 1 is above top 2
        return false; // No collision

    // None of the above test were true, collision! return true;

The resulting function


So here's the final function for you to copy and test out.
bool CheckCollision( const SDL_Rect &rect1, const SDL_Rect &rect2 )
{
// Find edges of rect1
int left1 = rect1.x;
int right1 = rect1.x + rect1.w;
int top1 = rect1.y;
int bottom1 = rect1.y + rect1.h;
// Find edges of rect2
int left2 = rect2.x;
int right2 = rect2.x + rect2.w;
int top2 = rect2.y;
int bottom2 = rect2.y + rect2.h;
// Check edges
if ( left1 > right2 )// Left 1 is right of right 2
return false; // No collision
if ( right1 < left2 ) // Right 1 is left of left 2
return false; // No collision
if ( top1 > bottom2 ) // Top 1 is below bottom 2
return false; // No collision
if ( bottom1 < top2 ) // Bottom 1 is above top 2
return false; // No collision
return true;
}
view raw collision.cpp hosted with ❤ by GitHub

If you want to shorten the code, you can remove the comments and replace the variables with the rect x / y / w / h values like so :

if ( rect1.x > ( rect2.x + rect2.w ) )// Left 1 is right of right 2
    return false; // No collision

I chose to not do this as the longer version is a bit easier to read and understand.

Our first game!


Our first game is of the "lead the chicken safely across the road" kind of things. Or, as in our case, "lead the square from the rectangle, past all the other square and to the other rectangle." You control the square with the arrow keys. If you hit a red square, you'll end up where you started. If you make it to the other side, you'll also end up where you started, but at least you have that tiny sense of victory for helping our poor little square!


The code just uses the different things we've learned so far, so I won't explain it other than in code comments and ( hopefully ) descriptive names.

So without further ado, here's the code :

// A simple "lead the chicken across the road" game using C++ and SDL2
// Compile : clang++ main.cpp -std=c++11 -lSDL2 -o Game or g++ main.cpp -std=c++11 -lSDL2 -o Game
#include <SDL2/SDL.h>
#include <iostream>
#include <vector>
bool InitEverything();
bool InitSDL();
bool CreateWindow();
bool CreateRenderer();
void SetupRenderer();
void Render();
void RunGame();
void AddEnemy();
void MoveEnemies();
void ResetPlayerPos();
bool CheckCollision( const SDL_Rect &rect1, const SDL_Rect &rect2 );
bool CheckEnemyCollisions();
// Window pos
int posX = 900;
int posY = 300;
int sizeX = 300;
int sizeY = 400;
int movementFactor = 15; // Amount of pixels the player move per key press
int lastEnemyPos = 50;
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Rect playerPos;
SDL_Rect topBar;
SDL_Rect bottomBar;
enum class Direction
{
Left,
Right
};
struct Enemy
{
Enemy( SDL_Rect pos_, int speed_, Direction dir_ )
{
pos = pos_;
speed = speed_;
dir = dir_;
}
SDL_Rect pos;
int speed;
Direction dir;
};
std::vector< Enemy > enemies;
int main( int argc, char* args[] )
{
if ( !InitEverything() )
return -1;
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
AddEnemy();
// Init top and bottom bar
topBar.x = 0;
topBar.y = 0;
topBar.w = sizeX;
topBar.h = 20;
bottomBar.x = 0;
bottomBar.y = sizeY - 20;
bottomBar.w = sizeX;
bottomBar.h = 20;
// Initlaize our player
playerPos.w = 20;
playerPos.h = 20;
ResetPlayerPos();
RunGame();
}
void RunGame()
{
bool loop = true;
while ( loop )
{
SDL_Event event;
while ( SDL_PollEvent( &event ) )
{
if ( event.type == SDL_QUIT )
loop = false;
else if ( event.type == SDL_KEYDOWN )
{
switch ( event.key.keysym.sym )
{
case SDLK_RIGHT:
playerPos.x += movementFactor;
break;
case SDLK_LEFT:
playerPos.x -= movementFactor;
break;
// Remeber 0,0 in SDL is left-top. So when the user pressus down, the y need to increase
case SDLK_DOWN:
playerPos.y += movementFactor;
break;
case SDLK_UP:
playerPos.y -= movementFactor;
break;
default :
break;
}
}
}
MoveEnemies();
// Check collisions against enemies
if ( CheckEnemyCollisions() )
ResetPlayerPos();
// Check collusion against bottom bar
// Since top bar covers the entire width, we only need to check the y value
// topBar.y refers to top of top bar ( top of the screen )
// Since 0,0 is the top-right we need to add topBar.h to find the bottom of topBar
if ( playerPos.y < ( topBar.y + topBar.h) )
ResetPlayerPos();
Render();
// Add a 16msec delay to make our game run at ~60 fps
SDL_Delay( 16 );
}
}
void Render()
{
// Clear the window and make it all red
SDL_RenderClear( renderer );
// Change color to black!
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
// Render top and bottom bar
SDL_RenderFillRect( renderer, &bottomBar );
SDL_RenderFillRect( renderer, &topBar );
// Change color to blue!
SDL_SetRenderDrawColor( renderer, 0, 0, 255, 255 );
// Render our "player"
SDL_RenderFillRect( renderer, &playerPos );
// Change color to red!
SDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 );
for ( const auto &p : enemies )
SDL_RenderFillRect( renderer, &p.pos );
// Change color to green!
SDL_SetRenderDrawColor( renderer, 255, 255, 255, 255 );
// Render the changes above
SDL_RenderPresent( renderer);
}
bool InitEverything()
{
if ( !InitSDL() )
return false;
if ( !CreateWindow() )
return false;
if ( !CreateRenderer() )
return false;
SetupRenderer();
return true;
}
bool InitSDL()
{
if ( SDL_Init( SDL_INIT_EVERYTHING ) == -1 )
{
std::cout << " Failed to initialize SDL : " << SDL_GetError() << std::endl;
return false;
}
return true;
}
bool CreateWindow()
{
window = SDL_CreateWindow( "Server", posX, posY, sizeX, sizeY, 0 );
if ( window == nullptr )
{
std::cout << "Failed to create window : " << SDL_GetError();
return false;
}
return true;
}
bool CreateRenderer()
{
renderer = SDL_CreateRenderer( window, -1, 0 );
if ( renderer == nullptr )
{
std::cout << "Failed to create renderer : " << SDL_GetError();
return false;
}
return true;
}
void SetupRenderer()
{
// Set size of renderer to the same as window
SDL_RenderSetLogicalSize( renderer, sizeX, sizeY );
// Set color of renderer to red
SDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 );
}
void MoveEnemies()
{
for ( auto &p : enemies )
{
if ( p.dir == Direction::Right )
{
p.pos.x += p.speed;
if ( p.pos.x >= sizeX )
p.pos.x = 0;
}
else
{
p.pos.x -= p.speed;
if ( ( p.pos.x + p.pos.w ) <= 0 )
p.pos.x = sizeX - p.pos.w;
}
}
}
bool CheckCollision( const SDL_Rect &rect1, const SDL_Rect &rect2 )
{
// Find edges of rect1
int left1 = rect1.x;
int right1 = rect1.x + rect1.w;
int top1 = rect1.y;
int bottom1 = rect1.y + rect1.h;
// Find edges of rect2
int left2 = rect2.x;
int right2 = rect2.x + rect2.w;
int top2 = rect2.y;
int bottom2 = rect2.y + rect2.h;
// Check edges
if ( left1 > right2 )// Left 1 is right of right 2
return false; // No collision
if ( right1 < left2 ) // Right 1 is left of left 2
return false; // No collision
if ( top1 > bottom2 ) // Top 1 is below bottom 2
return false; // No collision
if ( bottom1 < top2 ) // Bottom 1 is above top 2
return false; // No collision
return true;
}
bool CheckEnemyCollisions()
{
for ( const auto &p : enemies )
{
if ( CheckCollision( p.pos, playerPos) )
return true;
}
return false;
}
void AddEnemy()
{
if ( ( rand() % 2 ) == 0 )
{
enemies.push_back( Enemy( { rand() % 300, lastEnemyPos, 20, 20 }, 1, Direction::Right ) );
}
else
{
enemies.push_back( Enemy( { rand() % 300, lastEnemyPos, 20, 20 }, 1, Direction::Left ) );
}
lastEnemyPos += 25;
}
void ResetPlayerPos()
{
// sizeX / 2 = middle pixel of the screen
// playerPos.w / 2 = middle of the player
// So setting player x pos to [middle of screen] - [middle of player] means it will be centerd in the screen.
playerPos.x = ( sizeX / 2 ) - ( playerPos.w / 2 );
playerPos.y = sizeY - bottomBar.h;
}
view raw Simple Game.cpp hosted with ❤ by GitHub



Feel free to comment if you have anything to say or ask questions if anything is unclear. I always appreciate getting comments.

For a full list of my tutorials / posts, click here.

2 kommentarer:

  1. Hi, Great article. I believe you have made a mistake here:

    if ( top1 > bottom2 ) // Top 1 is below bottom 2
    return false; // No collision

    if ( bottom1 < top2 ) // Bottom 1 is above top 2
    return false; // No collision

    I think it should be:

    if ( top1 < bottom2 ) // Top 1 is below bottom 2
    return false; // No collision

    if ( bottom1 > top2 ) // Bottom 1 is above top
    return false; // No collision

    Am I right?

    SvarSlett
    Svar
    1. Hello!

      Remember that the SDL2 coordinate system is opposite. Higher y value means further down. So the code is correct. You can also verify this by running it, you'll see that the collisions are working properly.

      By the way, this is my old blog and I don't update it anymore. Please check out my new blog : headerphile.com.

      Slett