Advertisement

C++ and SDL2 Tile Collision

Started by July 31, 2017 04:04 AM
12 comments, last by Rutin 7 years, 3 months ago

I'm having trouble figuring out how to accomplish tile collision with SDL2 and C++.

I have a 2D map vector which contains either a 1 or a 0 for each index.


std::vector<std::vector<int> > map;

That map contains data for which tiles can be collided with and which tiles cannot.  Below is an example of what it could look like.


{{0, 1, 0, 1, 0},
 {1, 0, 1, 0, 0},
 {0, 0, 0, 1, 0},
 {0, 1, 1, 1, 0},
 {1, 0, 0, 0, 1}}

I also have a player class which contains some simple update, draw, move, etc. functions.  All the player's logic takes place in the update function.  The player's position information is stored in an SDL_RECT like so.


SDL_RECT player_rectangle;
  
player_rectangle.x = 0;
player_rectangle.y = 0;
player_rectangle.w = 16;
player_rectangle.h = 16;

As you may know by now, each tile in this game is 16 by 16 pixels.  And movement in the game is tile-based.  In the update function, I'm able to correct collisions with the edge of the map like so.


void update() {
  // Process movement
  
  if (player_rectangle.x < 0) player_rectangle.x = 0;
  if (player_rectangle.x > map_rectangle.w - 16) player_rectangle.x = map_rectangle.w - 16;
  if (player_rectangle.y < 0) player_rectangle.y = 0;
  if (player_rectangle.y > map_rectangle.h - 16) player_rectangle.y = map_rectangle.h - 16;
}

My issue with understanding how to do this lies in the fact that the player's position is measured in pixels, while the map's tiles are measured as a grid, each index being 16 by 16 pixels.  I've tried dividing the player's position by the tile's size in order to convert it to a grid so that I could try something similar to this.


if (map[player_rectangle.y / 16][player_rectangle.x / 16] == 1) // Correct collision

But this doesn't work all the time since the player's x and y positions aren't always factors of 16 when the player is moving between tiles.  In fact, I can't even figure out how I would correct this collision even if I could figure out how to map out the player's position to the collision map.

I'm aware this probably isn't the best approach, so I'm open to better ideas for accomplishing this.

A free–moving tile can cover at most four tiles.  Store the dimensions of the player as floats, use these and the velocity to determine which tiles to check for collision.  Once you’ve found the nearest colliding tile in each the X and Y directions, clamp the X or Y component of the player’s velocity such that the player does not collide with the tile(s).  This will allow for sliding and will prevent sticking and jittering.

Here are a few helpful resources I found:

http://www.metanetsoftware.com/technique/tutorialA.html
http://jonathanwhiting.com/tutorial/collision/

Advertisement

I'm not sure if i understand. But why you just not write a function that compares all the pixels from the player actual position until the player height/width ..? 
And for more performance you can create a static variable to hold the last position and direction of the last move, so, if the player continues moving towards the colition object the function will return false without any work.

i'm a newbie and i don't know anything about SDL but (if your program works like i think) it has logic for me.

Let’s assume your tiles are 32x32 in size, and your player is also 32x32.

If your map is as such:

{{0, 1, 0, 1, 0},

 {1, 0, 1, 0, 0},

 {0, 0, 0, 1, 0},

 {0, 1, 1, 1, 0},

 {1, 0, 0, 0, 1}}

 

And your player is at touching these tiles:


{{0, 1, 0, 1, 0},

 {1, 0, 1, 0, 0},

 {0, 0, 0, 1, 0},

 {0, 1, 1, 1, 0},

 {1, 0, 0, 0, 1}}

 

You would take the players position of x and y, and divide it by 32 to get the tile spot. So in this example we will assume 112 x and 80 y so the player is touching all 4 tiles. Now you would take that value and divide by 32 getting: 3.5 – (1 due to 0 based indexing), and 2.5 – (1 due to 0 based indexing). This would tell you that 2-3 and 1-2 are hit which means 2,1 and 2,2 and 3,1 and 3,2:

[y][x] in this example based on the tile grid.

[1][2] hit

[2][2] hit

[1][3] hit

[2][3] hit

What you should do is ghosting. This means you move in theory to the spot, and check if the collision allows it, then if it comes back false, don’t allow the movement before updating the real positions.

Programmer and 3D Artist

There is another way:

 

On all 4 sides of your player you define some sort of a collision margin - or collision points to say.

If your player is very tall, you need at least two collision points on the side and one for the top and for the bottom.
If you player is a square box then 4 points for each side in the middle are sufficient enough.

It depends on your player bodies, but i used two points for each side always - so i have more control and can detect "almost falling from edge" and some fancy stuff.

 

In your movement code, you dont use the Position directly anymore and rather use these points to determine which tile each point is intersecting. Each point is defined relative from the absolute position, so when you move, the points move along too.

 

Then for collision:

When you move left for example you calculate the tile position for the top-left and bottom-left collision point.

You loop on the Y top-to-bottom range and determine the solid state of the tile. If any tile is solid, you either prevent the X-movement or calculate a impulse velocity based on the delta of the tile right side. This way it will just touch it when it collides but can leave without problems.

 

For moving right its identical.

For falling or jumping its also identical, but you dont use the Y to loop over but use the X instead.

 

This technique is well known and used in a lof of old 2D platformers, like Sonic for example and works very well if you player is not too fast.

 

But when you have very fast moving players, you need to include a line-segment trace beginning from each collision with the length of the velocity divided by the timestep. Its just a addon to the loop i already explained, you just use the first found tile in the trace direction.

 

Btw. each collision point is just a few pixels away from your players body, otherside it will look weird or you will get stuck - depending on your sprite.

 

If you want to know more: Search for sonic physics guide or sonic collision guide.

collpoints.png

After some research into this, I've found a way that works for me.  I'll post it just in case anyone else would like to possibly use it.  However, I don't think it is the best approach, it may be quite resource intensive.

The system involves creating a one dimensional vector containing tile objects.


std::vector<Tile*> map;

Each tile object contains an SDL_Rect that contains information for the tile's height, width, and x and y positions.  It also contains a tile ID which is used to match up to the tileset I'm using.

Since every tile contains separate information for its position, the map can just be rendered by cycling through the vector and rendering every tile based on what position it has.

As for collision, the same sort of thing is done.  Collision detection involves cycling through every tile in the map in order to see if the player is colliding with a certain tile ID.


bool touchesWall(std::vector<Tile*> map) {
  for (int i = 0; i < map.size(); i++) {
    if (map[i]->getTileID() == 1) {
      if (checkCollision(player_rectangle, map[i]->getTileRectangle())) return true;
    }
  }
  return false;
}

The checkCollision function takes two rectangles and compares them to check whether they are colliding or not.  So in this case, I would check both the player's collision rectangle and the rectangle of whatever tile I'm checking.


bool checkCollision(SDL_Rect a, SDL_Rect b) {
  int leftA, leftB;
  int rightA, rightB;
  int topA, topB;
  int bottomA, bottomB;
  
  leftA = a.x;
  rightA = a.x + a.w;
  topA = a.y;
  bottomA = a.y + a.h;
  
  leftB = b.x;
  rightB = b.x + b.w;
  topB = b.y;
  bottomB = b.y + b.h;
  
  if (bottomA <= topB) return false;
  if (topA >= bottomB) return false;
  if (rightA <= leftB) return false;
  if (leftA >= rightB) return false;
  
  return true;
}

Although this works fine for what I'm trying to do.  I can't help but think about how straining that might be on the processor to be doing every frame.  For such a simple game, I don't want it taking up too much power for each entity on the screen.

For anyone who is wondering, this method comes from the LazyFoo tiling tutorial.

 http://lazyfoo.net/tutorials/SDL/39_tiling/index.php

Advertisement
28 minutes ago, Shinrai said:

For such a simple game, I don't want it taking up too much power for each entity on the screen.

As long as you think the performance is acceptable on the target hardware, it's good enough -- move on to the next task.

The moment it slows down more than you find acceptable on the target hardware, that's when you look into optimizing stuff. In this case, there are some good alternatives in the previous posts, to avoid having to check all tiles all the time (assuming fixed tile sizes), so I'd suggest bookmarking the thread and coming back later on if need be.

Note that, when wanting to optimize your game, never change things by guessing what's slow. Use a profiler and find out where the actual slow parts are, make changes and profile again to prove things are better. Repeat until it's fast enough for you.

Hello to all my stalkers.

1 hour ago, Lactose said:

As long as you think the performance is acceptable on the target hardware, it's good enough -- move on to the next task.

The moment it slows down more than you find acceptable on the target hardware, that's when you look into optimizing stuff. In this case, there are some good alternatives in the previous posts, to avoid having to check all tiles all the time (assuming fixed tile sizes), so I'd suggest bookmarking the thread and coming back later on if need be.

Note that, when wanting to optimize your game, never change things by guessing what's slow. Use a profiler and find out where the actual slow parts are, make changes and profile again to prove things are better. Repeat until it's fast enough for you.

This is very good advice. Unless you absolutely have to optimize in order to have your game run at the targeted specs you can keep your solution and move forward like Lactose suggested. In most games I've worked on you normally make it a practice to never cycle data that doesn't provide any benefit. Examples of this would be rendering map tiles you cannot see, checking collision that isn't possible, running AI code for enemies not on screen, or not within the required distance to act. The example I provided above will not cycle your array, but check at the exact index to confirm the value. I would strongly suggest building these habits early on in game development because it will make you a better programmer overall. On a small project it's not a big deal, but when you're talking about a bigger scale game with hundreds of thousands of lines of code it can be a daunting task to backtrack. Even AAA game studios fail to optimize their games and are unable to release patches for months to fix the issues.

Programmer and 3D Artist

13 hours ago, Shinrai said:

Each tile object contains an SDL_Rect that contains information for the tile's height, width, and x and y positions.

You surely don't need that to be stored - if tiles are uniformly sized, then you can calculate the relevant SDL_Rect whenever you need, just given their position.
 

13 hours ago, Shinrai said:

The checkCollision function takes two rectangles and compares them to check whether they are colliding or not.

You don't need this function: SDL already provides SDL_IntersectRect for this exact purpose.

On 8/1/2017 at 4:16 PM, Lactose said:

As long as you think the performance is acceptable on the target hardware, it's good enough -- move on to the next task.

And I've already reached the limit.  Making a map that has 40 by 36 tiles already slows the game down to a mere 17 frames per second.  I can't blame the processor though, doing 1440 tile checks for 3 entities is 4320 tile checks 60 times every second.  I guess I'll have to look into optimizing the code using another one of these methods.

This topic is closed to new replies.

Advertisement