Building a tilemap in AGK (Part 2)
Getting Started
This is a continuation of the previous tutorial and will build upon that existing structure. If you haven't already read it you should do so before continuing here.
Building a tilemap in AGK (Part 1)
The goal is to show you how to handle tiles of different types for collision and also how to improve the efficiency of very large maps. We'll start with the efficiency first.
Handling Very Large Maps
In the previous tutorial I mention large maps, that is a map larger than the screen. What will be discussed here is much much larger. For instance, imagine the overworld in
Legend of Zelda on the original NES. It's 256 x 88 tiles overall with a single screen showing only 16 x 11 tiles. In other words, you'll only have 176 sprites shown at a time
with a total of 22,528 sprites making up the entire map. That means you have 128 times more sprites NOT being used. Seems pretty wasteful, even if those sprites are clones they
still take up memory. What we can do is create an array to fit the size of the visible screen, the viewport, and only create enough sprites to fit that space. Plus one. You'll
see why shortly you need one extra row and column in the viewport.
We don't need a map of that size to demonstrate this technique, we'll work with existing map from the previous tutorial, map2.txt. First thing to do is
add a new variable to the UDT_Map structure. Call it view and define it as a 2D integer, similar to how you defined the data array. You'll also need two new variables to store
the viewport's width and height (as integers); viewWidth and viewHeight. You can also remove the 'spr' variable from the UDT_Tile structure since sprites will no longer be apart
of that data. Next we'll need to calculate the viewport dimensions and modify the sprite creation loop.
Just to make sure we're on the same page, your UDT_Map should now look like this:
Type UDT_Map
width as integer
height as integer
tileSize as integer
view as integer[0,0]
data as UDT_Tile[0,0]
x as float
y as float
maxX as integer
maxY as integer
viewWidth as integer
viewHeight as integer
EndType
Calculate the viewport dimensions and size the array accordingly. The reason for the + 0.0 is simply to ensure the division results in a float and doesn't truncate the decimal
value. In our example, the screen is 640px wide with a 48px tile. This results is 13.333 and we can't display 1/3 of a tile so we use ceil() to round up. We then add +1 for a total of
15 tiles across the screen. Vertically we end up with 11. Move the map over by 24px, or half a tile's width. If we only had 14 tiles to display, the last column would not reach the endge
of the screen. We need that extra tile at the end in each dimension for displaying partial tiles.
// Calculate number of tiles that can be shown on screen
map.viewWidth = ceil((getVirtualWidth()+0.0) / map.tileSize) + 1
map.viewHeight = ceil((getVirtualheight()+0.0) / map.tileSize) + 1
map.view.length = map.viewWidth
for x = 0 to map.viewWidth-1
map.view[x].length = map.viewHeight
next x
To help you visualize this, study the graphic below. Imagine the yellow rectangle as the viewport (the screen resolution). In this case we can only fit 3.3 tiles within our view.
As the tiles scroll to the left you can see how we can have a fraction of the first tile display but the last tile leaves a small gap from the edge of the screen. The red square shows that extra tile which is used to cover that bit.
Scrolling The Very Large Map
The next big change to our map code is how the map is positioned. Before, we simply offset all the tiles by the scroll amount. But since we only created enough sprites to cover the viewport
display and not the entire map, we can't do that anymore. With our tiles being 48px, that is the most we scroll the viewport. Once the first column is completely off screen, we shift everything
back but update the sprite frames to reflect other tiles in the map data.
As the view scrolls over it'll reposition back once a full tile goes off screen and updates the contents of those tiles as if the full map has shift. The user will never notice. So, how do
we dothis? The variables which track the map's scrolling offset stay the same as we'll use those values to point to the correct data. Those offset values get wrapped to constrain to the
tile's dimensions. In other words, if the map has scrolled 235 pixels we'll want that to wrap to a value between 0 and 47 (48px). We can do this very easily by using modulus, which gives the
fractional remainder of a division. So offset / tilesize (235 / 48) results in 4.89. You can then take the integer part of 4 and multiply that by 48 resulting in 192 and subtract that from
the 235 to end up with 43. Or take the fractional part of .89 (approximate) and multiple it by 48 to end up with 42.72 (again the fractional part is approximate in this example). Or keep it
simple and just use mod(235, 48) to get 43. What that division means is, if you scroll the map 235 pixels then the first 4.89 tiles are now off the screen. The fraction tells us the 5th tile
is 89% off screen but 11% is still visible, thus our first column in the viewport should point to column 5 in our map data. Here are the new offset values for positioning the viewport tiles:
ox = x*m.tileSize - mod(m.x, m.tileSize)
oy = y*m.tileSize - mod(m.y, m.tileSize)
And here are the offsets to point to the correct map data according to the scrolled amount.
wx = floor(m.x / m.tileSize)
wy = floor(m.y / m.tileSize)
When you scroll to the endge of the map there's a chance of the offsets trying to look beyond the map's data range, so we need to check those boundaries to avoid any possible out of range
errors on the array indices.
function setMapPosition(mx, my, m ref as UDT_Map)
m.x = mx
m.y = my
// Boundary checks to avoid scrolling beyond the map
if m.x < 0 then m.x = 0
if m.y < 0 then m.y = 0
if m.x > m.maxX then m.x = m.maxX
if m.y > m.maxY then m.y = m.maxY
for y = 0 to m.viewHeight-1
for x = 0 to m.viewWidth-1
// Local offsets for viewport tiles
ox = x*m.tileSize - mod(m.x, m.tileSize)
oy = y*m.tileSize - mod(m.y, m.tileSize)
// World offsets for map data tiles
wx = floor(m.x / m.tileSize)
wy = floor(m.y / m.tileSize)
// Boundary check for edge cases
if x+wx < m.data.length and y+wy < m.data[x+wx].length
// Update viewport tiles with correct tile information
setSpriteFrame(m.view[x,y], m.data[x+wx, y+wy].id)
endif
// Position viewport tiles
setSpritePosition(m.view[x, y], ox, oy)
next
next y
endfunction
Now you have a scrolling tile map that'll use the same number of sprites regardless of how big your map gets. The larger your world the bigger impact this will have.
This is the same technique I used when I built my Legend of Zelda clone and the export to HTML ran very well in a browser.
Character Movement
Let's get a character to move around the map. Keeping it simple, we'll create an empty sprite to represet "guy". I'm also going to create another UDT to store player data for the character. It'll contain the player's position, speed, and sprite ID.
Type UDT_Guy
x as float
y as float
spr as integer
speed as float
EndType
// Create a periwinkle-colored 32x32 sprite
guy.spr = createSprite(0)
setSpriteSize(guy.spr, 32, 32)
setSpriteColor(guy.spr, 128,128, 255, 255)
// How fast the character can move
guy.speed = 5
// Starting position, center of screen
guy.x = 320
guy.y = 240
First things first, update the movement controls. Instead of calling setMapPosition(), update the player's X,Y coords by the player's speed. Then set up some
boundaries to keep the player from moving off screen.
// Boundaries to keep guy on the screen.
if guy.x < 0 then guy.x = 0
if guy.x > getVirtualWidth()-getSpriteWidth(guy.spr) then guy.x = getVirtualWidth()-getSpriteWidth(guy.spr)
if guy.y < 0 then guy.y = 0
if guy.y > getVirtualHeight()-getSpriteHeight(guy.spr) then guy.y = getVirtualHeight()-getSpriteHeight(guy.spr)
Keep in mind, the origin of a sprite is the top left corner. That is why you see the screen's edges subtracted by the width and height of the character. These may be
different when you build you game. It all depends on what you determine to be the edge limits of your character.The next part is where the fun comes in. Assign a value to a variable called edgeBuffer. This will be the distance allowed from the screen edges the player can get before the map scrolls. If you want to hit the edges of the screen before moving, set it to 0. In my experience 0 is a bad choice as the player will move blindly without any chance to see what's in front of it. Setting the value to 48 means the player will always see at least 1 full tile in front of it. You can set it to 100 or more if you like, it's totally a personal preference. If you wish your player to always stay in the middle of the screen to where the map is always moving instead, you'll need to break the buffer variable into two separate values for the width and height of the screen. Unless you're a weirdo with a 1:1 aspect ratio square screen.
For example on a 640x480 screen, you'd set the X-buffer to 304 ([640-player.width]/2) and the Y-buffer to 224 ([480-player.width]/2).
Now we'll check when player hits edge buffer to move the map.
if guy.x < edgeBuffer
setMapPosition(map.x-guy.speed, map.y, map)
if map.x > 0 then guy.x = edgeBuffer
endif
if guy.x > getVirtualWidth()-getSpriteWidth(guy.spr)-edgeBuffer
setMapPosition(map.x+guy.speed, map.y, map)
if map.x < map.maxX then guy.x = getVirtualWidth()-getSpriteWidth(guy.spr)-edgeBuffer
endif
if guy.y < edgeBuffer
setMapPosition(map.x, map.y-guy.speed, map)
if map.y > 0 then guy.y = edgeBuffer
endif
if guy.y > getVirtualHeight()-getSpriteHeight(guy.spr)-edgeBuffer
setMapPosition(map.x, map.y+guy.speed, map)
if map.y < map.maxY then guy.y = getVirtualHeight()-getSpriteHeight(guy.spr)-edgeBuffer
endif
There's more than one thing going on here. Starting at the top, check if the player has hit the buffer area on the left side of the screen. If it has, move the map by the
same speed as the player. We also want to keep the player in that buffer area for as long as the map is able to scroll. As long as map.x is above 0 that means there's room
to move back to the left. Therefore, fix the character's position at the edge of the buffer area. If the map cannot scroll any farther in that direction then the character
is positioned as normal and it's other boundary checks above take over. This does two things for us. It keeps the player at the specified distance from the edge of the screen
while triggering the map scrolling yet it will allow the player to walk to the very edge of the screen when the map can no longer scroll in that direction. After the position
checks and map positioning, update the character's sprite position.If you've written everything correctly, you should have a character that you can now walk around your map, pushing the viewport as needed.
Helper Functions & Routines
Before tackling collision, I want to show you a few helper functions that will, well, help. The first is how to translate screen coordinates into world coordinates. What I mean be that is, your character will always be within the dimensions of the screen but your overall map may be 4000x5000 pixels. So how do we translate a point at [125,200] on the screen to something like [1250, 1845]? That's simple, add the map's offsets (x and y) to the local screen coordinates you want to translate. In this example, the map's offset would be [1125,1645]. If you want that in grid coordinates, simply divide each value by the tile size and trunc the decimal (floor it).
// Returns the X tile number in map 'M' given screen coordinate 'x'
function getWorldX(x, m ref as UDT_Map)
tx = floor((x + m.x) / m.tileSize)
endfunction tx
Add that function and repeat the process to create getWorldY() as well. If it's not already apparent as to why you need this information, to properly do collision we'll need to
know where the player is in terms of the world.
Collision
Collision can be as simple or as complex as you want. Some of this will depend on the style of game you create and how map data is stored. I will cover one basic scenario which should give you a foundation to build on and customize to your needs. Going back to an illustration from part 1 of the tutorial we'll need to determine which tiles are passable and which of those are impassable.How could your collision looks to the player can also depend on the artwork itself. Look at tile 6, it's half water half grass. Should we make it passable or not? If we do, it'll feel like the player can't get very close to the water. If we allow the player to walk on it then it appears to partially walk into the water. In this example we'll make two tiles impassable, 5 and 7 only. The tileset has 20 tiles, so on the first line of the map file we need to add 20 digits comprising of 0 and 1. 0 means the tile is passable, 1 is impassable. For this example, everything is 0 except for the 5th and 7th position. Below you'll find map3.txt.
16,16,48,tileset.png,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,5, 1, 2, 3,4,4,4,4, 1, 2, 2, 3,4,4
4,4,4, 6, 7, 8,4,4,4,4, 6, 7, 7, 8,4,4
4,4,4,11,12,13,4,4,4,4, 6, 7, 7, 8,4,5
4,4,4, 4, 4, 4,4,4,4,4, 6, 7, 7, 8,4,4
4,4,4, 4, 4, 4,4,4,4,4,11,12,12,13,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 5, 4,4,4,4,4, 4, 5, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,5,4,4, 4, 4, 4, 4,5,4
4,4,4, 5, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 5, 4, 4, 4,4,4
4,4,4, 4, 4, 4,4,4,4,4, 4, 4, 4, 4,4,4
Update the UDT_Map structure to include an array of integers, call it coll. Now to update the file loading. As a the collision data comes after the tileset image name
and we know that's the 4th token in that string, we can use countStringToken2() to find out how many remaining tokens are left on that line. Resize the coll array and
fill in the data.
// Get number of tiles
total = countStringTokens2(r$, ",") - 4
map.coll.length = total
// Because the animation frames start at 1 and not 0,
// in this case we'll also start the array at 1. It's
// only used as a lookup for collision this'll keep simple.
for i = 1 to map.coll.length
map.coll[i] = val(getStringToken2(r$, ",", i+4))
next i
Place that code just below where you read the image filename. Next, let's take a look at a simple collision check. The following function
has 3 parameters; a set of screen coordinates and the map. Step 1 is to relate those screen coordinates to a world tile followed by a simple
boundary check to avoid an out of bounds error. Step 2, get the tile at that location. Remember what we actually stored in the tile's ID field
is the animation frame number. Use that to lookup in the collision array. Remember a 0 means we can pass through it, 1 will block movement.
function checkPoint(cx, cy, m ref as UDT_Map)
// Get this point in world coordinates
tx = getWorldX(cx, m)
ty = getWorldY(cy, m)
if tx < 0 or ty < 0 or tx > m.width-1 or ty > m.height-1 then exitfunction 0
// Determine the tile in which this point lies
frame = m.data[tx,ty].id
// Return 0 or 1, if the tile is passable or not.
r = m.coll[frame]
// Strictly for showing the collision with tiles
if r = 1
bx = (tx*m.tileSize) - m.x
by = (ty*m.tileSize) - m.y
drawBox(bx, by, bx+m.tileSize, by+m.tileSize, yellow, yellow, yellow, yellow, 0)
endif
endfunction r
The last piece of code is not necessary but was added in for this example. If collision is made with a tile during player movement, it will outline
that tile with a yellow box. This is simply to help you visualize what is going on. You'll also see shortly you can collide with more than one tile
at a time.We need to adjust how the character moves as we'll want to check collision before actually updating the position. The character is more than a single point, it's a box. You can implement a pixel-perfect collision system and I do have an example of how to do that but it won't be covered here as it's outside the scope. For now, we'll keep collision simple and define the player as a simple box. Each corner of the box must be checked for collision, that's why we wrote the checkPoint() function above.
function moveGuy(g ref as UDT_Guy, vx, vy, m ref as UDT_Map)
w = getSpriteWidth(g.spr)
h = getSpriteHeight(g.spr)
a = checkPoint(g.x+vx, g.y+vy, m) // top left
b = checkPoint(g.x+vx+w, g.y+vy, m) // top right
c = checkPoint(g.x+vx, g.y+vy+h, m) // bottom left
d = checkPoint(g.x+vx+w, g.y+vy+h, m) // bottom right
if a+b+c+d = 0
g.x = g.x + vx
g.y = g.y + vy
endif
endfunction
The code at this point should be self explanatory as most of the work happens inside checkPoint(). Pass the guy (player data) and map to the function
as well as the amount to move the player along X and Y (velocity vectors). We check all four corners of the player and
if all four return 0 then no collisions happened and we can safely update the player's position. As the position is updated within the function it's
important to remember to pass 'guy' by reference just as we've done with the map. If there is a collision, the player's position will not be changed.So what do the controls look like now? Instead of assigning the position directly to guy.x and guy.y as we did before we'll call moveGuy() instead.
// Use arrow keys to scroll the map
if getRawKeyState(37) // left
moveGuy(guy, -guy.speed, 0, map)
endif
if getRawKeyState(39) // right
moveGuy(guy, guy.speed, 0, map)
endif
if getRawKeyState(38) // up
moveGuy(guy, 0, -guy.speed, map)
endif
if getRawKeyState(40) // down
moveGuy(guy, 0, guy.speed, map)
endif
Conclusion
If you've done everything correctly you should be able to move your guy around the map freely unless you run into water or a mushroom. One thing you can do as homework that I didn't touch on was fine tuning the movement during a collision. Let's say the player is 3px from an impassable tile but your movement velocity is 5px. If you move forward then 2px will overlap the tile resulting in a collision, thus the player won't move. This leaves a small gap of 2px from that tile that the player can never reach. It's a tiny amount but can be visually damaging overall to user's experience when playing your game. Ideally what you want to do in that case is trigger the collision but still move character. Only instead of moving the full 5px you would only move 3px. Now it'll look like the player actually is touching that tile and not just simply close to it. It's not too difficult to implement and gives a smoother playing experience.Even though UDT_Tile only contains a single variable now and you may be wondering why not just turn it into a simple array of integers instead, this structure future proofs your development. You can include any sort of additional data about each individual tile. Maybe a flag to signal a tile is a doorway or cave entrance. Use it to store animation data, your tiles don't have to be static. This sort of information would likely be written into your map file through an editor of some sort. The file format I used in this tutorial is very basic and simple. What you save and how you save it is entirely up to you and the needs of your game and how you design your map editor. I hope y'all learned something from this tutorial and found it useful. Questions, comments, mistakes, please find me on TGC Forum and let me know! I remember folks to keep your stick on the ice.
Download Full Code Here
SetErrorMode(2)
SetWindowTitle( "tut_tilemap" )
SetWindowSize(640,480, 0 )
SetVirtualResolution( 640,480)
SetSyncRate(60, 0 )
UseNewDefaultFonts( 1 )
Global yellow = 0 : yellow = makeColor(255,255,0)
Type UDT_Tile
id as integer
EndType
Type UDT_Map
width as integer
height as integer
tileSize as integer
view as integer[0,0]
data as UDT_Tile[0,0]
coll as integer[0]
x as float
y as float
maxX as integer
maxY as integer
viewWidth as integer
viewHeight as integer
EndType
Type UDT_Guy
x as float
y as float
spr as integer
speed as float
EndType
map as UDT_Map
guy as UDT_Guy
// Open file for reading
f = openToRead("map3.txt")
// Read first line
r$ = readLine(f)
map.width = val(getStringToken2(r$, ",", 1))
map.height = val(getStringToken2(r$, ",", 2))
map.tileSize = val(getStringToken2(r$, ",", 3))
imgFilename$ = getStringToken2(r$, ",", 4)
// Get number of tiles
total = countStringTokens2(r$, ",") - 4
map.coll.length = total
// Because the animation frames start at 1 and not 0,
// in this case we'll also start the array at 1. It's
// only used as a lookup for collision this'll keep simple.
for i = 1 to map.coll.length
map.coll[i] = val(getStringToken2(r$, ",", i+4))
next i
// Redfine the size of the 2D array
map.data.length = map.width
for x = 0 to map.width-1
map.data[x].length = map.height
next x
// Load map data
for y = 0 to map.height-1
r$ = readLine(f)
for x = 0 to map.width-1
map.data[x, y].id = val(getStringToken2(r$, ",", x+1))
next
next y
// Don't forget to close the file
closeFile(f)
// Calculate number of tiles that can be shown on screen
map.viewWidth = ceil((getVirtualWidth()+0.0) / map.tileSize) + 1
map.viewHeight = ceil((getVirtualheight()+0.0) / map.tileSize) + 1
map.view.length = map.viewWidth
for x = 0 to map.viewWidth-1
map.view[x].length = map.viewHeight
next x
// Calculate the max boundaries for map scrolling
map.maxX = (map.width * map.tileSize) - getVirtualWidth()
map.maxY = (map.height * map.tileSize) - getVirtualheight()
// The base of all tile sprites used in this map
spr_base = createSprite(loadImage(imgFilename$))
// Number of tiles in this tileset
tileCount = (getSpriteWidth(spr_base) / map.tileSize) * (getSpriteHeight(spr_base) / map.tileSize)
setSpriteAnimation(spr_base, map.tileSize, map.tileSize, tileCount)
// Position the map tiles
for y = 0 to map.viewHeight-1
for x = 0 to map.viewWidth-1
map.view[x, y] = cloneSprite(spr_base)
setSpriteFrame(map.view[x, y], map.data[x, y].id)
setSpritePosition(map.view[x, y], x*map.tileSize, y*map.tileSize)
next
next y
setSpriteVisible(spr_base, 0) // don't need to see this
// Create a periwinkle-colored 32x32 sprite
guy.spr = createSprite(0)
setSpriteSize(guy.spr, 32, 32)
setSpriteColor(guy.spr, 128,128, 255, 255)
// How fast the character can move
guy.speed = 5
// Starting position, center of screen
guy.x = getVirtualWidth() / 2
guy.y = getVirtualHeight() / 2
// How close the player can be to the edge of the screen
// before it starts scrolling the map.
edgeBuffer = 48
do
// Use arrow keys to scroll the map
if getRawKeyState(37) // left
moveGuy(guy, -guy.speed, 0, map)
endif
if getRawKeyState(39) // right
moveGuy(guy, guy.speed, 0, map)
endif
if getRawKeyState(38) // up
moveGuy(guy, 0, -guy.speed, map)
endif
if getRawKeyState(40) // down
moveGuy(guy, 0, guy.speed, map)
endif
// Boundaries to keep guy on the screen.
if guy.x < 0 then guy.x = 0
if guy.x > getVirtualWidth()-getSpriteWidth(guy.spr) then guy.x = getVirtualWidth()-getSpriteWidth(guy.spr)
if guy.y < 0 then guy.y = 0
if guy.y > getVirtualHeight()-getSpriteHeight(guy.spr) then guy.y = getVirtualHeight()-getSpriteHeight(guy.spr)
if guy.x < edgeBuffer
setMapPosition(map.x-guy.speed, map.y, map)
if map.x > 0 then guy.x = edgeBuffer
endif
if guy.x > getVirtualWidth()-getSpriteWidth(guy.spr)-edgeBuffer
setMapPosition(map.x+guy.speed, map.y, map)
if map.x < map.maxX then guy.x = getVirtualWidth()-getSpriteWidth(guy.spr)-edgeBuffer
endif
if guy.y < edgeBuffer
setMapPosition(map.x, map.y-guy.speed, map)
if map.y > 0 then guy.y = edgeBuffer
endif
if guy.y > getVirtualHeight()-getSpriteHeight(guy.spr)-edgeBuffer
setMapPosition(map.x, map.y+guy.speed, map)
if map.y < map.maxY then guy.y = getVirtualHeight()-getSpriteHeight(guy.spr)-edgeBuffer
endif
setSpritePosition(guy.spr, guy.x, guy.y)
// Displays the world tile coordinates under the mouse
mx = floor((getRawMouseX() + map.x) / map.tileSize)
my = floor((getRawMouseY() + map.y) / map.tileSize)
print(str(mx)+":"+str(my))
Sync()
loop
function getWorldX(x, m ref as UDT_Map)
tx = floor((x + m.x) / m.tileSize)
endfunction tx
function getWorldY(y, m ref as UDT_Map)
ty = floor((y + m.y) / m.tileSize)
endfunction ty
function moveGuy(g ref as UDT_Guy, vx, vy, m ref as UDT_Map)
w = getSpriteWidth(g.spr)
h = getSpriteHeight(g.spr)
a = checkPoint(g.x+vx, g.y+vy, m) // top left
b = checkPoint(g.x+vx+w, g.y+vy, m) // top right
c = checkPoint(g.x+vx, g.y+vy+h, m) // bottom left
d = checkPoint(g.x+vx+w, g.y+vy+h, m) // bottom right
if a+b+c+d = 0
g.x = g.x + vx
g.y = g.y + vy
endif
endfunction
function checkPoint(cx, cy, m ref as UDT_Map)
// Get this point in world coordinates
tx = getWorldX(cx, m)
ty = getWorldY(cy, m)
if tx < 0 or ty < 0 or tx > m.width-1 or ty > m.height-1 then exitfunction 0
// Determine the tile in which this point lies
frame = m.data[tx,ty].id
// Return 0 or 1, if the tile is passable or not.
r = m.coll[frame]
// Strictly for showing the collision with tiles
if r = 1
bx = (tx*m.tileSize) - m.x
by = (ty*m.tileSize) - m.y
drawBox(bx, by, bx+m.tileSize, by+m.tileSize, yellow, yellow, yellow, yellow, 0)
endif
endfunction r
function setMapPosition(mx, my, m ref as UDT_Map)
m.x = mx
m.y = my
// Boundary checks to avoid scrolling beyond the map
if m.x < 0 then m.x = 0
if m.y < 0 then m.y = 0
if m.x > m.maxX then m.x = m.maxX
if m.y > m.maxY then m.y = m.maxY
for y = 0 to m.viewHeight-1
for x = 0 to m.viewWidth-1
// Local offsets for viewport tiles
ox = x*m.tileSize - mod(m.x, m.tileSize)
oy = y*m.tileSize - mod(m.y, m.tileSize)
// World offsets for map data tiles
wx = floor(m.x / m.tileSize)
wy = floor(m.y / m.tileSize)
// Boundary check for edge cases
if x+wx < m.data.length and y+wy < m.data[x+wx].length
// Update viewport tiles with correct tile information
setSpriteFrame(m.view[x,y], m.data[x+wx, y+wy].id)
endif
// Position viewport tiles
setSpritePosition(m.view[x, y], ox, oy)
next
next y
endfunction