Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungBreeze TTS 2 vs ElevenLabs: Open Source TTS Verdict(23.09.2026 um 05:44 Uhr)
Sichere ProgrammierungAgentic AI vs Generative AI: The 2026 Verdict(23.09.2026 um 05:44 Uhr)
Sichere ProgrammierungI made my agent prove every quote against the source document(23.09.2026 um 05:45 Uhr)
Sichere Programmierung8mb.video Alternative: Skip the Line, Skip the Upsell(23.09.2026 um 05:47 Uhr)
Sichere ProgrammierungBuilding a GTA 6 JSON API for entities and current status(23.09.2026 um 05:52 Uhr)
Sichere ProgrammierungEvery filter needs a documented exception(23.09.2026 um 06:01 Uhr)
Sichere ProgrammierungBreeze TTS 2 vs ElevenLabs: Open Source TTS Verdict(23.09.2026 um 05:44 Uhr)
Sichere ProgrammierungAgentic AI vs Generative AI: The 2026 Verdict(23.09.2026 um 05:44 Uhr)
Sichere ProgrammierungI made my agent prove every quote against the source document(23.09.2026 um 05:45 Uhr)
Sichere Programmierung8mb.video Alternative: Skip the Line, Skip the Upsell(23.09.2026 um 05:47 Uhr)
Sichere ProgrammierungBuilding a GTA 6 JSON API for entities and current status(23.09.2026 um 05:52 Uhr)
Sichere ProgrammierungEvery filter needs a documented exception(23.09.2026 um 06:01 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

More than Maps

Overview In building my retro RPG game, Emberfall, I realized early on that UI was going to be important. Emberfall is inspired by games such as the original Bard's Tale and Dragon Wars. So, I wanted to pay homage to that era by…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Overview



In building my retro RPG game, Emberfall, I realized early on that UI was going to be important. Emberfall is inspired by games such as the original Bard's Tale and Dragon Wars.







So, I wanted to pay homage to that era by retaining a simple text interface with a bump in graphics quality.








Dilemma



Game Engines such as Mini Micro, don't have visual tools to build your game and although it does come with a GUI module, it wasn't feature rich enough for my needs. So, the first thing I did was create a generic GUI system that dealt with the basics of event handling and input. And although that was a great achievement, it wasn't going to be the hard part as I was about to find out.



It took several days to code the Guild Hall menu alone and it only had a handful of Widgets. I realized that it would take a very long time to code every Dialog that I needed especially when some, such as the Character Creation dialog, would require over 100 Widgets on screen. Placing each one by code, running the game to see the visual output and iteratively making adjustments, was killing my productivity. I needed to do this visually. Not to mention that my code was getting cluttered with hundreds of lines just for widget creation. It was making my code ugly.






Data Driven



So, I felt the answer was in creating a data driven UI that could be loaded from JSON files. First, I designed a simple Schema based on the meta data that my current Widgets needed:




{
"type": "Dialog",
"id": "mainPanel",
"x": 368,
"y": 224,
"width": 192,
"height": 160,
"children": [
{
"type": "Label",
"id": "name",
"x": 296,
"y": 296,
"width": 32,
"height": 16,
"text": "Name",
"color": "#100f24",
"alignment": "left"
},
...
}






I created a single factory class that was dedicated to loading the JSON and generating a single Dialog widget with all of its child widgets attached. And this did improve the code quality. Hundreds of lines of boilerplate code were reduced to a single line.






Abusing Tools



However, this was only the first step. I needed to be able to visually build the UI and generate the JSON from that. I went looking for stable UI tools that would fill this gap. I found lots of tools but mostly in the web design space. The few that were game development related had a price tag that was outside my budget range. I had to two options:




  • Create a UI tool

  • Abuse a tool that wasn't meant for UI development



Since creating my own tool would be a significant delay, I decided to look for a tool that I could abuse. Tiled is a well-known level editor that supports various map styles including Object layers which allows the arbitrary placement of objects such as rectangles and text which was perfect for my UI layout needs.





More importantly, Tiled supports custom properties and has a JavaScript API. So, I set about creating a JavaScript command that I could execute from the command palette that could export maps to the JSON needed by my UI.



Here is the code that I used to generate my JSON. It isn't 100% stable but it does work.




/**
* Tiled UI JSON Exporter for Emberfall
*
* Exports Tiled object layers as Emberfall UI Dialog JSON format.
*
* Setup:
* 1. Copy this script to your Tiled extensions folder:
* - macOS: ~/Library/Preferences/Tiled/extensions/
* - Windows: %APPDATA%\Tiled\extensions\
* - Linux: ~/.config/Tiled/extensions/
* 2. Restart Tiled
* 3. Use menu: File > Export as Emberfall UI JSON (top-level menu item, not Export As)
*
* Tiled Map Structure:
* - Ignored layers: Background (visual reference only)
* - Dialog layer: Contains Dialog custom properties (id, x, y, width, height, backgroundImage, isModal)
* - Child objects: Each with "type" property becomes a widget
* - Object text field maps to: "label" (Button/Label) or "text" (TextField)
*
* Coordinate conversion:
* - Tiled uses top-left origin; Emberfall uses bottom-left with center-based coordinates
* - Conversion: cx = obj.x + obj.width/2, cy = (mapHeight - obj.y) + obj.height/2
*/


// Helper for property fallback (avoids nullish-coalescing which may not be supported)
function getProp(obj, key, fallback) {
const val = obj.property(key);
if (val === undefined || val === null || val === "") return fallback;
return val;
}

function exportUIJson(map, fileName) {
const asset = map || tiled.activeAsset;
if (!asset || !asset.isTileMap) {
tiled.log("Error: No tilemap is currently active.");
return false;
}

tiled.log("Exporting tilemap: " + asset.fileName);

const mapHeight = asset.height * asset.tileHeight; // Convert to pixel height

// Find the Dialog layer (skip Background and other layers)
let dialogLayer = null;
for (const layer of asset.layers) {
if (layer.isObjectLayer && layer.name === "Dialog") {
dialogLayer = layer;
break;
}
}

if (!dialogLayer) {
tiled.log("Error: No 'Dialog' object layer found. Create an object layer named 'Dialog' with custom properties.");
return false;
}

// Export the dialog with its children
const dialog = exportLayer(dialogLayer, mapHeight);

if (!dialog) {
tiled.log("Error: Failed to export dialog.");
return false;
}

const json = JSON.stringify(dialog, null, 2);

// Determine output path - write to Emberfall workspace usr/data/interface folder
let outputPath = null;
if (fileName) {
outputPath = fileName;
} else if (asset && asset.fileName) {
try {
const fi = new FileInfo(asset.fileName);
// Use the Emberfall workspace path
outputPath = ".../emberfall/usr/data/interface/" + fi.baseName + "-ui.json";
} catch (e) {
// Fallback to manual parsing if FileInfo isn't available
const file = asset.fileName;
const lastSlash = file.lastIndexOf("/");
const lastDot = file.lastIndexOf(".");
const baseName = (lastSlash >= 0 ? file.substring(lastSlash + 1) : file);
const nameNoExt = lastDot > lastSlash ? baseName.substring(0, baseName.lastIndexOf(".")) : baseName;
outputPath = ".../emberfall/usr/data/interface/" + nameNoExt + "-ui.json";
}
} else {
outputPath = ".../emberfall/usr/data/interface/emberfall-ui.json";
}

// Write to file (commit if supported)
try {
tiled.log("Writing to: " + outputPath);
const outFile = new TextFile(outputPath, TextFile.WriteOnly);
outFile.write(json);
if (typeof outFile.commit === "function") {
outFile.commit();
tiled.log("Committed file to: " + outputPath);
} else {
outFile.close();
tiled.log("Closed file: " + outputPath);
}
tiled.log("UI JSON successfully exported to: " + outputPath);
return true;
} catch (e) {
tiled.log("Error writing file: " + e.message);
return false;
}
}

function exportLayer(layer, mapHeight) {
// Get layer custom properties for Dialog config
const dialogConfig = {
type: "Dialog",
id: getProp(layer, "id", "mainPanel"),
x: getProp(layer, "x", 240) + getProp(layer, "width", 320)/2,
y: (mapHeight - getProp(layer, "y", 160)) - getProp(layer, "height", 240)/2, // getProp(layer, "y", 160)
width: getProp(layer, "width", 320),
height: getProp(layer, "height", 240)
};

// Add optional Dialog properties
const backgroundImage = layer.property("backgroundImage");
if (backgroundImage !== undefined && backgroundImage !== null && backgroundImage !== "") {
dialogConfig.backgroundImage = backgroundImage;
}

const isModal = layer.property("isModal");
if (isModal !== undefined && isModal !== null) {
dialogConfig.isModal = isModal;
}

// Export child objects with "type" property as widgets
const children = [];
for (const obj of layer.objects) {
const type = obj.property("type");
if (type) { // Only export objects with "type" property
const widget = exportObject(obj, mapHeight, type);
if (widget) {
children.push(widget);
}
}
}

dialogConfig.children = children;
return dialogConfig;
}

function exportObject(obj, mapHeight, type) {
const widget = { type: type };


// Standard widget properties from Tiled object
const id = obj.property("id");
if (id !== undefined && id !== null && id !== "") {
widget.id = id;
}

// Convert coordinates: top-left (Tiled) to bottom-left center (Emberfall)
const cx = obj.x + obj.width / 2;
const cy = (mapHeight - obj.y) - obj.height / 2;
widget.x = Math.round(cx);
widget.y = Math.round(cy);
widget.width = Math.round(obj.width);
widget.height = Math.round(obj.height);

// Handle Text property and built-in text formatting - maps to different fields based on widget type
let textContent = null;
let textColor = null;
let textAlignment = null;

// obj.text is a direct string property (not an object)
if (obj.text && typeof obj.text === "string") {
textContent = obj.text;

// Extract text color if available (direct hex string)
if (obj.textColor) {
textColor = obj.textColor;
}

// Extract horizontal text alignment from textAlignment number
// Tiled textAlignment combines vertical and horizontal using Qt flags:
// Horizontal: Left=1, Right=2, HCenter=4
// Vertical: Top=32, VCenter=128, Bottom=64
if (obj.textAlignment !== undefined) {
const align = obj.textAlignment;
// Check which horizontal flag is set
if (align & 4) textAlignment = "center";
else if (align & 2) textAlignment = "right";
else if (align & 1) textAlignment = "left";
}
}
// Check for custom text/label properties as fallback
if (!textContent) {
const textProp = obj.property("text");
const labelProp = obj.property("label");
if (textProp !== undefined && textProp !== null && textProp !== "") {
textContent = textProp;
} else if (labelProp !== undefined && labelProp !== null && labelProp !== "") {
textContent = labelProp;
}
}

if (textContent) {
if (type === "Label" || type === "TextField") {
// For Label and TextField, text goes to "text" property
widget.text = textContent;
} else if (type === "Button") {
// For Button, text goes to "label" property
widget.label = textContent;
}
} else {
// Log warning if text is expected but missing
if (type === "Label" || type === "Button" || type === "TextField") {
tiled.log("Warning: Object '" + (id || "unnamed") + "' (type: " + type + ") has no text property");
}
}

// Apply extracted text color if found
if (textColor) {
widget.color = textColor;
}

// Apply extracted text alignment if found
if (textAlignment) {
widget.alignment = textAlignment;
}

// Widget-specific properties
const textProperties = ["text", "placeholder", "options", "shortcut", "label"];
for (const prop of textProperties) {
const value = obj.property(prop);
if (value !== undefined && value !== null && value !== "") {
// Skip if already set from object text
if (!(prop === "label" && widget.label) && !(prop === "text" && widget.text)) {
widget[prop] = value;
}
}
}

// Optional style properties from custom properties (will override built-in text properties if set)
const styleProperties = ["font", "color", "alignment"];
for (const prop of styleProperties) {
const value = obj.property(prop);
if (value !== undefined && value !== null && value !== "") {
widget[prop] = value;
}
}

// Handle color properties from Tiled (if stored as custom properties)
const colorProps = ["hoverColor", "shortcutColor", "shortcutHoverColor"];
for (const prop of colorProps) {
const value = obj.property(prop);
if (value !== undefined && value !== null && value !== "") {
widget[prop] = value;
}
}

return widget;
}

// Register the export action
var action = tiled.registerAction("exportUIJson", function() {
try {
const result = exportUIJson();
if (result) {
tiled.log("Export completed successfully");
} else {
tiled.log("Export failed - check active tilemap and Dialog layer");
}
} catch (e) {
tiled.log("Export error: " + e.message);
}
});

action.text = "Export Emberfall JSON"

// Log load to debug console
tiled.log("Emberfall UI exporter loaded - search 'Export Emberfall JSON' in command palette");









Conclusion



After making all of these changes which only took a couple of days to put together, I was able to build three dialogs in one day (including the one with over 100 widgets) and export them to my game in a matter of seconds.



This has really streamlined my approach to UI development and so I thought I would share. Maybe you might also benefit from using Tiled for more than maps.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten More than Maps

Thematisch verwandte Begriffe: More, than, Maps · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-18163 | IBM Financial Transaction Manager (FTM) for RedHat OpenShift could allow…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick