Well, I didn't see much of the Meowys Adventure project, but I still did something similar by looking at some of the code:
https://microstudio.dev/i/A198_/tilemap/
In summary, you must first keep in mind that your game's coordinate system is different from your map's coordinate system:
| Game coordinate system |
Map coordinate system |
| It's the one you use to draw sprites, images... |
It's the one used to place blocks in the map creator |
| You can use decimal values |
You can only use integer values (0,1,2,..) |
| You can use negative values |
You cannot use negative values |
So now the idea is to find a way to connect these coordinate systems. To do this, you need to establish how many game units one unit on the map will be (that is, one block, keeping in mind that the blocks on your map are squares, with equal width and height).
Then we'll set the map's block property, which will indicate the value we're looking for:
map = maps["my_map"]
map.block = 10
It's like saying, every 20 units in the game, there are 2 blocks on the map, or every 50 units in the game, there are 5 blocks on the map.
Now that we know the size of each block in the game, we can determine the total map size and draw it. However, before drawing, keep in mind that negative coordinates cannot be used on the map. Therefore, you have two options:
- Draw the map centered (0,0). This means that whenever you want to use the game's coordinates on the map, you'll have to add a value like
map.width*map.block/2 to compensate and prevent it from being negative (This is the method used by most people)
- Draw the map in the positive quadrant of your game's coordinate system. This will prevent you from having to add the value used in the other option (This is the method I will use)
So, to draw the map in the positive quadrant, it will be done as follows:
screen.drawMap(
map,
(map.width-1) * map.block/2, // x
(map.height-1) * map.block/2, // y
map.width * map.block, // width
map.height * map.block // height
)
The problem with doing it this way is that the map will only be drawn in one corner, but with a screen.setTranslation(-camera.x,-camera.y) and placing the character only in positive coordinates, it's fixed
Now let's do a quick example of how to switch from the game's coordinate system to the map's coordinate system, and vice versa.
To convert game coordinates to map coordinates, you must first divide them by the block size, and then round:
map_x = round(x/map.block)
map_y = round(y/map.block)
So if something is at coordinates (15.5, 20.2) in the game system, then it will be at coordinates (1,2) in the map system, In other words, it is within the block (1,2)
Now we'll do the reverse: find the location of that block in the game's coordinates, not the map's. To do this, multiply the previously obtained coordinates by the size of each block:
block_x = round(x/map.block)*map.block
block_y = round(y/map.block)*map.block
This will be useful for highlighting a specific block at certain coordinates, as shown in the example in the following script:
map = maps["map"]
map.block = 10
update = function()
local b = map.block
block_x = round(mouse.x/b)*b
block_y = round(mouse.y/b)*b
end
draw = function()
screen.clear()
local w = map.width
local h = map.height
local b = map.block
screen.drawMap(
map,
(w-1) * b/2, // x
(h-1) * b/2, // y
w * b, // width
h * b // height
)
screen.fillRect(block_x,block_y,10,10,"rgb(255,0,0)")
end
Now, let's talk about collisions. There are different ways to handle collisions, but I'll use the simplest one. To do this, we'll create a class that will be the type of object that respects collisions with the map.
This will have its speeds and dimensions in X and Y, preferably the size of the block:
Body = class
constructor = function(x,y,w=10,h=10)
this.x = x
this.y = y
this.vx = 0
this.vy = 0
this.w = w // width
this.h = h // height
end
update = function()
x += vx
y += vy
end
end
The idea behind collision detection is as follows:
- Imagine the character as a rectangle. If you want it to collide with a wall, you would need to determine which block it will be on in the future
- Also, in certain games, characters can hang from a corner of a block, so from that corner of the character, check if there is a block underneath
If this isn't clear, let's look at an example. Let's say your character has a certain velocity in the X direction (vx). Before applying x += vx, you check if at x+vx it is where a block is located. If it is, then it can only move until it is next to the block, and vx = 0. You will also have to check if it collides with corners in the Y direction. For example, if it tries to pass through an opening between two blocks, if it goes slightly higher, it will collide with the block above; if it goes slightly lower, it will collide with the block below; but if it stays in the middle, it won't collide with anything.
In short, it would be something like this:
local e = 1 // This is a value that I will explain later...
local block = map.block
// If you move to the right
if vx > 0 then
// You get the coordinates of the block that will be on the right
local right = round((x+vx+w/2)/block)
// You get the blocks located in the top and bottom right corners
local top = map.get(
right, // x
round((y+(h-e)/2)/block) // y
)
local bottom = map.get(
right, // x
round((y-(h-e)/2)/block) // y
)
if top or bottom then
/*
If there is something to the right (either in the top or bottom corner)
then position the character next to the block and also stop its speed
*/
x = right*block - (block/2 + w/2)
vx = 0
else
x += vx
end
// If you move to the left
elsif vx < 0 then
// ... It's technically the same as above, only certain signs change (+ -)
end
In the example, right would be the X coordinate of the blocks (those at the corners) that the character will collide with, while top and bottom are the Y coordinates of those blocks respectively. If it collides, then it will stop relative to X
The dimensions (width and length) of the character are also taken into account, that's why w/2 is used, to know that it is located to its right
While the variable e it's a value whose purpose I honestly don't know; I just saw it as necessary. It's like it prevents the character from getting stuck if they try to find perfect values. If the value is high, they'll be able to pass through certain things, but if it's very low (close to 0), it will be more difficult for them to get through holes. But in short, as long as it's not equal to 0, everything's fine :)
And for the left, it would be the same, only with the signs reversed. To simplify all this, you would need a function called sgn, which will indicate whether vx is positive or negative:
sgn = function(a)
if a >= 0 then 1 else -1 end
end
The simplified version for both directions (left and right) would look like this:
local e = 1
local block = map.block
if vx != 0 then
local gx = round((x+vx+ sgn(vx)*w/2 )/block)
local top = map.get(gx,round((y+(h-e)/2)/block))
local bottom = map.get(gx,round((y-(h-e)/2)/block))
if top or bottom then
x = gx*block - sgn(vx)*(block+w)/2
vx = 0
else
x += vx
end
end
There's also a problem where the character is larger than a block, and instead of entering an opening, it tries to collide with a block in the middle. To fix this, besides checking which blocks are in the corners, you also check which block is in the middle:
// ...
local top = map.get(gx,round((y+(h-e)/2)/block))
local bottom = map.get(gx,round((y-(h-e)/2)/block))
local middle = map.get(gx,round(y/block))
if top or bottom or middle then
// ...
else
x += vx
end
And that would be it. Now, the next step is to do the same thing but swapping what's in the X direction with what's in the Y direction. I won't put it here, but in the project I made (the link I showed at the beginning) it shows how it's finished, although there are some different variable names, so I'll correct that so it looks the same as in the example here... The project can also be used to determine where to place everything I've explained here, and it also has certain built-in functions to help you take advantage of situations where a character is on the ground or colliding with a wall
Lmao, I can't believe this took me hours to write; well, now whenever someone asks me how map collisions work, I'll just send them the link to this page MUAHAHAHA! MUAHAHAHA!
