Defold to tiled exporter

I usually create levels in Tiled and then export them to Defold, but sometimes I get lazy and make tweaks directly in Defold, which leaves the two out of sync. To fix this, I made an editor script that exports from Defold into a Tiled format.

Once installed, you can right-click on a tilemap and select “convert tilemap to tiled CSV.”

When activated, you’ll need to enter the tile’s width and height, since I couldn’t find a way to pull this from the editor.

Known limitations…

The editor scripting system doesn’t have a save-as dialog, so the script creates a file called “output_tiled_file.tmx” in the root project directory.

It also adds a dummy line referencing a fake tileset “tileset.tsx,” which you’ll need to replace in Tiled with your tileset or a new one created from the image used as the tilesource in Defold.

Hopefully it will be of some use to others.

local M = {}

M.tile_size_x = 0
M.tile_size_y = 0

local function active_function(opts)
    -- get path
    local path = editor.get(opts.selection, "path")

    -- find extension
    local start_index, end_index = string.find(path, ".tilemap", 1, true)

    -- if found return true
    if (start_index) then
        return true
    end
    -- not found
    return false
end

local function show_dialog()
    local result = editor.ui.show_dialog(editor.ui.dialog({
    title = "What are tiles width and height in pixels",
    content = editor.ui.grid({
    padding = editor.ui.PADDING.LARGE, -- add padding around dialog edges
    columns = {{}, {grow = true}}, -- make 2nd column grow
    children = {
        {
            editor.ui.label({ 
                text = "tile width",
                alignment = editor.ui.ALIGNMENT.RIGHT
            }),
            editor.ui.integer_field({
                value = M.tile_size_x,
                on_value_changed = function(number) M.tile_size_x = number end})
            },
            {
                editor.ui.label({ 
                    text = "tile height",
                    alignment = editor.ui.ALIGNMENT.RIGHT
                }),
                editor.ui.integer_field({
                    value = M.tile_size_y,
                    on_value_changed = function(number)
                        M.tile_size_y = number
                    end})
                },
            }
        }),

        buttons = {
            editor.ui.dialog_button({
                text = "Cancel",
                cancel = true,
                result = false
            }),
            editor.ui.dialog_button({
                text = "Perform",
                default = true,
                result = true
            })
        },
    }))

    return result       
end

local function tile_bounds(tiles)
    local min_x = 10000
    local min_y = 10000
    local max_x = -10000
    local max_y = -10000

    -- firstly calc bounds
    for x, y, tile in tilemap.tiles.iterator(tiles) do
        -- check x
        if (x<min_x) then
            min_x = x
        elseif x>max_x then
            max_x = x
        end
        -- check y
        if (y<min_y) then
            min_y = y
        elseif y>max_y then
            max_y = y
        end  
    end
    -- return results
    return min_x,min_y,max_x,max_y
end

local function create_token(token,value)
    return token.."=\""..value.."\""
end

