Lesson 03
Mapping Coordinate to Corresponding Tile and Implementing Wall Collision – SDL2 Tutorial
Summary
Summary of SDL2 Labyrinth Game Lesson
Overview
In this lesson, we continued developing the Labyrinth game using SDL2. The focus was on implementing collision detection so that the player cannot walk through walls.
Key Points
-
Initial Setup:
- The player can move in the game world, but walls need to be defined (white parts) to prevent movement through them (black parts).
-
Background Color Corrected:
- The walkable tile color was changed to black (RGB: 0, 0, 0).
-
Collision Detection Implementation:
- Collision detection logic was added to prevent the player from moving into wall tiles.
- The algorithm first checks if the player's new position would collide with a wall based on the current position and intended movement direction.
-
Coordinate Mapping:
- An essential step included converting the player's position to tile coordinates.
- The player's pixel position is mapped to the corresponding tile ID in a one-dimensional array representing the game map using a derived formula.
-
Tile ID Function:
- A function
map_coordinate_to_tile_ID
was created to convert coordinates into their respective tile indices and IDs.
- A function
-
Wall Checking Logic:
- Added logic to evaluate the next tile's ID when moving in any direction, verifying if it's walkable or not (e.g., wall tiles vs. floor tiles).
-
Refactoring:
- The repetitive nature of the code for different player movements (up, down, left, right) was highlighted, encouraging further refactoring to simplify the logic.
-
Final Touches:
- A clear structure emerged in the code, with specific checks for each direction the player might attempt to move.
- The final implementation maintained the player's actual position separate from the temporary new position used in collision detection.
Conclusion
The lesson concluded with a successful implementation of wall collision detection, reinforcing understanding of coordinate mapping and collision logic. Viewers were encouraged to refactor the code further for efficiency and readability.
Call to Action
The instructor requested support through subscriptions, comments, and notifications to help others interested in the topic and concluded the lesson.
Video Transcript
Welcome everybody to another lesson on SDL2.
We are going to continue our game, the Labyrinth game.
So here I have what we have so far, the player can move.
But we want the white parts to be the wall.
And the black ones to be the floor.
So you can walk on the floor but you cannot go over the wall.
Right now I can go over the wall.
The goal of this lesson is to learn how to make collision detection.
So as soon as I try to move into a wall, it will not let me.
Before doing that, let me fix the background color for the tiles
because I had put 44, 44, 44.
If I go to main.cvp, the line that sets the color of the walkable tile,
I am going to make it 0, 0, 0.
Let me recompile it, g++, main.cvp, lsdl2.
Let me run it again.
Now it is black in the walkable tile.
So what we have to do now is, when the player, this is going to be all about the player.
If the player tries to change the position from into a wall, we are not going to let it.
So where is the code to check that?
We have it right here under the game loop in the sdl pull event.
We have the sdl peel down and we have these four cases of going to the right, down, left and up.
So, see we already have an if statement to check if it goes out of bounds.
That is, it goes off the window, the screen within the window.
We are just going to have to add an extra track to see if it is trying to collide with a wall.
For that, we are going to need to learn about the coordinates to study it a little more.
Here I have this image that I made.
Let's assume we have a very simple grid of three tiles across and three tiles down to simplify everything.
Since each tile has a size of 32 by 32, if I want to reach the second tile in the first row,
that would be at position x equals 32 because we went the length of one tile.
The position y is zero because we start at the top left positioning when we paint.
If I want to get to the last tile of the first row, that would be the length of two tiles, which is 2 times 32 is 64.
Add position 0 for y.
If you want to get to the middle one, first start at x, walk the distance of one tile across, that would be x32.
Then once you are here, walk the distance of one tile down 32.
It becomes 32 comma 32 for the coordinate x, y respectively.
If we are, let's say the player is in this tile here,
and he tries to go down.
Say I input the arrow to go down, but that is the wall.
How am I going to check to see if that is the wall?
Well, the current position is 32-0 because we are in the second tile in the first row.
If I want to go to the second tile in the next row, I increase the y by 32, and that's what's happening.
We are going to do the following.
Get the next position, 32-32.
From that position, let's map that to the actual tile ID that corresponds to the tile 32-32.
In this case, the tile ID would be 1.
Let me put the number 1 here.
To get that, we have an array, remember?
The array of tiles.
Game map tiles.
You have to find a way to map the coordinate to the appropriate element in the array.
In this case, since we have one dimension, this would be, assume it's 9 elements.
0, 1, 2, 3, 4.
The fourth element would be the one we want.
The fourth element will give us the tile ID.
Now, we have to make a function that will translate 32-32 to the fourth element in the array game map tiles.
You can do that easily if you think about the, let's say, if I am in the first row,
if I am in the second row, that means the 32 is y is 32.
Now, if I want to get that, I would count, take that 32, divide by 32, so that means I am one row down.
Now, if you want to do the x, you have to take the x32, divide by 32, which is the tile size,
and you get one.
So that means I am one tile across.
Okay?
Because I am in, I can, because I am in the second line, I can assume that a certain number of tiles have already been run in the previous lines.
Since each line has three tiles, I can say that one, which is the line number in this case,
counting from 0, 32 divided by 32 is 1.
We are at line 1, assuming we count from 0.
And then you add, you actually multiply that with the number across,
3 times 1 plus 1 would be 4.
Let's see that in practice.
I think it's easier to see that as we do it.
So let's go here.
Let's try to make a function to translate.
Let's see.
Function to map the coordinates with the element in a real tile.
So let's say we give the, let's return the index, or rather the ID, right?
Let's return the, so we can first get the coordinate and then later we get the ID.
So let's do ID.
So let's see.
Map coordinate to tile ID.
This function will need, as input, we need to know what the position is that you want to map.
So let's take an SDL rack position and let's take the array of tiles.
I'm going to say intiles.
I could use a pointer notation, or I could do the intiles with the size here being 300.
Because it's 15 columns times 20, yeah, 15, 20 columns times 15 rows, 300.
So we take, let's see, let's start with y, x actually.
Let's do the simple case where suppose you are in 32, 0.
How can I get this tile here, this element?
So let's take position x, let's say it's 32.
Divide that by the tile size, 32.
We get what?
1, right?
So that means I am in the, if you count column from 0, I am column 1, which means column 2, the second column, right?
So that's correct.
So I could use that, right?
So let's take this, make a variable called index, and then I can return tiles at index.
That would work only on the one dimension in the x, right?
From left to right, assuming the game only has one row.
So that would take care of everyone.
If you say that the x is 0, I am here, right?
And 0, position x is 0.
0 divided by 32 is 0, meaning I am at column 0, which is the first column.
So index would be 0.
Tiles at 0 would give me this tile here, which has ID 0 because it's a black tile, and you can walk over.
Now if you do x32, the next tile, 32 divided by 32 is 1.
Column 1 means it's the second column.
Therefore the index here would be 1, right?
And then we return tile sub 1, which would be 0 because it's a walkable tile.
And so on.
When you get to 64, 64 divided by 32 is 2.
When you get to the element sub 2, that means 0, 1, 2, the third element, and that would be tiles at sub 2, would give you 1 wall tile.
Now let's do the y dimension.
You can think of the same way as I did.
Ignore the x, focus only on the y.
Assume the map only has one column and that's the first one, and then that will be generalized to everything as you finish.
Okay, so if you do that, ignore that for now.
Let's do the index.
Assume there's only the y dimension, right?
So we're going to look at position y.
Divide that by 32 and that will give you the position.
Except if you notice here, if I look at 00, that will be position y0, so I divide 32 with 0, I'm incorrect.
Why tile sub 0 is the first one, it's correct.
If I go to the next row as this tile number 3 here, element 3 sub with index 3, I am at what?
I am at 0, 32.
Now position y being 32 divided by 32 is 1.
Index becomes 1, but tiles at index 1 is not exactly here if we have multiple columns.
So we have to add the amount of columns from the preceding lines.
So that means I have to add 1, 2.
If I want to get to 3 and I'm at 0, so we have to add 1, 2, 3, number of columns in the previous line.
So I would have to add 3 for it to be correct.
And the same would apply if I were at the middle tile here.
The middle tile would be 32, 32.
Look at the UI, it's 32 divided by 32 is 1, plus 3 is 4.
So it's correctly mapped to 4 here.
Now where do we get this from?
Well we can use the position x divided by 32 here.
So that would be this previous one.
But you have to multiply by the number of columns.
Okay, so if we think about that, let's see here.
So let's see this one.
If you are in the second row first element, that would be 3.
Let's see if this makes any sense.
So x is 0, so 0 divided by 32 is 0, times 3 is 0.
That would be 0, position y is 32, 32 divided by 32 would be 1.
That's still not exactly it.
Right, so we have to think maybe that's wrong.
Maybe I have to look at this here.
Yeah, I think that's it.
Maybe this side here would go here on the y side.
Assuming we have a 3, we're just considering this 3 by 3, okay?
We're going to change that later.
So if I'm here, I am at 32, I'm 0, 32, right?
Let me put it here so you 0, 32.
If I am at 0, 32.
So 0 would make the second right hand side 0.
32 divided by 32 would be 1, times 3, because there's 3 in the preceding line.
That would be 0, 1, 2, 3. That's correct.
Now if we go to the next one, this one at 32, 32, we would get what?
For the right hand side, it would be 32 divided by 32 is 1.
So we got 1.
So 1 to the right for the column, and then you add position y.
32 divided by 32 is 1, times 3, 3 plus 1 is 4, correct?
Now let's see at the last one, which is 64, 64.
If you look at the position x, I have to walk 1, 2, right?
2 tiles to the right.
That would be what? 64 divided by 32 is 2, correct?
Now we have to walk the number of lines, times the number of columns for each line.
So that would be 64 divided by 32 is 2, times 3, is 6.
So we need to walk 6 tiles previously, and then additional 2.
So 6 plus 2, 8.
So that would correctly map to the last element here.
So that's the formula, okay?
We figure it out.
Now we have to generalize this for any number of grid of tiles at any size.
In the case here, the y, how many columns we have?
We have 64 width of the screen divided by 32.
That would be 20, right?
20 tiles across, so that's why we have 20 here.
If you want to make that more clear, maybe I want to put tiles across in a variable.
Oops, tiles across to make it more clear.
And the other one would be the tile size, right?
Obviously, I think these could go into a constant somewhere if we had a better organized code, maybe in a class.
So we don't have to repeat yourself.
Since our tile size has the width and height of the same size, it's 32 by 32, I can use one variable.
But you could have a tile that has a different width than the height, so you would have to separate these two numbers.
Now we have that.
Let's see.
The next step as you have that map is to finally see if you walk into that tile and you check it's a mall, you prevent the user from moving.
So let's go here.
Let's copy the name of the function map coordinate to tile ID.
Here where you check if the player is allowed to move, you're going to check.
Let's see.
So you're going to try to move and then change the position.
One thing you could do is maybe you allow the player to move to change the position rectangle.
And then after that you check did the user move into a wall.
If the player did move into a wall, you send him back.
That's one way.
The other way is you check it beforehand and do not allow him to move in the first place.
Maybe you can do that one, the ladder.
Don't allow it to go in the first place.
Let's do a one case if you're going to the right.
So you add to 32.
So it's checking if it's still within the screen.
That's great.
And if it's within the screen we're fine.
We can move but we have to check if hitting a wall.
How can we check that?
Well we can map the coordinate to the tile ID and check.
Now what's the first argument?
That would be the rectangle of the position, player position.
The second one would be the array of tiles.
In this case would be game map tiles.
You get the tile ID here.
Let's say next tile ID.
In any check, if the next tile ID is a wall,
well actually not a wall.
You only want to allow the change in position if it's not a wall.
Because your game is so simple.
Or you can also think if the next tile is a walkable tile.
Right now we only have one kind of walkable tile and that's the ID 0.
But maybe your game had multiple tiles, multiple tile IDs that were walkable.
So that's one way to generalize that.
So we could create this function just for the sake of,
in the some day it's going to have more than one kind of walkable tile.
So if is walkable tile the next tile ID, you allow him to change.
Let's make this function.
A bull is walkable tile.
And this will take the tile ID and tile ID.
And we're just going to check if the tile ID is whatever is walkable.
In this case if it's 0 it's walkable.
Otherwise if it's not 0 it just returns false it's not walkable.
You could make these a constant actually.
So if you want to make some kind of enum.
That's one way let's make an enum here.
In the global thing.
Enum, let's see tile type or something.
And the first one would be a floor right walkable.
Second view of wall.
And let's see if that works.
So this would be 0 right?
This would be 1.
So let's go here.
Of course we don't have to have enum but it's better for us to visualize what it really means.
So I can say tile type dot floor.
It was correspond to 0.
Now with that let's check it out.
Let's try.
And I have one problem.
I think my syntax might be wrong.
Expected primary expression before token.
Let me check the syntax for enums in C++.
Refresh my memory.
Cpp enum.
Enum name red, green, blue.
Okay.
Did I make it right?
Color R whatever.
Let's check.
So it's complaining that 92.
Expected primary expression before that token.
Maybe I am confusing while your languages.
Let me see.
Color red.
Okay.
Maybe if I did.
What would happen if I did?
Okay.
So something is happening here.
I could walk over the first but not the second wall there.
Obviously only due to the right.
So we can go from the left and from up.
We can go to the right.
There is a one off error that we have to fix.
As I am going to the right there is one off.
Let's see.
What's going on?
Take that.
Oh I see.
We add 32 but the play position did not change yet.
Not yet.
So I would have to make a new next position.
You have to add 32 to the X.
So that's one problem there.
So let's see.
Can we make that?
We could make some, let's say, a new rectangle for the new position.
And then we can override that into the first one.
That's one way.
So maybe we could do that.
If I make a new SDL rectangle here.
New position.
New player position.
Let's initialize that to player.
So I have to copy it.
I don't know if there is a copy constructor here.
I wonder if that works.
Let me just check.
Player position.
Otherwise I have to do one each one by one.
Let me check.
I doubt it works.
I don't know.
C++ you have to do everything by hand.
Let me check.
I want to do this and I want to SDC out.
New position.
I don't even know if that would work.
Not declared.
Oh it's wreck.
Sorry.
Cannot convert new position to type.
Yeah right.
So it doesn't work.
So I have to do this.
One by one.
Wow.
XYWH.
To the right.
Zero zero 32 32.
Zero 32.
Wait.
XY.
This is what I'm interested in.
32 zero.
Is that correct?
We are in 32.
That's the previous position.
Let's see.
Previous position would be zero 32, zero.
That's right.
Okay so it's actually copying.
Now if I do, I want to take this and put it here at the end.
And I'm going to assign the player position.
New position.
And with that, I want to change the new position here.
New position.
Dot ax equals new.
Let me see if that works.
And then here would be, actually wouldn't be, actually I'm changing.
Let's stop a little bit here.
I'm changing new position.
I'm modifying it.
Even I'm doing optimistically thinking that he can move but that's not actually good.
So maybe I want to backtrack and simplify this and keep it simple.
Without we keep it simple here.
Let's keep it simple for now.
Let me backtrack this.
Sorry about that.
I want to do this.
We're going to do a similar thing.
I don't want to do inside here.
This will not change the player position plus 32 yet.
So we send the old position instead of the new one here.
So let's say he's within the bounds of the screen.
And let's say that you want to take the SDL rack, a new position.
Let's take that as player position here and then you take new position dot ax equals what?
New position dot ax plus 32.
And then you actually change it and you pass it along.
And then you get the tile ID.
If it's blockable you can actually change the player position.
Let's see.
I'm trying to go to the right.
It doesn't go.
I can go down left.
I can go from left.
That's perfectly fine but I cannot go back to the right.
So that's great.
So we do the same thing now.
So that worked with this temporary variable here.
Essentially what I was trying to do before is just factor this out.
Because I already know it would be repeated in every case.
So we can do the going down.
Let's add a, in the third line first column, let's add a wall there.
So I'm going to the game map and the third line first column, I'm going to add a wall.
And if I do that, we're going to get it there.
Oops.
I have now it here.
Okay, I want to change that so we cannot walk through.
So if we are within screen bounds, we're going to make the new position, play position, copy that.
Then we say new position, y equals new position, y plus 32 going down.
And then you get the next tile ID.
Same thing, into next.
Tile ID is going to be map coordinate to tile ID.
So we take the coordinate, new position, and map to the element in the game map tile as a ray.
And then you check, is it a walkable tile?
If it is walkable tile, the next tile ID, we can proceed to change the position.
Let's try it out.
So I'm trying to go in down, it doesn't go down.
I'm pressing the down arrow, it won't go.
Do the same thing for the others.
How about that?
You do that.
Positive video and do that.
Okay, let's go.
Same thing, can just pretty much copy and paste similar pattern.
So if we're at the screen bounds, make the XDR rack for the new position.
Copy from the play position.
The new position, dot X, is going to be new position, dot X minus 32 going to the left.
Then check the, make a variable to start a tile ID.
The next one, map the coordinate to tile ID of the new position.
Give map tiles.
Check if it's walkable tile, the next tile ID.
If so, you can succeed, you can move this up here to within the with statement.
Let's test.
So we have to try to go to the left, so I'm going to go around and try to go to the left.
I'm pressing the left arrow, it won't go because it's a wall.
Okay, now you can do the same thing.
I'm just going to copy and paste because I'm tired already.
So I'm going to copy this, paste it here and change things.
So this is going to be Y, Y minus 32.
Next tile ID, same thing.
Walkable tile and then Y, Y minus 32 here.
And then you remove that.
So try going up, let's add, actually that's fine.
I can go around going down and I'm going to try to go up, up, press up, I press up, it won't go.
Now I'm trying to go around, try to go around, it won't go around the wall now.
Okay, I'm trying to go through the wall, it won't let me through in all directions.
Up, down, right and left.
Okay, so that's it.
We implemented collision.
Now the code is very repetitive, so that's why in the beginning I wanted to factor out all these things.
So maybe let's try doing that now.
I could take the rect out.
Let's see, if I put this here.
Okay.
And then remove from the orders, go down, left and up.
Very good.
Now I have one single variable.
And then let's try to see if it didn't break anything.
And it went out.
Up, left, down, up, right.
Okay, fine.
Now can we do better?
Next style ID.
So one.
We're declaring a variable every time we could pull that out.
That's one way.
Or, let's see.
The thing is we have to map to the new position.
The only way to get the new position is to after changing it, after checking if it's within bounds.
So maybe we still got to keep that.
Got to think a little bit more about that.
Of course you can always make some sort of loop and loop through the different things, but I don't recommend that.
Maybe it's better to leave it this way.
My concern pulling this out is there's an if, else, if, else, if, as if.
Assuming there's only those kinds of commands, and I'm not using this variable anywhere else,
I could pull it out, but there's no ifs case to set it.
So I could maybe do that.
Let's do, if I pull it out here.
Style ID.
I can just declare and just set it here.
So I only have one.
I don't know if it would be good in the long term, in terms of isolating the logic for each of these parts.
Actually, I wanted to isolate this whole thing because I want to handle only the player input.
So maybe you can do that as well, because everything is here in the game loop.
I want to move everything out.
So let's take, try to take this out.
And I took this out, and I call this function to, let's say, handle player input.
Let's say we have that function, and we call it, and we make it here, outside.
Let's say void handle player input.
And I paste that code here.
What would be necessary for it to work?
We have the rect.
We need a player position.
We don't have locally, so maybe we want to get that in a variable.
So player position is in the argument here.
So I have to pass that down because of this guy.
So we pass it here.
Now we want to change it, right?
We actually want to change the position.
So it's something that has to be mutated.
In order to do that, I have to pass, I don't know.
I think structures are always passed by reference to C++, is that right?
Cpp structure passed, always passed by reference.
I do not remember, or if it's by value.
R, this is C.
Well, let's try actually.
I don't know, let's try without, let's pass by value and see if it actually updates the position here,
on not just in the local scope of the function.
So we got that.
And let me close this.
Back.
So we have position, we don't have window width nor height.
Okay, so that's the constant here.
Maybe I want to pull this out in a global scope.
Let me pull it out.
I'm going to put a global scope so it can be used by any function.
And then this is fine event.
Now I don't have event.
So maybe I want to give the event here.
So event, we're going to add the types later, don't worry.
So we have to pass this.
So event here.
Do I worry about changing the event here?
I don't think so because I'm just accessing something so it could be,
think event, is it a structure?
So that's fine.
Player position, new position, map coordinates.
Oh, I don't have game map tiles.
Something have to pass.
So let's put game map tiles here.
And let's pass game map tiles here.
And I'm just going through every variable we need.
Fine, then the same thing repeats, repeats.
I think we're good.
Let's try it out.
I have to give the types here.
So this would be SDL event.
The second one would be SDL rack type of the player position.
And game map tiles is an array of integers.
So int, game map tiles, I could have put pointer or I could put here,
I have to know the size exactly.
That would be 300, right? 15 times 20.
Let's see.
No, lots of problems.
I put a P here, game P. That's the typo.
Okay.
Nothing is changing as I press the arrows precisely because of what I mentioned before
about passing by value versus by reference.
So I think this has to change, right?
So maybe if I did this, what would have happened if it's passed by reference?
Now it's working.
All right.
So it's working fine.
Doesn't go out of bounds.
And it doesn't go through all walls.
So I think we're good for this lesson.
Let's review everything we did.
So the goal was to implement wall collision, right?
We don't want to allow the player to go through these walls, these white tiles.
The way we did that was first we had to find a way to map the position coordinates to the actual element that holds the tile ID, right?
We have an array of game map tiles.
And each element holds the tile ID.
Zero means it's a walkable tile.
One means it's a wall tile that will not let you walk through.
So this function that we made, map coordinate to tile ID, takes the position that you're trying to move into and takes the array of tiles.
And it will map that position based on this formula here that we figured out.
Okay.
You take position y divided by tile size times the tiles across, plus the position x divided by tile size.
If you add this up, you get the index of the element that you want.
And then from that index, you can translate that to the tile ID in the array of tiles.
Remember that?
And then we looked at if we try to move into that, how can we make the collision?
Well, we have the parts where we look at the input.
If it's within bounds, we can go ahead and we add 32 to the new position, in this case to the x, right?
We try to go make it go to the new position and we check if that's a valid position.
If so, if it's a walkable tile, we made this function just simply check if the tile ID is zero, meaning I put an enum here, right?
Tile type, floor.
Then you can walk through and then you can go ahead and change the player position.
So I kept the new position and the actual player position separate to be able to pass the new position to map coordinate to tile ID.
And then do the same thing here and here and here.
And I leave it as an exercise for you if you can find a better way to refactor this and make it less repetitive.
But for now, leave it like that. It just works.
We isolated that into a handle player input function, moved some things around to be able to make this possible.
I put a reference to direct because player position needs to actually change.
So we learned that this was being passed by value.
And because of that, it was only changed locally in this function, but not outside from the call lead.
So this reference here will ensure that the variable that's passed in, right?
So the value passed in will actually be the changes to that will be reflected in the call lead scope.
Alright, so I think that's it for now.
Please support our channel, subscribe, leave a comment, turn on notifications.
And that helps us a lot and let other people know if they are interested in the topic.
And I'll see you in the next one. Goodbye.
No comments yet (loading...)
No comments yet (loading...)
Did you like the lesson? 😆👍
Consider a donation to support our work: