Tiled Plugin Tutorial

Who this tutorial is for:

This tutorial is for anyone who wants to learn how to write a plugin for tiled. Plugins enable tiled to read and/or write any map format. If you are reading this tutorial, it is probably because you want to use tiled for your game, and your game is already designed to use a specific, proprietary map format.

Prerequisites:

This tutorial uses java, and the syntax is not explained. It will help if you already know how to program in java, but you might be able to get by if you understand c++ syntax. If you don't know java, you can learn it with the official java tutorial. I assume that you already have the tiled source code and know how to compile tiled. I assume you already have a java compiler and apache ant installed. Any tiled code samples will be using latest svn (735 at the time of this writing) but latest stable (0.6.1) will work equally well. I will give filesystem paths and commands as if you are using Linux, so if you are using Windows, you will need to convert these (~/svn/tiled -> C:\Documents and Settings\Username\My Documents\tiled, for example). I will assume that your tiled source directory is ~/svn/tiled.

You need to understand how to use tiled. You need to know how to add layers, load tilesets from a png file, draw tiles, and save a map. You can learn all of this in the "Creating a simple map" tutorial.

Step 1: An output plugin that doesn't do anything useful

The first step is to write an output plugin that writes a blank file when you select "Save". Here is the skeleton of our file plugin. Do not try to compile this skeleton code.

// TutorialMapWriter.java:

package tiled.plugins.tutorial;

import java.io.*;

import tiled.io.*;
import tiled.core.*;
import tiled.util.*;
import tiled.mapeditor.selection.SelectionLayer;

public class TutorialMapWriter implements MapWriter
{

// the class info goes here.

}

Your plugin code should start with the "package tiled.plugins.<plugin name>" line. I am calling this one tutorial. We will be using functions from tiled, so we import several tiled packages. And then the class definition. Our class must implement MapWriter.

Because our class implements MapWriter, it must define every function declared in MapWriter, as well as those functions which are defined in interfaces that MapWriter extends from. MapWriter extends PluggableMapIO and FileFilter. Here is a list of the functions which we must define in order for our plugin to work with tiled:

from MapWriter

from PluggableMapIO

from FileFilter

This is 10 functions. It might seem like quite a few, but some of them we can get away with minimum effort. For example, if we don't need to write a proprietary tileset, then we don't have to write much for the "writeTileset" functions. And most of the rest are easy, and can be cut + pasted from this tutorial or the tiled examples, assuming you are willing to publish your plugin under the GNU GPLv2. The only function of any significant length will be "public void writeMap(Map map, OutputStream out)".

Here is our complete "empty shell" plugin:

// TutorialMapWriter.java
	
package tiled.plugins.tutorial;

import java.io.*;

import tiled.io.*;
import tiled.core.*;
import tiled.util.*;
import tiled.mapeditor.selection.SelectionLayer;

public class TutorialMapWriter implements MapWriter
{
	/**
	 * Saves a map to a file.
	 * 
	 * @param map the map to be saved
	 * @param filename the filename of the map file
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, String filename) throws IOException
	{
		writeMap(map, new FileOutputStream(filename));
	}

	/**
	 * Writes a map to an already opened stream. Useful
	 * for maps which are part of a larger binary dataset
	 * 
	 * @param map the Map to be written
	 * @param out the output stream to write to
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, OutputStream out) throws IOException {
		// blank function for now
	}
	
	/**
	 * Overload this to write a tileset to an open stream. Tilesets are not
	 * supported by this writer.
	 * 
	 * @param set
	 * @param out
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, OutputStream out) throws Exception {
		logger.error("Tilesets are not supported!");
	}

	/**
	 * Saves a tileset to a file. Tilesets are not supported by this writer.
	 * 
	 * @param set
	 * @param filename the filename of the tileset file
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, String filename) throws Exception {
		logger.error("Tilesets are not supported!");
		logger.error("(asked to write " + filename + ")");
	}

	/**
	 * Lists supported file extensions. This function is used by the editor to
	 * find the plugin to use for a specific file extension.
	 *
	 * @return a comma delimited string of supported file extensions
	 * @throws Exception
	 */
	public String getFilter() throws Exception {
		return "*.lua";
	}

	/**
	 * Returns a short description of the plugin, or the plugin name. This
	 * string is displayed in the list of loaded plugins under the Help menu in
	 * Tiled.
	 *
	 * @return a short name or description
	 */
	public String getName() {
		return "Tiled Tutorial exporter";
	}

	/**
	 * Returns a long description (no limit) that details the plugin's
	 * capabilities, author, contact info, etc.
	 *
	 * @return a long description of the plugin
	 */
	public String getDescription() {
		return
			"This is a simple plugin that writes a blank file.\n" +
			"You may distribute this plugin under the terms of the GNU GPLv2.\n";
	}

	/**
	 * Returns the base Java package string for the plugin
	 *
	 * @return String the base package of the plugin
	 */
	public String getPluginPackage() {
		return "Tiled Tutorial Writer Plugin";
	}

	/**
	 * The PluginLogger object passed by the editor when the plugin is called to load
	 * or save a map can be used by the plugin to notify the user of any
	 * problems or messages.
	 *
	 * @param logger
	 */
	public void setLogger(PluginLogger logger) {
		this.logger = logger;
	}

	/**
	 * java.io.FileFilter Interface
	 */
	public boolean accept(File pathname) {
		try {
			String path = pathname.getCanonicalPath();
			if (path.endsWith(".lua")) {
				return true;
			}
		} catch (IOException e) {}
		return false;
	}

    private PluginLogger logger;
}

Explanation of code:

/**
	 * Saves a map to a file.
	 * 
	 * @param map the map to be saved
	 * @param filename the filename of the map file
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, String filename) throws IOException
	{
		writeMap(map, new FileOutputStream(filename));
	}

This is the function that tiled calls to save the map. Every instance I've seen in the example plugins just makes this function a wrapper around writeMap(Map, OutputStream).

/**
	 * Writes a map to an already opened stream. Useful
	 * for maps which are part of a larger binary dataset
	 * 
	 * @param map the Map to be written
	 * @param out the output stream to write to
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, OutputStream out) throws IOException {
		// blank function for now
	}

This is the meat of TutorialMapWriter. It is where the map actually gets written. Of course, for our skeleton plugin it is blank, but in most useful plugin writers, this will be the longest function in the class.

/**
	 * Overload this to write a tileset to an open stream. Tilesets are not
	 * supported by this writer.
	 * 
	 * @param set
	 * @param out
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, OutputStream out) throws Exception {
		logger.error("Tilesets are not supported!");
	}

	/**
	 * Saves a tileset to a file. Tilesets are not supported by this writer.
	 * 
	 * @param set
	 * @param filename the filename of the tileset file
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, String filename) throws Exception {
		logger.error("Tilesets are not supported!");
		logger.error("(asked to write " + filename + ")");
	}

These are functions to save a tileset. Tilesets are not covered in this tutorial, so these functions just log an error using our PluginLogger, logger. A PluginLogger is a tiled class which logs errors to the console.

	/**
	 * Lists supported file extensions. This function is used by the editor to
	 * find the plugin to use for a specific file extension.
	 *
	 * @return a comma delimited string of supported file extensions
	 * @throws Exception
	 */
	public String getFilter() throws Exception {
		return "*.lua";
	}

This function helps tiled determine what format to save the map file in when "Save by Extension" is used. By returning "*.lua", we are telling tiled that any file that ends with .lua is a potential candidate for our map format.

	/**
	 * Returns a short description of the plugin, or the plugin name. This
	 * string is displayed in the list of loaded plugins under the Help menu in
	 * Tiled.
	 *
	 * @return a short name or description
	 */
	public String getName() {
		return "Tiled Tutorial exporter";
	}

	/**
	 * Returns a long description (no limit) that details the plugin's
	 * capabilities, author, contact info, etc.
	 *
	 * @return a long description of the plugin
	 */
	public String getDescription() {
		return
			"This is a simple plugin that writes a blank file.\n" +
			"Released under the terms of the GPLv2.\n";
	}

These functions are explained well in the comments. I'd like to note that "getDescription" is used for the Help Info.

	/**
	 * Returns the base Java package string for the plugin
	 *
	 * @return String the base package of the plugin
	 */
	public String getPluginPackage() {
		return "Tiled Tutorial Writer Plugin";
	}

