Kabosu - Creando cosas

Logo de la página. Gato esférico con colores verdoso.

Reto Open Source: Tiled

Publicado: 2025-10-27

Etiquetas: Git, Reto Open Source, Software


Buenos días, tardes o noches:

En este primer Reto Open Source voy a comentar unos ejercicios que hice con Tiled, un editor de niveles con licencia libre y que utiliza la librería QT para la interfaz gráfica. La primera experiencia que tuve con el programa fue hace más de 10 años cuando lo usé para crear los niveles de mi juego Tiburcio's Adventure.

Preparativos

Los primero era bajarse el código del programa. Está en Github así que es sencillo:

git clone https://github.com/mapeditor/tiled.git

Luego tuve que bajarme las dependencias y compilar el programa. En el fichero README.md del repositorio están todas las instrucciones. Se compila y ejecuta el programa con:

qbs run -p tiled

Captura de pantalla de Tiled

Ejercicio 1a: Good Morning Vietnam

Para asegurarme de que estaba ejecutando realmente el código compilado por mí necesitaba añadir algún mensaje simple. Desde mi adolescencia en vez de "Hola, mundo" suelo poner "Good Morning Vietnam!" así que esta vez no iba a ser diferente.

Busqué cuál podría ser la primera función que se ejecuta al iniciar el programa. Decidí poner mi mensaje en el constructor de la ventana principal de la aplicación (fichero src/tiled/mainwindow.cpp)

Allí metí mi línea

printf("\n\nGOOD MORNING VIETNAM!\n\n");

y sorprendentemente funcionó. Al recompilar y abrir la aplicación pude ver mi mensaje en la terminal.

Ejercicio 1b: Mensaje de debug

printf está muy bien pero descubrí que QT ofrece su propia forma de escribir logs con qDebug así que me puse a probarlo.

En la función resizeMap del mismo fichero que antes añadí

qDebug() << "Resize Map clicked! ";

Funcionó bien. No tenía mayores dificultades.

Ejercicio 2: Cambiar el título de la ventana de la aplicación

Mi siguiente ejercicio era ver algún cambio hecho por mí en la aplicación en sí misma y no en la terminal. El título de la ventana de la aplicación me pareció un buen lugar para dejar mi huella.

Busqué cómo se definía el título de la aplicación y encontré la función updateWindowTitle en el mismo fichero que había usado en los ejercicios anteriores. Esta función concatena el fichero que tengas abierto con el nombre del proyecto y el nombre de la aplicación "Tiled". Al final usaba setWindowTitle para poner esa cadena texto como título.

Borré todo el contenido de la función y la dejé como

setWindowTitle(tr("Tiled - Good Morning Vietnam!"));

Aprendí que en QT puedes usar la función tr() para obtener la traducción de la cadena que le pasas al idioma del sistema. Las funciones de QT como setWindowTitle() reciben un objeto de tipo QString en vez de String y tr hace la conversión por mí.

Captura de pantalla de Tiled con el título cambiado

Ejercicio 3: Nueva opción de menú

Al día siguiente me propuse hacer algo un poco más complicado: añadir una nueva opción al menú de la barra superior. Como hago en estos casos, busqué una candidata de la que copiarme. Elegí la opción "About Tiled"

El ejercicio parecía trivial a priori: declarar una función nueva en mainwindow.h, definirla y conectar al botón del menú en mainwindow.cpp.

connect(mUi->actionHello, &QAction::triggered, this, &MainWindow::showHelloDialog);

...

void MainWindow::showHelloDialog() {
    QMessageBox::information(this, tr("Hello"), tr("Good morning Vietnam!"));
}

Pero, ¿de dónde salía ese mUi->actionHello? No sabía dónde se definía ni de dónde venían sus elementos. Es lo que tiene no saber absolutamente de programación en QT. Una búsqueda rápida en internet me dio la solución: QT viene con un programa llamado designer que permite editar visualmente cómo se ve la aplicación. Los menús de la ventana se definen ahí. Abrí el fichero mainwindows.ui con este editor y pude añadir el nuevo menú "Hello".

