Discord
Login
Community
DARK THEME

Map Collisions

I've reviewed the other post labeled "Maps and map collisions" several times to try and understand the sequence of how I'm supposed to get the collision but I'm kinda lost. If someone could help break it down further if possible, I'd appreciate it

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!

Thank you soooooooooo much for this! Once I finish my project I'll make sure that I put the link to here in the description. This is awesome!

Where do find out about this "block" property of maps, I do not see this in the documentation?

Wow, that's amazing! I've never seen such a detailed explanation... but I have a question. For my RPG games, I mainly use an array map. Is that a good method?

The map.block property isn't actually in the documentation, as I added it myself as I mentioned at the beginning of the explanation; although I'm not sure if this is the correct way to do it, I do it this way to avoid having loose variables later, but you can easily create a global variable called map_block and use it as the block size for your map

Regarding using arrays, it could work, since the logic is the same. A map actually works using an array; if you check its properties, you can find the array it uses. If you use an array, you could add several properties to a block, although it would have some problems:

  • Editing anything in the array would be complicated since you wouldn't have an editor like the one in MicroStudio
  • Drawing it would be more complicated, since drawing sprite by sprite would be more expensive than drawing a map. But I'm not entirely sure about that; I'd need to know more about how the map works

Here's an example: I'm using the number one to create walls, so if I understand correctly, enlarging the map will no longer make collision work?

clamp = function(value, lower_limit, upper_limit)
  local val = max(value, lower_limit)
  val = min(val, upper_limit)
  return val
end

init = function()
  map = [
    [0,0,0,0],
    [0,0,0,0],
    [0,0,1,0],
    [0,0,0,0]
  ]
  
  map_Pr = object
    x = 0
    y = 0
    case = 10
    color = "rgb(0,197,66)"
  end
  
  player = object
    x = -15
    y = -15
    taille = 10
    color = "rgb(255,170,0)"
  end
end

update = function()
  local oldX = player.x
  local oldY = player.y
  
  if keyboard.press.UP then player.y += map_Pr.case
  elsif keyboard.press.RIGHT then player.x += map_Pr.case
  elsif keyboard.press.DOWN then player.y -= map_Pr.case
  elsif keyboard.press.LEFT then player.x -= map_Pr.case
  end
  
  local j = floor((player.x + 15) / 10)
  local i = floor((player.y + 15) / 10)
  
  if i < 0 or i > 3 or j < 0 or j > 3 then
    player.x = oldX
    player.y = oldY
  elsif map[i][j] == 1 then
    player.x = oldX
    player.y = oldY
  end
  
  player.x = clamp(player.x, -15, 15)
  player.y = clamp(player.y, -15, 15)
end

draw = function()
  screen.clear("rgb(0,170,255)")
  
  for i = 0 to 3
    for j = 0 to 3
      local px = j * 10 - 15
      local py = i * 10 - 15
      local c = map_Pr.color
      if map[i][j] == 1 then c = "black" end
      
      screen.fillRect(px, py, 10, 10, c)
      screen.drawRect(px, py, 10, 10, "rgb(0,105,35,0.2)")
    end
  end
  
  screen.fillRect(player.x, player.y, player.taille, player.taille, player.color)
  screen.drawRect(player.x, player.y, player.taille, player.taille, "rgb(197,131,0)")
end

Also, if I want to create trees, I can't just give the map length and tell the code to generate a random number generator, because you're right, placing each sprite is time-consuming and tedious. Thank you for your reply.

I was looking into it, and it's sort of true. Since you have a section that indicates the map limits, specifically i<0 or i>3, expanding the array will cause the character to become stuck upon reaching the limit. The best solution is to use variables in that case, using the lengths of your array instead of 3

Also, for the collision detection you're looking for, I'd recommend using the player's primary coordinates from your map's coordinate system (or, in your case, the array's) to avoid complications later. Otherwise, you'd have to transform coordinates multiple times to convert from one system to another

Regarding the trees, yes, it's somewhat of an issue. I'm referring more to the lag they can cause. If you try to draw several trees (like 500), it can lead to FPS drops. So, you could implement a system that only uses the trees visible on the screen. However, for the map (or your array), you could also use an image. Instead of two loops in the draw function, you could use an image where you draw all your blocks initially, and then modify that image whenever you want to change a block

But to summarize further, you just need to use array boundaries as variables instead of numbers, so you can have arrays of different sizes, and the same goes for the size of each block in the array

I was actually testing out this map thing and made this test project, with the same way of moving around and all that, in case you want to check it out

https://microstudio.dev/i/A198_/slime/

That's awesomw, thank you for your response and your example, I'm truly grateful

Post a reply

Progress

Status

Preview
Cancel
Post
Validate your e-mail address to participate in the community