Difference between revisions of "TiledMapLoader"

Line 9: Line 9:
  
 
<source lang="lua">
 
<source lang="lua">
 +
-- see https://love2d.org/wiki/TiledMapLoader for latest version
 
-- loader for "tiled" map editor maps (.tmx,xml-based) http://www.mapeditor.org/
 
-- loader for "tiled" map editor maps (.tmx,xml-based) http://www.mapeditor.org/
 
-- supports multiple layers
 
-- supports multiple layers
Line 15: Line 16:
 
-- NOTE : function GetMousePosOnMap () return gMouseX+gCamX-gScreenW/2,gMouseY+gCamY-gScreenH/2 end
 
-- NOTE : function GetMousePosOnMap () return gMouseX+gCamX-gScreenW/2,gMouseY+gCamY-gScreenH/2 end
  
kTileSize = 32
+
kTileSize = 64
 
kMapTileTypeEmpty = 0
 
kMapTileTypeEmpty = 0
 
local floor = math.floor
 
local floor = math.floor
 
local ceil = math.ceil
 
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)
 
function TiledMap_Load (filepath,tilesize,spritepath_removeold,spritepath_prefix)
spritepath_removeold = spritepath_removeold or "../"
+
    spritepath_removeold = spritepath_removeold or "../"
spritepath_prefix = spritepath_prefix or ""
+
    spritepath_prefix = spritepath_prefix or ""
kTileSize = tilesize or 32
+
    kTileSize = tilesize or kTileSize or 32
gTileGfx = {}
+
    gTileGfx = {}
+
 
local tiletype,layers = TiledMap_Parse(filepath)
+
    local tiletype,layers = TiledMap_Parse(filepath)
gMapLayers = layers
+
    gMapLayers = layers
for first_gid,path in pairs(tiletype) do  
+
    for first_gid,path in pairs(tiletype) do
path = spritepath_prefix .. string.gsub(path,"^"..string.gsub(spritepath_removeold,"%.","%%."),"")
+
        path = spritepath_prefix .. string.gsub(path,"^"..string.gsub(spritepath_removeold,"%.","%%."),"")
local raw = love.image.newImageData(path)
+
        local raw = love.image.newImageData(path)
local w,h = raw:getWidth(),raw:getHeight()
+
        local w,h = raw:getWidth(),raw:getHeight()
local gid = first_gid
+
        local gid = first_gid
local e = kTileSize
+
        local e = kTileSize
for y=0,floor(h/kTileSize)-1 do
+
        for y=0,floor(h/kTileSize)-1 do
for x=0,floor(w/kTileSize)-1 do
+
        for x=0,floor(w/kTileSize)-1 do
local sprite = love.image.newImageData(kTileSize,kTileSize)
+
            local sprite = love.image.newImageData(kTileSize,kTileSize)
sprite:paste(raw,0,0,x*e,y*e,e,e)
+
            sprite:paste(raw,0,0,x*e,y*e,e,e)
gTileGfx[gid] = love.graphics.newImage(sprite)
+
            gTileGfx[gid] = love.graphics.newImage(sprite)
gid = gid + 1
+
            gid = gid + 1
end
+
        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
 
end
Line 46: Line 91:
  
 
function TiledMap_GetMapTile (tx,ty,layerid) -- coords in tiles
 
function TiledMap_GetMapTile (tx,ty,layerid) -- coords in tiles
local row = gMapLayers[layerid][ty]
+
    local row = gMapLayers[layerid][ty]
return row and row[tx] or kMapTileTypeEmpty
+
    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
 
end
  
function TiledMap_DrawNearCam (camx,camy)
+
-- todo : maybe optimize during parse xml for types registered as to-be-listed before parsing ?
camx,camy = floor(camx),floor(camy)
+
function TiledMap_ListAllOfTypeOnLayer (layerid,iTileType)
local screen_w = love.graphics.getWidth()
+
local res = {}
local screen_h = love.graphics.getHeight()
+
local w = TiledMap_GetMapW()
local minx,maxx = floor((camx-screen_w/2)/kTileSize),ceil((camx+screen_w/2)/kTileSize)
+
local h = TiledMap_GetMapH()
local miny,maxy = floor((camy-screen_h/2)/kTileSize),ceil((camy+screen_h/2)/kTileSize)
+
for x=0,w-1 do  
for z = 1,#gMapLayers do
+
for y=0,h-1 do  
for x = minx,maxx do
+
if (TiledMap_GetMapTile(x,y,layerid) == iTileType) then table.insert(res,{x=x,y=y}) end
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
 +
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
 
end
  
Line 79: Line 159:
 
     local arg = {}
 
     local arg = {}
 
     string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
 
     string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
  arg[w] = a
+
    arg[w] = a
 
     end)
 
     end)
 
     return arg
 
     return arg
Line 127: Line 207:
  
 
local function getTilesets(node)
 
local function getTilesets(node)
local tiles = {}
+
    local tiles = {}
for k, sub in ipairs(node) do
+
    for k, sub in ipairs(node) do
if (sub.label == "tileset") then
+
        if (sub.label == "tileset") then
tiles[tonumber(sub.xarg.firstgid)] = sub[1].xarg.source
+
            tiles[tonumber(sub.xarg.firstgid)] = sub[1].xarg.source
end
+
        end
end
+
    end
return tiles
+
    return tiles
 
end
 
end
  
 
local function getLayers(node)
 
local function getLayers(node)
local layers = {}
+
    local layers = {}
for k, sub in ipairs(node) do
+
layers.width = 0
if (sub.label == "layer") then --  and sub.xarg.name == layer_name
+
layers.height = 0
local layer = {}
+
    for k, sub in ipairs(node) do
table.insert(layers,layer)
+
        if (sub.label == "layer") then --  and sub.xarg.name == layer_name
width = tonumber(sub.xarg.width)
+
layers.width  = max(layers.width ,tonumber(sub.xarg.width ) or 0)
i = 1
+
layers.height = max(layers.height,tonumber(sub.xarg.height) or 0)
j = 1
+
            local layer = {}
for l, child in ipairs(sub[1]) do
+
            table.insert(layers,layer)
if (j == 1) then
+
layer.name = sub.xarg.name
layer[i] = {}
+
--~ print("layername",layer.name)
end
+
            width = tonumber(sub.xarg.width)
layer[i][j] = tonumber(child.xarg.gid)
+
            i = 0
j = j + 1
+
            j = 0
if j > width then
+
            for l, child in ipairs(sub[1]) do
j = 1
+
                if (j == 0) then
i = i + 1
+
                    layer[i] = {}
end
+
                end
end
+
                layer[i][j] = tonumber(child.xarg.gid)
end
+
                j = j + 1
end
+
                if j >= width then
return layers
+
                    j = 0
 +
                    i = i + 1
 +
                end
 +
            end
 +
        end
 +
    end
 +
    return layers
 
end
 
end
  
 
function TiledMap_Parse(filename)
 
function TiledMap_Parse(filename)
local xml = LoadXML(love.filesystem.read(filename))
+
    local xml = LoadXML(love.filesystem.read(filename))
local tiles = getTilesets(xml[2])
+
    local tiles = getTilesets(xml[2])
local layers = getLayers(xml[2])
+
    local layers = getLayers(xml[2])
return tiles, layers
+
    return tiles, layers
 
end
 
end
 
</source>
 
</source>

Revision as of 11:51, 21 April 2012

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 = 64
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