Yesterday, I began working on a new importer. Tiled has deprecated the XML format and with AGK2's json loading ability I wanted to see if I could take advantage of that. I'm happy to say that so far the json loading is working very well, and since I don't have to do the parsing in Tier1 it is MUCH quicker. A 500x500 map with 2 layers loads instantly, at least as far as reading the map file. It will still need to generate the sprites and load images. The sprites shouldn't slow it down, the image loading will obviously depend on the size of the tileset image and how many. But honestly, half the project before was the parser. There are a few caveats though; Your file must be saved as TMJ/JSON format and the layer format as uncompressed CSV. While this will increase the size of the files, by using the built-in json loader I would not be able to decode the base64 without first loading into a temp array then parsing it. This would more than likely be a lot slower. The other important note is you'll need to make one minor change to the file before agk can read it. While agk can load json object from a string, loading from a file only works on an array. The Tiled format saves a json object so it must be wrapped with square brackets [ ] so it's seen as an array of map objects and not just a single object. That's it. Not a big deal but can be frustrating as I keep forgetting to do this and wonder why my maps suddenly refuse to load. Failure to load the json will not produce an error, you'll just end up with an empty array.
I'll post some photos once I have some map rendering done, but as I said the loading/parsing was always a big chunk of the work and it looks promising so far.
Going over my old code, a sprite was created for every tile in the map. While this simplified several things, it's obviously inefficient when it comes to very large maps. Happy to say this is now being addressed.
Command Set:
- tmx_LoadMap()
- tmx_SetMapPosition()
- tmx_SetMapOffset()
- tmx_SetMapScissor()
- tmx_SetLayerAlpha()
- tmx_SetLayerVisible()
- tmx_PickTile()
- tmx_GetTileProperty()
Features:
JSON/csv format only (uncompressed)
Layer alpha and visibility
External tilesets (tsx) in xml and embedded tilesets done.
Only orthogonal maps are implemented, isometric to come soon.
For example, with 32x32 tile size on a 640x480 screen, you will have at most 336 tile sprites (21 * 16) per layer. As these are cloned sprites, the number of truly unique sprites (image data) is only equal to the number of tilesets loaded. As the map scrolls around and the sprites are updated with proper tile images, there are chances of it having to delete a sprite and reclone another because of images existing within different tilesets. While this should not have any noticable impact, for efficiency it'd be recommended not using more than a single tileset per layer. Until I do more testing I can't say how much this really matters or not. An option I'd have is loading all the tilesets and combining them into a single image in AGK. As far as I can tell, Tiled breaks image tilesets into tiles sizes equal to that of the map's tile size, so I should theoretically never have tilesets with different tile sizes.
Current largest map size tested is 528x512, that's 12x the size of the legend of zelda overworld map. It slowed down a little bit compared to a small map, but nothing major. And loading is still instananeous. I'm still tweaking the code to get more speed but the biggest slow down seems to come from simply positioning the sprites. If I have 2000 fps, it drops to 500 when moving the sprites. Stripping everything out but the positioning yields about the same result, so there may not be anything I can do about it. For reference, hat was with 825 sprites being moved at a time.
Older v.2
https://forum.thegamecreators.com/thread/219035
Loading the tilset (tmx) files requires my xml parser, you can find it here:
https://github.com/phaelax/agkxml/blob/master/xmlparser.agc
Example:
// Project: tmxv3
// Created: 2024-02-22
// Tiled version 1.10.2
SetErrorMode(2)
SetWindowTitle( "tmxv3" )
SetWindowSize( 1024, 768, 0 )
SetVirtualResolution( 1024, 768 )
SetSyncRate(0, 0 )
UseNewDefaultFonts( 1 )
#include 'tmxloader.agc'
map as TMX_Map
map = tmx_LoadMap("desert.tmj")
scrollSpeed# = 1
do
// arrow keys to scroll map
oldy# = oy#
oldx# = ox#
if getRawKeyState(40) // down
oy# = oy# + scrollSpeed#
if oy# > map.maxScrollHeight then oy# = map.maxScrollHeight
endif
if getRawKeyState(38) // down
oy# = oy# - scrollSpeed#
if oy# < 0 then oy# = 0
endif
if getRawKeyState(37) // left
ox# = ox# - scrollSpeed#
if ox# < 0 then ox# = 0
endif
if getRawKeyState(39) // right
ox# = ox# + scrollSpeed#
if ox# > map.maxScrollWidth then ox# = map.maxScrollWidth
endif
// press <space> to toggle layer visibility
if getrawkeypressed(32)
tmx_SetLayerVisible(map, 1, 1-map.layers[1].visible)
endif
// update map position
if oldY# <> oy# or oldX# <> ox#
tmx_SetMapPosition(map, ox#, oy#)
endif
Sync()
loop
tmxloader.agc
#include 'xmlparser.agc'
Type TMX_Frame
duration as integer
tileid as integer
EndType
Type TMX_Tile
animation as TMX_Frame[]
id as integer
image as string
imageheight as integer
imagewidth as integer
x as integer
y as integer
width as integer
height as integer
objectgroup as TMX_Layer
probability as float
properties as TMX_Property[]
terrain as integer[]
_type as string
EndType
Type TMX_Chunk
data as integer[] // this field can be either an array of ints or a string (this is stupid)
//data as string // base64 encoded
height as integer // height in tiles
width as integer // width in tiles
x as integer // x coordinate in tiles
y as integer // y coordinate in tiles
EndType
Type TMX_Object
x as float
y as float
EndType
Type TMX_Layer
name as string
chunks as TMX_Chunk[]
class as string
compression as string // zlib, vzip, zstd, or empty (tilelayer only)
data as integer[] // this field can be either an array of ints or a string (this is stupid)
//data as string // base64 encoded (data is for a tilelayer only)
draworder as string // topdown or index (objectgroup only)
encoding as string // csv or base64 (tilelayer only)
height as integer // row count, same as map height (tilelayer only)
width as integer // column count, same as map width (tilelayer only)
id as integer
image as string
//layers as _Layer[] // group only (not able to contain self in agk)
locked as integer // boolean, used by editor
objects as TMX_Object[]
offsetx as float // horizontal layer offset in pixels
offsety as float // vertical layer offset in pixels
opacity as float
parallaxx as float
parallaxy as float
properties as TMX_Property[]
repeatx as integer // boolean
repeaty as integer // boolean
startx as integer
starty as integer
tintcolor as string
transparentcolor as string
_type as string //tilelayer, objectgroup, imagelayer, group
visible as integer // boolean
x as integer // horizontal layer offset in tiles
y as integer // vertical layer offset in tiles
EndType
Type TMX_Tileset
name as string
source as string
image as string
columns as integer
firstgid as integer
imageheight as integer
imagewidth as integer
margin as integer
properties as TMX_Property[]
spacing as integer
tilecount as integer
tileheight as integer
tilewidth as integer
baseSprite as integer
EndType
Type TMX_Property
name as string
_type as string
value as string
EndType
Type TMX_SpriteTile
id as integer // sprite id
tileId as integer // tile id from the layer data (may not be necessary)
EndType
Type TMX_Map
backgroundcolor as string
width as integer // width of map in tiles
height as integer // height of map in tiles
layers as TMX_Layer[]
`nextobjectid as integer
orientation as string
properties as TMX_Property[]
renderorder as string
tileheight as integer
tilewidth as integer
tilesets as TMX_Tileset[]
infinite as integer // boolean
viewport as TMX_SpriteTile[0,0,0] // This holds the sprite data that's actually shown on screen
vWidth as integer // viewport width in tiles
vHeight as integer // viewport height in tiles
localX as float // X position within the map
localY as float // Y position within the map
offsetX as float // X map position on screen
offsetY as float // Y map position on screen
maxScrollWidth as integer // Map position cannot exceed this number (does not account for map offset)
maxScrollHeight as integer // Map position cannot exceed this number (does not account for map offset)
EndType
/************************************************************************
*
*
*
************************************************************************/
function tmx_LoadMap(filename as string)
local map as TMX_Map[]
local dom as XML_Element[]
local tid, tsid, tileIndex, tileId, frameId, i, x, y as integer
map.load(filename)
// Load the tileset (TSX) files which contain image path
for i = 0 to map[0].tilesets.length
`dom as XML_Element[]
dom = xml_loadDocument(map[0].tilesets[i].source)
tid = xml_FindFirstTag(dom, "tileset")
map[0].tilesets[i].name = xml_GetAttributeValueByName(dom, tid, "name")
map[0].tilesets[i].tilewidth = val(xml_GetAttributeValueByName(dom, tid, "tilewidth"))
map[0].tilesets[i].tileheight = val(xml_GetAttributeValueByName(dom, tid, "tileheight"))
map[0].tilesets[i].tilecount = val(xml_GetAttributeValueByName(dom, tid, "tilecount"))
map[0].tilesets[i].baseSprite = createSprite(loadImage(xml_GetAttributeValueByName(dom, xml_GetChildIdById(dom, tid, 0), "source")))
setSpriteAnimation(map[0].tilesets[i].baseSprite, map[0].tilesets[i].tilewidth, map[0].tilesets[i].tileheight, map[0].tilesets[i].tilecount)
setSpriteVisible(map[0].tilesets[i].baseSprite, 0)
next i
// calculate viewport dimensions
map[0].vWidth = tmx_min(ceil(getVirtualWidth() / map[0].tileWidth) + 1, map[0].width)
map[0].vheight = tmx_min(ceil(getVirtualHeight() / map[0].tileHeight) + 1, map[0].height)
// define viewport array sizes
map[0].viewport.length = map[0].layers.length
for i = 0 to map[0].layers.length
map[0].viewport[i].length = map[0].vWidth
for x = 0 to map[0].vWidth-1
map[0].viewport[i,x].length = map[0].vheight
next x
next i
// Create the viewport sprites for each layer
for i = 0 to map[0].layers.length
for y = 0 to map[0].vheight-1
for x = 0 to map[0].vWidth-1
tileIndex = y * map[0].width + x
tileId = map[0].layers[i].data[tileIndex]
if tileId > 0
tsid = tmx_GetTilesetIdFromTileId(map[0], tileId)
map[0].viewport[i, x, y].id = cloneSprite(map[0].tilesets[tsid].baseSprite)
setSpritePosition(map[0].viewport[i, x, y].id, x*map[0].tilewidth, y*map[0].tileheight)
frameId = tileId - map[0].tilesets[tsid].firstgid + 1
setSpriteFrame(map[0].viewport[i, x, y].id, frameId)
else
// no tile for location, create one using last tileset
map[0].viewport[i, x, y].id = cloneSprite(map[0].tilesets[tsid].baseSprite)
endif
next x
next y
next i
map[0].maxScrollWidth = map[0].width * map[0].tilewidth - getVirtualWidth()
map[0].maxScrollHeight = map[0].height * map[0].tileheight - getVirtualHeight()
map[0].maxScrollWidth = tmx_max(map[0].maxScrollWidth, 0)
map[0].maxScrollHeight = tmx_max(map[0].maxScrollHeight, 0)
tmx_SetMapPosition(map[0], 0, 0)
endfunction map[0]
/************************************************************************
*
* Layer id starts at 0
* Hidden layers do not have their positions updated in viewport
*
************************************************************************/
function tmx_SetLayerVisible(map ref as TMX_Map, layer as integer, bool as integer)
map.layers[layer].visible = bool
for y = 0 to map.vHeight-1
for x = 0 to map.vWidth-1
setSpriteVisible(map.viewport[layer, x, y].id, bool)
next x
next y
if bool = 1 then tmx_SetMapPosition(map, map.localX, map.localY)
endfunction
/************************************************************************
*
* Layer id starts at 0
*
************************************************************************/
function tmx_SetLayerAlpha(map ref as TMX_Map, layer as integer, value as integer)
for y = 0 to map.vHeight-1
for x = 0 to map.vWidth-1
setSpriteColorAlpha(map.viewport[layer, x, y].id, value)
next x
next y
endfunction
/************************************************************************
*
* Repositions the map on the screen.
*
************************************************************************/
function tmx_SetMapOffset(map ref as TMX_Map, x as float, y as float)
map.offsetX = x
map.offsetY = y
tmx_SetMapPosition(map, map.localX, map.localY)
endfunction
function tmx_GetMapX(map ref as TMX_Map)
endfunction map.localX
function tmx_GetMapY(map ref as TMX_Map)
endfunction map.localY
function tmx_GetMapOffsetX(map ref as TMX_Map)
endfunction map.offsetX
function tmx_GetMapOffsetY(map ref as TMX_Map)
endfunction map.offsetY
/************************************************************************
*
* Define a smaller visible area of map than the default of the screen
*
************************************************************************/
function tmx_SetMapScissor(map ref as TMX_Map, x1 as integer, y1 as integer, x2 as integer, y2 as integer)
for i = 0 to map.layers.length
for y = 0 to map.vHeight-1
for x = 0 to map.vWidth-1
setSpriteScissor(map.viewport[i, x, y].id, x1, y1, x2, y2)
next x
next y
next i
endfunction
/************************************************************************
*
* Local map coordinates
*
************************************************************************/
function tmx_SetMapPosition(map ref as TMX_Map, ox# as float, oy# as float)
map.localX = ox#
map.localY = oy#
tileTotal = (map.width * map.height) // make sure we don't go out of array bounds
// Loop over all the map layers
for i = 0 to map.layers.length
// Hidden layers are not updated. If you need to rely on data from invisible sprites, comment out this check
if map.layers[i].visible = 1
for y = 0 to map.vHeight-1
localY = y*map.tileheight - mod(map.localY, map.tileheight) // local position of viewport tile
wy = floor(map.localY / map.tileheight) // World offsets for map data tiles
for x = 0 to map.vWidth-1
localX = x*map.tilewidth - mod(map.localX, map.tilewidth) // local position of viewport tile
wx = floor(map.localX / map.tilewidth)
//if map.viewport[i, x, y].id > 0
tileIndex = (y+wy) * map.width + (x+wx)
if tileIndex < tileTotal
tileId = map.layers[i].data[tileIndex]
if tileId > 0 `and map.viewport[i, x, y].tileId <> tileId
tsid = tmx_GetTilesetIdFromTileId(map, tileId)
targetTilesetImg = getSpriteImageID(map.tilesets[tsid].baseSprite)
currentTilesetImg = getSpriteImageID(map.viewport[i, x, y].id)
// each viewport sprite is a clone of the animated tileset image sprite
// If the viewport sprite needs updated to a tile from a different tileset,
// delete the current sprite and create a new cloned sprite using the correct tilset base
// To reduce or eliminate this, it' recommended to use no more than 1 tileset per layer
if currentTilesetImg <> targetTilesetImg
deleteSprite(map.viewport[i, x, y].id)
map.viewport[i, x, y].id = cloneSprite(map.tilesets[tsid].baseSprite)
endif
// checking visibility before setting it gives a very small performance increase, but almost negligible.
if getSpriteVisible(map.viewport[i, x, y].id) = 0 then setSpriteVisible(map.viewport[i, x, y].id, 1)
// Setting viewport tiles uses animation frames. Convert tile id into a local number
frameId = tileId - map.tilesets[tsid].firstgid + 1
`cf = getSpriteCurrentFrame(map.viewport[i, x, y].id)
`if frameId <> cf then
// appears to be marginally faster to just set the frame everytime rather than to check if it's different first
setSpriteFrame(map.viewport[i, x, y].id, frameId)
setSpritePosition(map.viewport[i, x, y].id, map.offsetX + localX, map.offsetY + localY)
else
// necessary when a layer doesn't fill up all tile spaces (if you had separate layer containing only shrubbery on top a ground layer)
setSpriteVisible(map.viewport[i, x, y].id, 0)
endif
else
// this fixes an edge case issue
setSpriteVisible(map.viewport[i, x, y].id, 0)
endif
//endif
next x
next y
endif
next i
endfunction
/************************************************************************
*
* Returns tileset ID given a tileId
*
************************************************************************/
function tmx_GetTilesetIdFromTileId(m ref as TMX_Map, id as integer)
for i = 0 to m.tilesets.length
//start = m.tilesets[i].firstgid
//finish = m.tilesets[i].firstgid + m.tilesets[i].tilecount - 1
if id >= m.tilesets[i].firstgid and id < m.tilesets[i].firstgid + m.tilesets[i].tilecount then exitfunction i
next i
endfunction 0
/************************************************************************
*
* Helper functions
*
************************************************************************/
function tmx_min(n1, n2)
if n1 > n2 then exitfunction n2
endfunction n1
function tmx_max(n1, n2)
if n1 > n2 then exitfunction n1
endfunction n2