# TiledMapLoader

loader for Tiled (mapeditor) map files.

see this forum thread for demo.zip : http://love2d.org/forums/viewtopic.php?f=5&t=2411&p=25929#p25929

note : for an alternative implementation see also http://love2d.org/forums/viewtopic.php?f=5&t=2097&hilit=tiled

    -- see https://love2d.org/wiki/TiledMapLoader for latest version-- loader for "tiled" map editor maps (.tmx,xml-based) http://www.mapeditor.org/-- supports multiple layers-- NOTE : function ReplaceMapTileClass (tx,ty,oldTileType,newTileType,fun_callback) end-- NOTE : function TransmuteMap (from_to_table) end -- from_to_table[old]=new-- NOTE : function GetMousePosOnMap () return gMouseX+gCamX-gScreenW/2,gMouseY+gCamY-gScreenH/2 end

    kTileSize = 32
    kMapTileTypeEmpty = 0
    local floor = math.floor
    local ceil = math.ceil
    local max = math.max
    local min = math.min
    local abs = math.abs
    gTileMap_LayerInvisByName = {}

    function TiledMap_Load (filepath,tilesize,spritepath_removeold,spritepath_prefix)
        spritepath_removeold = spritepath_removeold or "../"
        spritepath_prefix = spritepath_prefix or ""
        kTileSize = tilesize or kTileSize or 32
        gTileGfx = {}

        local tiletype,layers = TiledMap_Parse(filepath)
        gMapLayers = layers
        for first_gid,path in pairs(tiletype) do
            path = spritepath_prefix .. string.gsub(path,"^"..string.gsub(spritepath_removeold,"%.","%%."),"")
            local raw = love.image.newImageData(path)
            local w,h = raw:getWidth(),raw:getHeight()
            local gid = first_gid
            local e = kTileSize
            for y=0,floor(h/kTileSize)-1 do
            for x=0,floor(w/kTileSize)-1 do
                local sprite = love.image.newImageData(kTileSize,kTileSize)
                sprite:paste(raw,0,0,x*e,y*e,e,e)
                gTileGfx[gid] = love.graphics.newImage(sprite)
                gid = gid + 1
            end
            end
        end
    end

    function TiledMap_GetMapW () return gMapLayers.width end
    function TiledMap_GetMapH () return gMapLayers.height end

    -- returns the mapwidth actually used by tiles
    function TiledMap_GetMapWUsed ()
    	local maxx = 0
    	local miny = 0
    	local maxy = 0
    	for layerid,layer in pairs(gMapLayers) do
    		if (type(layer) == "table") then for ty,row in pairs(layer) do
    			if (type(row) == "table") then for tx,t in pairs(row) do
    				if (t and t ~= kMapTileTypeEmpty) then
    					miny = min(miny,ty)
    					maxy = max(maxy,ty)
    					maxx = max(maxx,tx)
    				end
    			end end
    		end end
    	end
    	return maxx + 1,miny,maxy+1
    end

    -- x,y= position for nearest-distance(square,not round), z= layer, maxrad= optional limit for searching
    -- returns x,y
    -- if x,y can be far outside map, set a sensible maxrad, otherwise it'll get very slow since searching outside map isn't optimized
    function TiledMap_GetNearestTileByTypeOnLayer (x,y,z,iTileType,maxrad)
    	local w = TiledMap_GetMapW()
    	local h = TiledMap_GetMapW()
    	local maxrad2 = max(x,w-x,y,h-y) if (maxrad) then maxrad2 = min(maxrad2,maxrad) end
    	if (TiledMap_GetMapTile(x,y,z) == iTileType) then return x,y end
    	for r = 1,maxrad2 do
    		for i=-r,r do
    			local xa,ya = x+i,y-r if (TiledMap_GetMapTile(xa,ya,z) == iTileType) then return xa,ya end -- top
    			local xa,ya = x+i,y+r if (TiledMap_GetMapTile(xa,ya,z) == iTileType) then return xa,ya end -- bot
    			local xa,ya = x-r,y+i if (TiledMap_GetMapTile(xa,ya,z) == iTileType) then return xa,ya end -- left
    			local xa,ya = x+r,y+i if (TiledMap_GetMapTile(xa,ya,z) == iTileType) then return xa,ya end -- right
    		end
    	end
    end

    function TiledMap_GetMapTile (tx,ty,layerid) -- coords in tiles
        local row = gMapLayers[layerid][ty]
        return row and row[tx] or kMapTileTypeEmpty
    end

    function TiledMap_SetMapTile (tx,ty,layerid,v) -- coords in tiles
        local row = gMapLayers[layerid][ty]
    	if (not row) then row = {} gMapLayers[layerid][ty] = row end
    	row[tx] = v
    end

    -- todo : maybe optimize during parse xml for types registered as to-be-listed before parsing ?
    function TiledMap_ListAllOfTypeOnLayer (layerid,iTileType)
    	local res = {}
    	local w = TiledMap_GetMapW()
    	local h = TiledMap_GetMapH()
    	for x=0,w-1 do
    	for y=0,h-1 do
    		if (TiledMap_GetMapTile(x,y,layerid) == iTileType) then table.insert(res,{x=x,y=y}) end
    	end
    	end
    	return res
    end

    function TiledMap_GetLayerZByName (layername) for z,layer in ipairs(gMapLayers) do if (layer.name == layername) then return z end end end
    function TiledMap_SetLayerInvisByName (layername) gTileMap_LayerInvisByName[layername] = true end

    function TiledMap_IsLayerVisible (z)
    	local layer = gMapLayers[z]
    	return layer and (not gTileMap_LayerInvisByName[layer.name or "?"])
    end

    function TiledMap_GetTilePosUnderMouse (mx,my,camx,camy)
    	return	floor((mx+camx-love.graphics.getWidth()/2)/kTileSize),
    			floor((my+camy-love.graphics.getHeight()/2)/kTileSize)
    end

    function TiledMap_DrawNearCam (camx,camy,fun_layercallback)
        camx,camy = floor(camx),floor(camy)
        local screen_w = love.graphics.getWidth()
        local screen_h = love.graphics.getHeight()
        local minx,maxx = floor((camx-screen_w/2)/kTileSize),ceil((camx+screen_w/2)/kTileSize)
        local miny,maxy = floor((camy-screen_h/2)/kTileSize),ceil((camy+screen_h/2)/kTileSize)
        for z = 1,#gMapLayers do
    	if (fun_layercallback) then fun_layercallback(z,gMapLayers[z]) end
    	if (TiledMap_IsLayerVisible(z)) then
        for x = minx,maxx do
        for y = miny,maxy do
            local gfx = gTileGfx[TiledMap_GetMapTile(x,y,z)]
            if (gfx) then
                local sx = x*kTileSize - camx + screen_w/2
                local sy = y*kTileSize - camy + screen_h/2
                love.graphics.draw(gfx,sx,sy) -- x, y, r, sx, sy, ox, oy
            end
        end
        end
        end
        end
    end

    -- ***** ***** ***** ***** ***** xml parser

    -- LoadXML from http://lua-users.org/wiki/LuaXml
    function LoadXML(s)
      local function LoadXML_parseargs(s)
        local arg = {}
        string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
        arg[w] = a
        end)
        return arg
      end
      local stack = {}
      local top = {}
      table.insert(stack, top)
      local ni,c,label,xarg, empty
      local i, j = 1, 1
      while true do
        ni,j,c,label,xarg, empty = string.find(s, "<(%/?)([%w:]+)(.-)(%/?)>", i)
        if not ni then break end
        local text = string.sub(s, i, ni-1)
        if not string.find(text, "^%s*$") then
          table.insert(top, text)
        end
        if empty == "/" then  -- empty element tag
          table.insert(top, {label=label, xarg=LoadXML_parseargs(xarg), empty=1})
        elseif c == "" then   -- start tag
          top = {label=label, xarg=LoadXML_parseargs(xarg)}
          table.insert(stack, top)   -- new level
        else  -- end tag
          local toclose = table.remove(stack)  -- remove top
          top = stack[#stack]
          if #stack < 1 then
            error("nothing to close with "..label)
          end
          if toclose.label ~= label then
            error("trying to close "..toclose.label.." with "..label)
          end
          table.insert(top, toclose)
        end
        i = j+1
      end
      local text = string.sub(s, i)
      if not string.find(text, "^%s*$") then
        table.insert(stack[#stack], text)
      end
      if #stack > 1 then
        error("unclosed "..stack[stack.n].label)
      end
      return stack[1]
    end

    -- ***** ***** ***** ***** ***** parsing the tilemap xml file

    local function getTilesets(node)
        local tiles = {}
        for k, sub in ipairs(node) do
            if (sub.label == "tileset") then
                tiles[tonumber(sub.xarg.firstgid)] = sub[1].xarg.source
            end
        end
        return tiles
    end

    local function getLayers(node)
        local layers = {}
    	layers.width = 0
    	layers.height = 0
        for k, sub in ipairs(node) do
            if (sub.label == "layer") then --  and sub.xarg.name == layer_name
    			layers.width  = max(layers.width ,tonumber(sub.xarg.width ) or 0)
    			layers.height = max(layers.height,tonumber(sub.xarg.height) or 0)
                local layer = {}
                table.insert(layers,layer)
    			layer.name = sub.xarg.name
    			--~ print("layername",layer.name)
                width = tonumber(sub.xarg.width)
                i = 0
                j = 0
                for l, child in ipairs(sub[1]) do
                    if (j == 0) then
                        layer[i] = {}
                    end
                    layer[i][j] = tonumber(child.xarg.gid)
                    j = j + 1
                    if j >= width then
                        j = 0
                        i = i + 1
                    end
                end
            end
        end
        return layers
    end

    function TiledMap_Parse(filename)
        local xml = LoadXML(love.filesystem.read(filename))
        local tiles = getTilesets(xml[2])
        local layers = getLayers(xml[2])
        return tiles, layers
    end

## usage demo

(see zip file in forum for map files and graphics)

    -- small demo for TiledMap loader
    love.filesystem.load("tiledmap.lua")()

    gKeyPressed = {}
    gCamX,gCamY = 100,100

    function love.load()
    	TiledMap_Load("map/map01.tmx")
    end

    function love.keyreleased( key )
    	gKeyPressed[key] = nil
    end

    function love.keypressed( key, unicode )
    	gKeyPressed[key] = true
    	if (key == "escape") then os.exit(0) end
    	if (key == " ") then -- space = next mal
    		gMapNum = (gMapNum or 1) + 1
    		if (gMapNum > 10) then gMapNum = 1 end
    		TiledMap_Load(string.format("map/map%02d.tmx",gMapNum))
    		gCamX,gCamY = 100,100
    	end
    end

    function love.update( dt )
    	local s = 500*dt
    	if (gKeyPressed.up) then gCamY = gCamY - s end
    	if (gKeyPressed.down) then gCamY = gCamY + s end
    	if (gKeyPressed.left) then gCamX = gCamX - s end
    	if (gKeyPressed.right) then gCamX = gCamX + s end
    end

    function love.draw()
        love.graphics.print('arrow-keys=scroll, space=next map', 50, 50)
    	love.graphics.setBackgroundColor(0x80,0x80,0x80)
    	TiledMap_DrawNearCam(gCamX,gCamY)
    end

Category:

- Snippets