local function export_tilemap(opts)

    -- show dialog
    if (show_dialog()==false) then
        return
    end

    -- get path
    local path = editor.get(opts.selection, "path")

    -- test if layers exist
    if editor.can_get(path, "layers") == false then
        pprint("Not tilemap")
    end

    -- get layers
    local layers = editor.get(path, "layers")

    local min_x = math.huge
    local min_y = math.huge
    local max_x = -math.huge
    local max_y = -math.huge
    local width = 0
    local height = 0

    local map_width = -math.huge
    local map_height = -math.huge

    -- create a buffer
    local tiledata = {}
    local layer_height = {}
    local layer_width = {}
    local layer_id = {}

    for layer_index = 1, #layers do

        -- get layer id
        layer_id[layer_index] = editor.get(layers[layer_index], "id")

        -- get tiles
        local tiles = editor.get(layers[layer_index], "tiles")

        -- get bounds
        min_x,min_y,max_x,max_y = tile_bounds(tiles)

        -- check bounds
        if (min_x==math.huge or min_y==math.huge or max_x==-math.huge or max_y==-math.huge) then
            pprint("No tile data in layer: "..layer_id[layer_index])
            return
        end

        -- calc width height
        width = (max_x - min_x) + 1
        height = (max_y - min_y) + 1  

        -- store height and width
        layer_height[layer_index] = height
        layer_width[layer_index] = width

        -- calc buffer  
        local buffer_size = width * height

        -- create nested table
        tiledata[layer_index] = {}

        -- initialize the buffer to 0 (Empty space)
        for i = 1, buffer_size do
            tiledata[layer_index][i] = 0
        end

        -- populate with actual tile data
        for tx, ty, tile_id in tilemap.tiles.iterator(tiles) do
            -- normalize coordinates so our bottom-left tile starts at 0,0
            local normalized_x = tx - min_x
            local normalized_y = height-(ty - min_y)-1
            local index = (normalized_y * width) + normalized_x + 1

            -- get the specific tile ID at this coordinate
            tiledata[layer_index][index] = tile_id
        end

        -- update map bounds
        if (width>map_width) then
            map_width = width
        end
        if (height>map_height) then
            map_height = height
        end

    end

    -- open file
    local file, error_message = io.open("output_tiled_file.tmx", "w")

  
    if not file then
        return
    end

    local string_data  = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
    file:write(string_data.."\n")

    string_data = "<" .. create_token("map version","1.9") 
    string_data = string_data.." "..create_token("tiledversion","1.9.0")
    string_data = string_data.." "..create_token("orientation","orthogonal")
    string_data = string_data.." "..create_token("renderorder","right-down")
    string_data = string_data.." "..create_token("width",tostring(map_width))
    string_data = string_data.." "..create_token("height",tostring(map_height))
    string_data = string_data.." "..create_token("tilewidth",tostring(M.tile_size_x))
    string_data = string_data.." "..create_token("tileheight",tostring(M.tile_size_y))
    string_data = string_data.." "..create_token("nextlayerid",tostring(#layers+1))
    string_data = string_data.." "..create_token("nextobjectid",tostring(1))..">"
    file:write(string_data.."\n")

    -- write tile set line
    string_data = "\t<tileset firstgid=\"1\" source=\"tileset.tsx\"/>"
    file:write(string_data.."\n")


    -- write layers
    for layer_index = 1, #layers do

        -- write header
        string_data = "\t<"..create_token("layer id",tostring(layer_index))
        string_data = string_data.." "..create_token("name",layer_id[layer_index])
        string_data = string_data.." "..create_token("width",tostring(layer_width[layer_index]))
        string_data = string_data.." "..create_token("height",tostring(layer_height[layer_index]))..">"
        file:write(string_data.."\n")

        -- add csv encoding
        string_data = "\t\t<data encoding=\"csv\">"
        file:write(string_data.."\n")

        local data = tiledata[layer_index]
        local l_width = layer_width[layer_index]
        local l_height = layer_height[layer_index]

        -- write data row by row for readability
        for y = 0, l_height - 1 do
            local row_data = {}
            for x = 1, l_width do
                local index = (y * l_width) + x
                table.insert(row_data, tostring(data[index]))
            end

            -- concatenate the row with commas
            local row_string = table.concat(row_data, ",")

            -- add a trailing comma if it's not the very last row
            if y < l_height - 1 then
                row_string = row_string .. ","
            end

            file:write("\t\t\t" .. row_string .. "\n")
        end

        string_data = "\t\t</data>"
        file:write(string_data.."\n")

        string_data = "\t</layer>"
        file:write(string_data.."\n")
    end

    string_data = "</map>"
    file:write(string_data.."\n")

    file:close()
end

function M.get_language_servers()
    return {}
end

function M.get_commands()
    return {
        {
            label = "Convert Tilemap to Tiled CSV",
            locations = { "Assets" },
            query = {
                selection = { type = "resource", cardinality = "one" }
            },

            active = active_function,
            run = export_tilemap,
        }
    }
end

function M.get_prefs_schema()
    return {}
end

function M.get_http_server_routes()
    return {}
end

return M
3 Likes