Captura de pantalla de QT Designed

Tras actualizar la ventana con Designer ya funcionaba mi nuevo menú.

Ejercicio 4: Nuevo atributo para los mapas

Hasta ahora más que aprender cómo funciona Tiled estaba aprendiendo sobre QT. Que no está mal saber cosas nuevas pero el objetivo de este reto era Tiled.

Los mapas tiene diferentes propiedades: tamaño del mapa (o si es infinito), tamaño de los tiles, orientación... Mi siguiente ejercicio fue añadir una propiedad nueva. A falta de alguna idea graciosa la llamé "helloWorld".

Para ello añadí la definición a map.h:

bool helloWorld;

Luego en map.cpp puse un print para asegurarme de que había guardado el valor de la propiedad correctamente.

Finalmente estuve analizando cómo se guardaban las propiedades que ya existen e hice unos cambios en newmapdialog.cpp que es la ventana desde la que se configuran los mapas.

Me he dado cuenta de que no terminé el ejercicio porque no modifiqué la ventana de nuevo mapa para que hubiera un checkbox para este atributo así que siempre tenía el valor "true". Tampoco modifiqué el menú de edición para poder cambiar el valor una vez creado el mapa. Trabajo futuro.

Ejercicio 5: Simulando un cambio real

Para el siguiente (y último ejercicio) abrí la página de releases de Tiled. Por suerte cada versión tiene una lista de los cambios realizados. Había varios bugs que podrían ser interesantes. Por ejemplo "Fixed crash when closing the last file with multiple custom properties selected". Seguramente sea un cambio de muy pocas líneas. Me llamó la atención que casi todas las versiones tenían un montón de cambios que empezaban con "Scripting". No sabía que Tiled suportaba scripting y decidí simular uno de esos commits para aprender.

Estos son los títulos de los commits de "Scripting" de Tiled 1.11.0. Parece que son todos de exponer campos de la aplicación al módulo de scripting. No deberían ser muy complicados.

