I currently work closely with an artist and he often finds it troublesome tweaking UI via code. I figured there’d be a tool out there that would allow you to dynamically import artwork, position them on screen, and then export the layout as XML. Unfortunately I’ve been having one hell of a time finding such a program! Has anyone come across (or created) such a tool?
Interface Designer
I’m assuming this isn’t what you’re looking for - and I’m sure there’s a better answer out there, somewhere - but you could use a program like Adobe Illustrator or Inkscape, which export as SVG. SVG is an open standard based on XML, but it gets messy really fast. Theoretically, you could parse the XML (maybe first parse & clean it up) and then draw your UIs in-game.
Alternatively, this is kind of a wacky idea, and I don’t know how well it would work, but you could try doing something similar in a level editor like Tiled or Ogmo Editor, just basically drawing rectangles and images. Getting them configured and set up to do this might be troublesome, but they both export very clean, readable XML you can use.
I’m thinking that if you consider everything an “Entity” or “Object” or whatever it is rather than a tile, it might work for you.
So basically I don’t have a super easy answer for you, but here’s a couple of ideas. Hopefully someone else will chime in with a smarter/simpler answer.
(PS: if a tool doesn’t exist and you want it, make it! you’re a programmer, right? hack something together!)
I would go with the old-shool aproach. Start with a layout using Pencil or Axure and then code it. Design first, development later. If that’s not it, you should check out http://gleed2d.codeplex.com/. It’s a multi-purpose level editor that can export stuff to XML, has layers and some support for vector objects.
Short answer is that no, there’s no UI designer for flashpunk/flash in general. Not like interface builder in Xcode or eclipse’s swing designer. You’ll need to roll your own. Making one in XML would be pretty easy, but it’d be easier to just use something like OGMO. You can do pretty precise layout in OGMO, and you can use entities to mark where you want your buttons/widgets to appear. Just whip up some OGMO entities for the buttons, and then whip up some code in your flash game to create your UI element according to the layout in the OGMO level XML.
How precise a UI are we talking here? Like is it just some boxes and buttons, or is it like Photoshop level complex?
@mikezila I’m actually using my own port of FP to Monkey, so I wasn’t looking for anything platform specific. I just needed something that exported XML.
@rostok Regarding the “old-school” approach: that workflow doesn’t work for us as we like to iterate on content more than once. Also we feel its more of the artists job to position the elements correctly rather than the programmer.
@JonathanStoler Being a programmer also means that I’d rather reuse code (or in this case a tool) rather than reinventing the wheel. I was already thinking about creating our own editor but wanted to make sure something didn’t already exist, as I feel that it would be a commonly need tool
@ Everyone I had already considered using something like Tiled or Ogmo, but I feel like they don’t provide enough flexibility. Especially with ogmo since to add a new entity or add properties you have to go and edit the project file then reload it, rather than simply changing those properties on the fly. Tiled seems to fit our needs more, but unless I’m missing something, you can only import artwork as tilesets.
We’re simply just looking for a tool where you can load up artwork on the fly, position them on screen, then save/export the UI as XML.
I don’t know of anything like that. Though if all you need is some simple cords I think this is a good opportunity to develop a tool to scratch your specific itch. It should be pretty easy to whip up. I made something very similar in Ruby to edit levels for a game I was making at the time. It didn’t export XML, though, or I’d hook you up
If you don’t mind deciphering it, I lost the source code to a level editor I made, but here is where all the logic was handled (decompiled). It’s a little difficult to read due to the compiling/decompiling removing my local variable names. I used MinimalComps for a basic UI.
Here it is:
package
{
import com.bit101.components.*;
import flash.display.*;
import flash.events.*;
import flash.geom.*;
import flash.net.*;
import net.flashpunk.*;
import net.flashpunk.graphics.*;
import net.flashpunk.utils.*;
public class editorWorld extends World
{
public var loadedMap:FileReference;
public var loadedTiles:FileReference;
public var savePopup:Window;
public var bwindow:Window;
public var spb:Boolean = false;
public var twindow:Window;
public var w:InputText;
public var h:InputText;
public var l:PushButton;
public var _tiles:TilemapEx;
public var tilemapLoaded:Boolean = false;
public var tE:Entity;
public var currentID:int = 0;
public var bmp:BitmapData;
public var bitmap:Bitmap;
public var originalbmp:Bitmap;
public var bmoverlay:BitmapData;
public var firstTime:Boolean = true;
final public static function GetBitmap(param1:*, param2:int = -1258, param3:int = -1258) : Bitmap
{
var _loc_4:Bitmap = null;
var _loc_5:BitmapData = null;
if(param1 is Image)
{
_loc_5 = new BitmapData(param1.width, param1.height, false, 0);
param1.render(_loc_5, FP.zero, FP.zero);
_loc_4 = new Bitmap(_loc_5);
}
else
{
if(param1 is BitmapData)
{
_loc_4 = new Bitmap(param1);
}
}
if(!_loc_4)
{
throw new Error("Invalid source image.");
}
if(param2 != -1258)
{
_loc_4.x = param2;
}
if(param3 != -1258)
{
_loc_4.y = param3;
}
return _loc_4;
}
public function editorWorld()
{
this.w = new InputText(null, 1, 0, "Tile Width");
this.h = new InputText(null, 1, 16, "Tile Height");
this.l = new PushButton(null, 0, 45, "Load Tilemap", this.loadTilemap);
super();
FP.screen.color = 3355443;
this.w.width = this.w.width - 2;
this.h.width = this.h.width - 2;
}
override public function begin() : void
{
this.bwindow = new Window(FP.stage, 50, 50, 100, 61, "Load/Save Window");
this.bwindow.addChild(new PushButton(null, 0, 0, "load", this.loadMapFile));
this.bwindow.addChild(new PushButton(null, 0, 21, "save", this.saveClicked));
this.twindow = new Window(FP.stage, 155, 50, 100, 85, "Tileset");
this.twindow.addChild(this.w);
this.twindow.addChild(this.h);
this.twindow.addChild(this.l);
this.tE = addGraphic(this._tiles, 5);
}
private function saveClicked(param1:Event) : void
{
this.spb = true;
popup("Save files with \nthe .xml extension!");
}
private function loadTilemap(param1:Event) : void
{
var _loc_2:FileFilter = null;
if((this.w.text == "Tile Width") && this.h.text == "Tile Height" && this.w.text == "" && this.h.text == "")
{
this.loadedTiles = new FileReference();
this.loadedTiles.addEventListener(Event.SELECT, this.imgSelected);
_loc_2 = new FileFilter("Images: (*.jpeg, *.jpg, *.gif, *.png)", "*.jpeg; *.jpg; *.gif; *.png");
this.loadedTiles.browse([_loc_2]);
}
else
{
popup("Please input\nWidth and Height.");
}
}
private function imgSelected(param1:Event) : void
{
this.loadedTiles.removeEventListener(Event.SELECT, this.imgSelected);
this.loadedTiles.addEventListener(Event.COMPLETE, this.imgComplete);
this.loadedTiles.load();
}
private function imgComplete(param1:Event) : void
{
this.loadedTiles.removeEventListener(Event.COMPLETE, this.imgComplete);
var _loc_2:Loader = new Loader();
_loc_2.contentLoaderInfo.addEventListener(Event.COMPLETE, this.loadBytesHandler);
_loc_2.loadBytes(this.loadedTiles.data);
}
private function loadBytesHandler(param1:Event) : void
{
var _loc_2:LoaderInfo = param1.target;
_loc_2.removeEventListener(Event.COMPLETE, this.loadBytesHandler);
this.bmp = new BitmapData(_loc_2.content.width, _loc_2.content.height, true, 0);
this.bmp.draw(_loc_2.content);
FP.stage.removeChild(this.twindow);
this.twindow = new Window(FP.stage, 200, 50, 4 + this.bmp.width, 26 + this.bmp.height, "Tileset");
this.bitmap = GetBitmap(new BitmapData(int(this.w.text), int(this.h.text), true, 2287293730.00));
this.bitmap.y = 2;
this.twindow.addChild(GetBitmap(this.bmp, 2, 2));
this.twindow.addChild(this.bitmap);
FP.stage.addChild(this.twindow);
this._tiles = new TilemapEx(this.bmp, 640, 480, int(this.w.text), int(this.h.text), true);
this._tiles.setRect(0, 0, this._tiles.columns, this._tiles.rows);
this.tilemapLoaded = true;
}
public function popup(param1:String = "") : void
{
this.savePopup = new Window(FP.stage, 270, 190, 100, 100, "Popup");
this.savePopup.addChild(new PushButton(null, 0, 60, "OK", this.saveMapFile));
var _loc_2:Text = new Text(null, 0, 0, param1);
_loc_2.selectable = false;
_loc_2.editable = false;
_loc_2.height = 58;
this.savePopup.addChild(_loc_2);
}
public function loadMapFile(param1:Event) : void
{
var _loc_2:FileFilter = null;
if(this.tilemapLoaded)
{
this.loadedMap = new FileReference();
this.loadedMap.addEventListener(Event.SELECT, this.selectHandler);
_loc_2 = new FileFilter("XML files: (*.xml)", "*.xml");
this.loadedMap.browse([_loc_2]);
}
}
private function selectHandler(param1:Event) : void
{
this.loadedMap.removeEventListener(Event.SELECT, this.selectHandler);
this.loadedMap.addEventListener(Event.COMPLETE, this.loadCompleteHandler);
this.loadedMap.load();
}
private function loadCompleteHandler(param1:Event) : void
{
var _loc_3:XML = null;
this.loadedMap.removeEventListener(Event.COMPLETE, this.loadCompleteHandler);
this._tiles.setRect(0, 0, this._tiles.columns, this._tiles.rows);
var _loc_2:XML = new XML(this.loadedMap.data);
var _loc_4:int = 0;
var _loc_5:* = _loc_2.@tile;
for each(_loc_3 in _loc_5)
{
this._tiles.setTile(_loc_3.@col, _loc_3.@row, _loc_3.@id);
}
updateGFX();
}
public function saveMapFile(param1:Event) : void
{
var _loc_2:XML = null;
var _loc_3:int = 0;
var _loc_4:int = 0;
var _loc_5:uint = 0;
var _loc_6:XML = null;
this.spb = false;
if(this.savePopup != null)
{
FP.stage.removeChild(this.savePopup);
}
if(this.tilemapLoaded)
{
_loc_2 = new XML("<level> </level>");
_loc_3 = 0;
while(_loc_3 < this._tiles.rows)
{
_loc_4 = 0;
while(_loc_4 < this._tiles.columns)
{
_loc_5 = this._tiles.getTile(_loc_4, _loc_3);
if(_loc_5 != TilemapEx.EMPTY_TILE)
{
_loc_6 = new XML("<tile/>");
_loc_6.col = _loc_4.toString();
_loc_6.row = _loc_3.toString();
_loc_6.id = _loc_5.toString();
_loc_2.appendChild(_loc_6);
}
_loc_4++;
}
_loc_3++;
}
new FileReference().save(_loc_2, "level.xml");
}
}
override public function update() : void
{
updateGFX();
resetCheck();
if(this.tilemapLoaded)
{
if(this.bitmap)
{
this.bitmap.x = (int(this.currentID % this._tiles.columns)) * int(this.w.text) + 2;
this.bitmap.y = (int(this.currentID / this._tiles.columns)) * int(this.h.text) + 2;
}
if(this.currentID != TilemapEx.EMPTY_TILE)
{
if(Input.pressed(Key.RIGHT))
{
if(this.currentID >= (int((this.bmp.width / int(this.w.text)) + (this.bmp.height / int(this.h.text)))) - 1)
{
this.currentID = TilemapEx.EMPTY_TILE;
}
else
{
this.currentID = this.currentID + 1;
}
}
if(Input.pressed(Key.LEFT))
{
if(this.currentID >= 1)
{
this.currentID = this.currentID - 1;
}
else
{
this.currentID = TilemapEx.EMPTY_TILE;
}
}
}
else
{
if(this.currentID == TilemapEx.EMPTY_TILE)
{
if(Input.pressed(Key.RIGHT))
{
this.currentID = 0;
}
if(Input.pressed(Key.LEFT))
{
this.currentID = (int((this.bmp.width / int(this.w.text)) + (this.bmp.height / int(this.h.text)))) - 1;
}
}
}
}
if(!(pointInWindow(Input.mouseX, Input.mouseY, this.bwindow)) && !(pointInWindow(Input.mouseX, Input.mouseY, this.twindow)) && this.tilemapLoaded)
{
this.spb;
if(this.spb && this.savePopup)
{
if(!(pointInWindow(Input.mouseX, Input.mouseY, this.savePopup)))
{
if(Input.mouseDown)
{
this._tiles.setTile(Input.mouseX / this._tiles.tileWidth, Input.mouseY / this._tiles.tileHeight, this.currentID);
super.update();
}
if(Input.pressed(Key.SPACE))
{
this._tiles.floodFill(Input.mouseX / this._tiles.tileWidth, Input.mouseY / this._tiles.tileHeight, this.currentID);
super.update();
}
}
}
else
{
Input.mouseDown;
if(Input.mouseDown && this.tilemapLoaded)
{
this._tiles.setTile(Input.mouseX / this._tiles.tileWidth, Input.mouseY / this._tiles.tileHeight, this.currentID);
super.update();
}
else
{
if(Input.pressed(Key.SPACE) && this.tilemapLoaded)
{
this._tiles.floodFill(Input.mouseX / this._tiles.tileWidth, Input.mouseY / this._tiles.tileHeight, this.currentID);
super.update();
}
}
}
}
else
{
super.update();
}
}
public function pointInWindow(param1:int, param2:int, param3:Window) : Boolean
{
if(param1 < param3.x || param2 < param3.y || param1 > (param3.x + param3.width) || param2 > (param3.y + param3.height))
{
return false;
}
return true;
}
public function isOnScreen() : Boolean
{
if(Input.mouseX < 0 || Input.mouseY < 0 || Input.mouseX > FP.screen.width || Input.mouseY > FP.screen.height)
{
return false;
}
return true;
}
private function updateGFX() : void
{
this.tE.graphic = this._tiles;
}
private function resetCheck() : void
{
if(Input.pressed(Key.R))
{
remove(this.tE);
this.tilemapLoaded = false;
this.currentID = 0;
FP.stage.removeChild(this.bwindow);
FP.stage.removeChild(this.twindow);
begin();
if(this._tiles)
{
this._tiles.setRect(0, 0, this._tiles.columns, this._tiles.rows);
}
if(this.spb)
{
FP.stage.removeChild(this.savePopup);
}
}
}
override public function render() : void
{
var _loc_1:int = 0;
var _loc_2:int = 0;
var _loc_3:uint = 0;
var _loc_4:uint = 0;
var _loc_5:int = 0;
var _loc_6:int = 0;
super.render();
if(this.tilemapLoaded)
{
_loc_1 = 0;
while(_loc_1 <= this._tiles.width)
{
Draw.linePlus((this._tiles.x + _loc_1) - 1, this._tiles.y, (this._tiles.x + _loc_1) - 1, this._tiles.y + this._tiles.height, 16777215, 0.25);
_loc_1 = _loc_1 + this._tiles.tileWidth;
}
_loc_2 = 0;
while(_loc_2 <= this._tiles.height)
{
Draw.linePlus(this._tiles.x, (this._tiles.y + _loc_2) - 1, this._tiles.x + this._tiles.width, (this._tiles.y + _loc_2) - 1, 16777215, 0.25);
_loc_2 = _loc_2 + this._tiles.tileHeight;
}
_loc_3 = Input.mouseX;
_loc_4 = Input.mouseY;
_loc_5 = (_loc_3 - this._tiles.x) / this._tiles.tileWidth;
_loc_6 = (_loc_4 - this._tiles.y) / this._tiles.tileHeight;
if(_loc_3 > this._tiles.x && _loc_3 < (this._tiles.x + this._tiles.width) && _loc_4 > this._tiles.y && _loc_4 < (this._tiles.y + this._tiles.height))
{
if(this.currentID == TilemapEx.EMPTY_TILE)
{
Draw.rectPlus(this._tiles.x + (_loc_5 * this._tiles.tileWidth), this._tiles.y + (_loc_6 * this._tiles.tileHeight), this._tiles.tileWidth, this._tiles.tileHeight, 13369344, 0.60, true);
}
else
{
Draw.rectPlus(this._tiles.x + (_loc_5 * this._tiles.tileWidth), this._tiles.y + (_loc_6 * this._tiles.tileHeight), this._tiles.tileWidth, this._tiles.tileHeight, 52224, 0.60, true);
}
}
}
}
private function fixTiles(param1:BitmapData) : BitmapData
{
var _loc_2:BitmapData = new BitmapData(param1.width + int(this.w.text), param1.height, false, 8947848);
var _loc_3:Matrix = new Matrix();
if(param1.height <= int(this.h.text))
{
_loc_3.translate(int(this.w.text), 0);
_loc_2.draw(param1, _loc_3);
return _loc_2;
}
return param1;
}
}
}
I think I’m just going to make my own editor in Adobe Air. I will definitely make a post about it here in the creations forum when it gets to a finished state.
Oh…I thought gleed2d was one I had checkout previously but its not. After playing with it for awhile, it seems like we have a winner! Thanks mate!
EDIT: Actually, I need a way to add editable text which gleed2d does not support.
And what about good old flash? just put some UI elements on the stage and have one additional movie clip to scan them and output xml.