This function returns the string that is used to identify this plugin in the "About Plugins" dialog.

	/**
	 * The PluginLogger object passed by the editor when the plugin is called to load
	 * or save a map can be used by the plugin to notify the user of any
	 * problems or messages.
	 *
	 * @param logger
	 */
	public void setLogger(PluginLogger logger) {
		this.logger = logger;
	}

This function is called by tiled so we can assign our private PluginLogger, logger.

	/**
	 * java.io.FileFilter Interface
	 */
	public boolean accept(File pathname) {
		try {
			String path = pathname.getCanonicalPath();
			if (path.endsWith(".lua")) {
				return true;
			}
		} catch (IOException e) {}
		return false;
	}

I don't know what this does. It seems only relevant to opening map files, which does not apply to a MapWriter. I just copied this code from the svn lua plugin.

    private PluginLogger logger;

This is where we declare our private PluginLogger, logger

Step 1.1: Compiling our plugin

The next step is to compile our plugin. We will be using the ant system, so that our plugin compiles any time that we compile tiled.

Change directory to ~/svn/tiled/plugins. Make a copy of the directory "json" and name it "tutorial" (cp -a json tutorial). Change directory to tutorial and edit "build.xml". Change every instance of "json" to "tutorial". Now do the same for the file MANIFEST.MF. Make sure to match the case (json -> tutorial and JSON -> Tutorial). Rename the directory ~/svn/tiled/plugins/tutorial/src/tiled/plugins/json to tutorial. Now edit the file ~/svn/tiled/plugins/build.xml. Add two lines for tutorial, one for dist and one for clean. Each line should look like the json line above it. Now save the tutorial plugin code, listed above, as ~/svn/tiled/plugins/tutorial/src/tiled/plugins/tutorial/TutorialMapWriter.java. Remove JSON* from that directory.

If you are using the svn copy, you probably have a lot of ".svn" directories in your source archives. Our tutorial plugin is not in the svn repository, so you should remove the .svn directories from ~/svn/tiled/plugins/tutorial. Change directory to ~/svn/tiled/plugins/tutorial, and run "rm -rf `find -name .svn`". Notice the backticks (`).

Now, change directory again to ~/svn/tiled and run ant. If everything goes right, which it rarely does, you should now have compiled your tiled plugin (and tiled itself if it was not already compiled).

Step 1.2: Testing our plugin

Now run tiled by changing directory to ~/svn/tiled/dist and running "java -jar tiled.jar. Make a new map and save it, using your new plugin. Exit tiled and look at the file you saved. It is empty, but at least it exists.

Step 2: Making a useful plugin

Now that you know the basics of writing a tiled plugin, you are going to write something useful. It is going to save in a lua format. The output file will look something like this:

bottom_layer[0] = { 0, 0, 1, 1, 2, 3, 5, 8, ... }
bottom_layer[1] = { ... }
...
bottom_layer[74] = { ... }
...
middle_layer[0] = ...
...
upper_layer[0] = ...
...
obstacle_layer[0] = ...
...

These maps will have 4 layers. The bottom_layer[0] = ... line represents the top row of the bottom layer. bottom_layer[1] is the second row of the bottom layer. The same goes for the middle and upper layers. The obstacle layer is not visible, but describes where the player can and can not move. It's format is the same as the other layers, except that rather than each value in the array corresponding to the id of the tile, it is either 0 or 1, where 0 allows movement, and 1 does not.

Here is the complete TutorialMapWriter code:

// TutorialMapWriter.java
	
package tiled.plugins.tutorial;

import java.io.*;
import java.util.Iterator;

import tiled.io.*;
import tiled.core.*;
import tiled.util.*;
import tiled.mapeditor.selection.SelectionLayer;

public class TutorialMapWriter implements MapWriter
{
	/**
	 * Saves a map to a file.
	 * 
	 * @param map the map to be saved
	 * @param filename the filename of the map file
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, String filename) throws IOException
	{
		writeMap(map, new FileOutputStream(filename));
	}

	/**
	 * Writes a map to an already opened stream. Useful
	 * for maps which are part of a larger binary dataset
	 * 
	 * @param map the Map to be written
	 * @param out the output stream to write to
	 * @throws java.io.IOException
	 */
	public void writeMap(Map map, OutputStream out) throws IOException {
		boolean bottom_layer, middle_layer, upper_layer, obstacle_layer;
		Iterator ml;
		String str;
		MapLayer layer;

		// Before we do anything else, make sure that we have every necessary layer
		ml = map.getLayers();
		bottom_layer = middle_layer = upper_layer = obstacle_layer = false;
		while (ml.hasNext()) {
			layer = (MapLayer)ml.next();
			str = layer.getName();
			if (str.equals("bottom_layer")) bottom_layer = true;
			if (str.equals("middle_layer")) middle_layer = true;
			if (str.equals("upper_layer")) upper_layer = true;
			if (str.equals("obstacle_layer")) obstacle_layer = true;
		}
		
		if (bottom_layer == false || middle_layer == false
		    || upper_layer == false || obstacle_layer == false)
			throw new IOException("Missing layers. Requires bottom_layer, " +
				"middle_layer, upper_layer, and obstacle_layer");

		// Create a writer out of the OutputStream
		writer = new OutputStreamWriter(out);

		// If our map file had a header, we would write it here

		// Call private function writeMapLayer for every layer in the file
		ml = map.getLayers();
		while (ml.hasNext()) {
			layer = (MapLayer)ml.next();
			writeMapLayer(layer);
		}

		// Finish up
		writer.flush();
		writer = null;
	}
	
	/**
	 * Writes a map layer.
	 * @param l the map layer
	 * @throws java.io.IOException
	 */
	private void writeMapLayer(MapLayer l) throws IOException {
		for (int y = 0; y < l.getHeight(); y++) {
			writer.write(l.getName() + "[" + String.valueOf(y) + "] = {");
			for (int x = 0; x < l.getWidth(); x++) {

				Tile tile = ((TileLayer)l).getTileAt(x, y);
				int gid = 0;
				if (tile != null) {
					gid = tile.getGid();
				}

				writer.write(" " + String.valueOf(gid) + ",");
			}
			writer.write(" }\n");
		}
		
		writer.write("\n");
	}
	
	/**
	 * Overload this to write a tileset to an open stream. Tilesets are not
	 * supported by this writer.
	 * 
	 * @param set
	 * @param out
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, OutputStream out) throws Exception {
		logger.error("Tilesets are not supported!");
	}

	/**
	 * Saves a tileset to a file. Tilesets are not supported by this writer.
	 * 
	 * @param set
	 * @param filename the filename of the tileset file
	 * @throws Exception
	 */
	public void writeTileset(TileSet set, String filename) throws Exception {
		logger.error("Tilesets are not supported!");
		logger.error("(asked to write " + filename + ")");
	}

	/**
	 * Lists supported file extensions. This function is used by the editor to
	 * find the plugin to use for a specific file extension.
	 *
	 * @return a comma delimited string of supported file extensions
	 * @throws Exception
	 */
	public String getFilter() throws Exception {
		return "*.lua";
	}

	/**
	 * Returns a short description of the plugin, or the plugin name. This
	 * string is displayed in the list of loaded plugins under the Help menu in
	 * Tiled.
	 *
	 * @return a short name or description
	 */
	public String getName() {
		return "Tiled Tutorial exporter";
	}

	/**
	 * Returns a long description (no limit) that details the plugin's
	 * capabilities, author, contact info, etc.
	 *
	 * @return a long description of the plugin
	 */
	public String getDescription() {
		return
			"This is the Tutorial Plugin. It writes a tiled map in a simple Lua format.\n" +
			"You may distribute this plugin under the terms of the GNU GPLv2.\n";
	}

	/**
	 * Returns the base Java package string for the plugin
	 *
	 * @return String the base package of the plugin
	 */
	public String getPluginPackage() {
		return "Tiled Tutorial Writer Plugin";
	}

	/**
	 * The PluginLogger object passed by the editor when the plugin is called to load
	 * or save a map can be used by the plugin to notify the user of any
	 * problems or messages.
	 *
	 * @param logger
	 */
	public void setLogger(PluginLogger logger) {
		this.logger = logger;
	}

	/**
	 * java.io.FileFilter Interface
	 */
	public boolean accept(File pathname) {
		try {
			String path = pathname.getCanonicalPath();
			if (path.endsWith(".lua")) {
				return true;
			}
		} catch (IOException e) {}
		return false;
	}

	private PluginLogger logger;
	private Writer writer;
}

Explanation of code:

Most of the functions in this code have already been explained above, and I won't repeat those here.

	public void writeMap(Map map, OutputStream out) throws IOException {
		boolean bottom_layer, middle_layer, upper_layer, obstacle_layer;
		Iterator ml;
		String str;
		MapLayer layer;

This is the beginning of writeMap, where we declare the variables that will be used in this function. We will use the layer booleans to make sure that the map the user is trying to save has all necessary layers. ml is our MapLayer iterator. str is our generic string, and layer is the current layer we are working with.

		// Before we do anything else, make sure that we have every necessary layer
		ml = map.getLayers();
		bottom_layer = middle_layer = upper_layer = obstacle_layer = false;
		while (ml.hasNext()) {
			layer = (MapLayer)ml.next();
			str = layer.getName();
			if (str.equals("bottom_layer")) bottom_layer = true;
			if (str.equals("middle_layer")) middle_layer = true;
			if (str.equals("upper_layer")) upper_layer = true;
			if (str.equals("obstacle_layer")) obstacle_layer = true;
		}

This code block makes sure that the current map has every necessary layer. If the layer does exist, then the corresponding _layer variable is set to true. This block uses Map.getLayers(), which returns an iterator to the beginning of the MapLayer list, and MapLayer.getName(), which returns the name of the MapLayer.

	if (bottom_layer == false || middle_layer == false
		    || upper_layer == false || obstacle_layer == false)
			throw new IOException("Missing layers. Requires bottom_layer, " +
				"middle_layer, upper_layer, and obstacle_layer");

The _layer variables were set in the previous code block. If any of the necessary layers are missing, then we throw an IOException. Tiled will catch this exception, and display an error to the user, who can then fix it.

		// Create a writer out of the OutputStream
		writer = new OutputStreamWriter(out);

		// If our map file had a header, we would write it here

Self-explanatory

		// Call private function writeMapLayer for every layer in the file
		ml = map.getLayers();
		while (ml.hasNext()) {
			layer = (MapLayer)ml.next();
			writeMapLayer(layer);
		}

We now reset ml back to the beginning of the MapLayer list, so we can iterate through them again. We call writeMapLayer() for every MapLayer. writeMapLayer() is explained below.

	// Finish up
		writer.flush();
		writer = null;
	}

Self-explanatory

	/**
	 * Writes a map layer.
	 * @param l the map layer
	 * @throws java.io.IOException
	 */
	private void writeMapLayer(MapLayer l) throws IOException {
		for (int y = 0; y < l.getHeight(); y++) {
			writer.write(l.getName() + "[" + String.valueOf(y) + "] = {");
			for (int x = 0; x < l.getWidth(); x++) {

				Tile tile = ((TileLayer)l).getTileAt(x, y);
				int gid = 0;
				if (tile != null) {
					gid = tile.getGid();
				}

				writer.write(" " + String.valueOf(gid) + ",");
			}
			writer.write(" }\n");
		}
		
		writer.write("\n");
	}

This code block loops through each row in a layer, and each column in a layer, and writes the data according to the map format explained above. I introduced a new class, Tile, which represents a single tile in a layer. New functions are MapLayer.getHeight(), MapLayer.getWidth(), MapLayer.getTileAt(x, y), and tile.getGid(). MapLayer.getHeight() and MapLayer.getWidth() return the layer height and width, respectively. Maplayer.getTileAt(x, y) returns the tile at x:y. Tile.getGid() returns the value of the tile.

Testing TutorialMapWriter:

Now compile our complete TutorialMapWriter, and run tiled. Make a new file of any size, and try to save that file as a TutorialMapWriter. You can't, because you haven't defined the necessary layers. Now add the necessary layers, load a tileset, and draw in each layer. Save the file again. Load the saved file in a text editor. Nice.

Exercises

There are some minor bugs/incompleteness in the code above. Nothing to detract from learning to write a tiled plugin. If you feel you understand what you've learned, try fixing them.

License

Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, translate, distribute, and sub-license this documentation for any purpose, including commercial use, as long as you give me credit for this work, and mark modified documents as such. You may incorporate this document, or pieces, into a larger work, as long as you credit me with the part you used.
(C) Brandon Barnes <winterknight@nerdshack.com>