Scripting: Added API for working with worlds (with dogboydog, #3539)
Scripting: Added Object.setProperty overload for setting nested values
Scripting: Added Tile.image for accessing a tile's image data
Scripting: Added Image.copy overload that takes a rectangle
Scripting: Added Tileset.imageFileName and ImageLayer.imageFileName
Scripting: Added FilePath.localFile and FileEdit.fileName (string alternatives to Qt.QUrl properties)
Scripting: Added tiled.color to create color values
Scripting: Made Tileset.margin and Tileset.tileSpacing writable
Scripting: Restored compatibility for MapObject.polygon (#3845)
Scripting: Fixed issues with editing properties after setting class values from script
Scripting: Fixed setting/getting object reference values when nested as a class member

Decidí simular el de "Scripting: Added Tileset.imageFileName and ImageLayer.imageFileName" aunque, ahora que me doy cuenta, realmente solo implementé Tileset.imageFileName. El otro se me olvidó. Parecía sencillo. Seguramente el nombre de fichero sea una cadena que esté en el tileset, pensé.

Antes de ponerme con el ejercicio me puse a leer sobre cómo se hacía scripting para Tiled. Al parecer era en Javascript o Typescript y había poquísima información al respecto. Al final encontré un ejemplo y a partir de ahí me hice un pequeño "hola, mundo". Descubrí que para añadir scripting a un proyecto de hay que crear un directorio extensions/ y ahí meter los ficheros Javascript. Tiled los ejecutará automáticamente.

Esta fue mi extensión de prueba. Conseguí crear una nueva opción de menú tal y cómo había hecho en el ejercicio 3 pero esta vez en Javascript. Luego añadí un montón de mensajes de texto para intentar averiguar qué estructura tenían los objetos como openAssets.

const action = tiled.registerAction("CustomAction", function(action) {
    tiled.log(action.text + " was " + (action.checked ? "checked" : "unchecked"))
})

action.text = "My Custom Action"
action.checkable = true
action.shortcut = "Ctrl+K"

tiled.extendMenu("Edit", [
    { action: "CustomAction", before: "SelectAll" },
    { separator: true }
]);

tiled.log("Hi!")
tiled.log(tiled.project.fileName)
tiled.log(Tileset.fileName)
tiled.log(TileMap.fileName)

for (var a in tiled.openAssets) {
    var asset = tiled.openAssets[a]
    tiled.log(asset.fileName)
}

Tras este pequeño aprendizaje me lancé a modificar el código para añadir Tileset.imageFileName. El primer paso era buscar un commit anterior a ese cambio (281d1e0) y obtener esa versión del código. Era un commit de diciembre de 2023 y el autor de Tiled implementó imageFileName en enero de 2024. No había mucha diferencia. Pensaba que había cogido el commit inmediatamente anterior pero al escribir este artículo me he dado cuenta de que no.

Tardé varios días en hacer este ejercicio porque no conseguí que lo que yo hacía se reflejase en el entorno de scripting. Al final descubrí que yo estaba haciendo cambios en tileset.h y realmente los debía de hacer en editabletileset.h. Una vez me di cuenta de esto ya fue todo muy rápido.

Usé como referencia la documentación de la API

imageFileName: string

The file name of the image used by this tileset. Empty in case of image collection tilesets.

note You'll want to set up the tile size, tile spacing, margin and transparent color as appropriate before setting this property, to avoid repeatedly setting up the tiles in response to changing parameters.
not/ Map files are supported tileset image source as well.
since 1.11

Ni idea de qué es eso de "image collection tilesets" pero había una función que se llamaba isCollection() así que la usé:

inline const QString Tileset::imageFileName() {
    if (this->isCollection()) {
        return QString::fromStdString("What?");
    }
    return mImageReference.source.toString();
}

Otra cosa que tuve que averiguar mediante prueba y error es que el nombre del fichero se encontraba en el campo "source" de la imagen de referencia.

inline const QString EditableTileset::imageFileName() const
{
    return tileset()->imageFileName();
}

Aún así no funcionaba. Me faltaba definir la interfaz Javascript con Q_PROPERTY:

Q_PROPERTY(QString imageFileName READ imageFileName)

Y ya estaba. Creo que tardé 3 o 4 horas en total a lo largo de una semana hasta descubrir cómo hacerlo. Hice un script que mostraba los ficheros que tenía abiertos y funcionaba perfectamente.

tiled.log('Hello, world!');

tiled.log(tiled.openAssets);
for (var i in tiled.openAssets) {
    var a = tiled.openAssets[i];
    if (!a.isTileset) {
        tiled.log('Not a tileset');
        continue;
    }
    tiled.log('class name:' + a.className);
    tiled.log('name')
    tiled.log(a.name);
    tiled.log('imageFileName')
    tiled.log(a.imageFileName);
    tiled.log('----------')
}

Finalizado el ejercicio solo quedaba comparar mi solución con la implementación del autor de Tiled. Para ello busqué el commit que él hizo.

En ese commit parecen haber más cámbios aparte de hacer accesible la propiedad imageFileName en el entorno de scripting. Además me di cuenta de que yo solo implementé la opción de leer el nombre del fichero pero no se me ocurrió implementar la escritura. En el commit real están ambas opciones. El autor también reusa una función que ya existía en vez de escribir una como he hecho yo. Quitando eso creo que mi solución no estaba demasiado mal.

Conclusiones

Me he divertido bastante con este "reto" y he aprendido algo tanto de Tiled como de QT. He visto que también hay scripting en Python y creo que lo voy a probar en un futuro.

Respecto al último ejercicio. Creo que cuando haga ese tipo de simulaciones he de leer al menos el mensaje del commit antes de empezar. Puede que eso dé bastantes pistas de la solución ya pero al menos me permitirá escribir algo más cercano a lo que realmente se implementó y me será más fácil comparar mi solución con el código oficial.


Artículo anterior: Retos Open Source