diff --git a/com/haxepunk/tmx/TmxCollisionObject.hx b/com/haxepunk/tmx/TmxCollisionObject.hx new file mode 100644 index 0000000..aa3ce75 --- /dev/null +++ b/com/haxepunk/tmx/TmxCollisionObject.hx @@ -0,0 +1,21 @@ +package com.haxepunk.tmx; + +import com.haxepunk.tmx.TmxVec4; + +/** + * ... + * @author Jero + */ +class TmxCollisionObject extends TmxVec4 +{ + public var type:String; + public var radius:Int; + public var polyPoints:Array; + public function new(x:Float, y:Float, width:Float, height:Float, type:String = "box", radius:Int = 0, polyPoints:Array = null) + { + this.type = type; + this.radius = radius; + this.polyPoints = polyPoints; + super(x, y, width, height); + } +} \ No newline at end of file diff --git a/com/haxepunk/tmx/TmxEntity.hx b/com/haxepunk/tmx/TmxEntity.hx index 43d4b36..30f4b35 100644 --- a/com/haxepunk/tmx/TmxEntity.hx +++ b/com/haxepunk/tmx/TmxEntity.hx @@ -9,20 +9,28 @@ import com.haxepunk.masks.Grid; import com.haxepunk.masks.SlopedGrid; import com.haxepunk.masks.Masklist; import com.haxepunk.tmx.TmxMap; +import com.haxepunk.tmx.TmxTileSet; +import com.haxepunk.tmx.TmxCollisionObject; +import com.haxepunk.masks.Circle; +import com.haxepunk.masks.Hitbox; +import com.haxepunk.masks.Polygon; +import flash.geom.Rectangle; +import com.haxepunk.graphics.atlas.TileAtlas; -private abstract Map(TmxMap) + +private abstract TmxMapData(TmxMap) { private inline function new(map:TmxMap) this = map; @:to public inline function toMap():TmxMap return this; @:from public static inline function fromString(s:String) - return new Map(new TmxMap(Xml.parse(openfl.Assets.getText(s)))); + return new TmxMapData(new TmxMap(Xml.parse(openfl.Assets.getText(s)))); @:from public static inline function fromTmxMap(map:TmxMap) - return new Map(map); + return new TmxMapData(map); @:from public static inline function fromMapData(mapData:MapData) - return new Map(new TmxMap(mapData)); + return new TmxMapData(new TmxMap(mapData)); } class TmxEntity extends Entity @@ -31,7 +39,7 @@ class TmxEntity extends Entity public var map:TmxMap; public var debugObjectMask:Bool; - public function new(mapData:Map) + public function new(mapData:TmxMapData) { super(); @@ -88,6 +96,158 @@ class TmxEntity extends Entity } } + /** + * Loads all graphics for the layers in layerNames and, optionally, loads collision information from tiles and sets masks with that + * @param layerNames layers you want to show + * @param skip gid of tiles that should be skipped. + * @param collideLayer the name of the layer to collide with. (ignored if addTileMask = false) + * @param addTileMask if we should process tile collision information and generate a mask. + * @param typeName the type to set for masks. + */ + public function loadGraphics(layerNames:Array, skip:Array = null, collideLayer:String = "collide", addTileMask:Bool = false, typeName:String = "solid"):Void + { + var gid:Int; + var layer:TmxLayer; + var ml:Masklist = null; + var tilesCollisions:Array = new Array(); + var tileMaps:TmxOrderedHash = new TmxOrderedHash(); + + for (name in layerNames) + { + if (map.layers.exists(name) == false) + { +#if debug + trace("Layer '" + name + "' doesn't exist"); +#end + continue; + } + layer = map.layers.get(name); + if (!layer.visible) + { +#if debug + trace("Layer '" + name + "' is not visible."); +#end + continue; + } + for (row in 0...layer.height) + { + for (col in 0...layer.width) + { + gid = layer.tileGIDs[row][col]; + if (gid < 0) continue; + var set = map.getGidOwner(gid); + if (set == null) + continue; + gid -= set.firstGID; + + if (gid < 0) continue; + + if (!tileMaps.exists( name + "_" + set.name)) + { + #if flash + var _tileset = set.image; + #else + var _tileset = new TileAtlas(set.image); + #end + var tm:TmxTilemap = new TmxTilemap(_tileset, map.fullWidth, map.fullHeight, map.tileWidth, map.tileHeight, set.spacing, set.spacing, set.offsetX, set.offsetY); + tileMaps.set(name + "_" + set.name, tm); + } + + if (skip == null || Lambda.has(skip, gid) == false) + { + if (addTileMask && name == collideLayer) + { + var tileMask:Array = set.getCollisionsByGid(gid); + if (tileMask != null && tileMask.length > 0) + { + var rect:Rectangle = new Rectangle(x + (col * set.tileWidth), y + (row * set.tileHeight), set.tileWidth, set.tileHeight); + for (colObj in tileMask) + { + var _colObj:TmxCollisionObject = new TmxCollisionObject(colObj.x + rect.x, colObj.y + rect.y, colObj.width, colObj.height, colObj.type, colObj.radius, colObj.polyPoints); + if (_colObj.type == "poly") + { + var pArr:Array = new Array(); + var even:Bool = true; + for (p in _colObj.polyPoints) + { + pArr.push(p + (even ? rect.x : rect.y)); + even = !even; + } + _colObj.polyPoints = pArr; + } + tilesCollisions.push(_colObj); + } + } + else + { + #if debug + trace("You have a tile in layer " + name + " with id: " + gid + " that has no collision setup"); + #end + } + } + tileMaps.get(name + "_" + set.name).setTile(col, row, gid); + } + } + } + for (tm in tileMaps) + { + addGraphic(tm); + } + } + if (addTileMask && tilesCollisions.length > 0) + { + var co:TmxCollisionObject; + var masks:Array = new Array(); + for (co in tilesCollisions) + { + switch (co.type) + { + case "circle": + masks.push(new Circle(co.radius, Std.int(co.x), Std.int(co.y))); + #if debug + trace("Adding circle mask"); + if (debugObjectMask) + { + var c = Image.createCircle(co.radius, 0xFF0000, 0.5); + c.x = Std.int(co.x); + c.y = Std.int(co.y); + addGraphic(c); + } + #end + case "poly": + var p:Polygon = Polygon.createFromArray(co.polyPoints); + p.x = 0; + p.y = 0; + masks.push(p); + #if debug + trace("Adding poly mask"); + if (debugObjectMask) + { + var i:Image = Image.createPolygon(p, 0xFF0000, 0.5); + addGraphic(i); + } + #end + case "box": + masks.push( new Hitbox(Std.int(co.width), Std.int(co.height), Std.int(co.x), Std.int(co.y))); + #if debug + trace("Adding box mask"); + if (debugObjectMask) + { + var i:Image = Image.createRect(Std.int(co.width), Std.int(co.height), 0xFF0000, 0.5); + i.x = Std.int(co.x); + i.y = Std.int(co.y); + addGraphic(i); + } + #end + } + } + ml = new Masklist(masks); + mask = ml; + this.type = typeName; + setHitbox(map.fullWidth, map.fullHeight); + } + } + public function loadMask(collideLayer:String = "collide", typeName:String = "solid", skip:Array = null) { var tileCoords:Array = new Array(); @@ -123,7 +283,7 @@ class TmxEntity extends Entity setHitbox(grid.width, grid.height); return tileCoords; } - + public function loadSlopedMask(collideLayer:String = "collide", typeName:String = "solid", skip:Array = null) { if (!map.layers.exists(collideLayer)) @@ -133,12 +293,12 @@ class TmxEntity extends Entity #end return; } - + var gid:Int; var layer:TmxLayer = map.layers.get(collideLayer); var grid = new SlopedGrid(map.fullWidth, map.fullHeight, map.tileWidth, map.tileHeight); var types = Type.getEnumConstructs(TileType); - + for (row in 0...layer.height) { for (col in 0...layer.width) @@ -170,7 +330,7 @@ class TmxEntity extends Entity } } } - + this.mask = grid; this.type = typeName; setHitbox(grid.width, grid.height); diff --git a/com/haxepunk/tmx/TmxLayer.hx b/com/haxepunk/tmx/TmxLayer.hx index 80d6315..8ec6d93 100644 --- a/com/haxepunk/tmx/TmxLayer.hx +++ b/com/haxepunk/tmx/TmxLayer.hx @@ -22,7 +22,7 @@ class TmxLayer public var visible:Bool; public var tileGIDs:Array>; public var properties:TmxPropertySet; - + public function new(source:Fast, parent:TmxMap) { properties = new TmxPropertySet(); @@ -30,16 +30,16 @@ class TmxLayer name = source.att.name; x = (source.has.x) ? Std.parseInt(source.att.x) : 0; y = (source.has.y) ? Std.parseInt(source.att.y) : 0; - width = Std.parseInt(source.att.width); - height = Std.parseInt(source.att.height); - visible = (source.has.visible && source.att.visible == "1") ? true : false; + width = Std.parseInt(source.att.width); + height = Std.parseInt(source.att.height); + visible = (source.has.visible && source.att.visible == "1") || !source.has.visible ? true : false; opacity = (source.has.opacity) ? Std.parseFloat(source.att.opacity) : 0; - + //load properties var node:Fast; for (node in source.nodes.properties) properties.extend(node); - + //load tile GIDs tileGIDs = []; var data:Fast = source.node.data; @@ -87,7 +87,7 @@ class TmxLayer } } } - + public function toCsv(tileSet:TmxTileSet = null):String { var max:Int = 0xFFFFFF; @@ -113,7 +113,7 @@ class TmxLayer } return result; } - + /* ONE DIMENSION ARRAY public static function arrayToCSV(input:Array, lineWidth:Int):String { @@ -131,7 +131,7 @@ class TmxLayer return result; } */ - + private static function csvToArray(input:String):Array> { var result:Array> = new Array>(); @@ -149,7 +149,7 @@ class TmxLayer } return result; } - + private static function base64ToArray(chunk:String, lineWidth:Int, compressed:Bool):Array> { var result:Array> = new Array>(); @@ -158,11 +158,11 @@ class TmxLayer { #if (js && !format) throw "Need the format library to use compressed map on html5"; - #else + #else data.uncompress(); #end } - + data.endian = Endian.LITTLE_ENDIAN; while(data.position < data.length) { @@ -174,10 +174,10 @@ class TmxLayer } return result; } - + private static inline var BASE64_CHARS:String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - private static function base64ToByteArray(data:String):ByteArray + + private static function base64ToByteArray(data:String):ByteArray { var output:ByteArray = new ByteArray(); //initialize lookup table @@ -187,22 +187,22 @@ class TmxLayer { lookup[BASE64_CHARS.charCodeAt(c)] = c; } - + var i:Int = 0; - while (i < data.length - 3) + while (i < data.length - 3) { // Ignore whitespace if (data.charAt(i) == " " || data.charAt(i) == "\n") { i++; continue; } - + //read 4 bytes and look them up in the table var a0:Int = lookup[data.charCodeAt(i)]; var a1:Int = lookup[data.charCodeAt(i + 1)]; var a2:Int = lookup[data.charCodeAt(i + 2)]; var a3:Int = lookup[data.charCodeAt(i + 3)]; - + // convert to and write 3 bytes if(a1 < 64) output.writeByte((a0 << 2) + ((a1 & 0x30) >> 4)); @@ -210,10 +210,10 @@ class TmxLayer output.writeByte(((a1 & 0x0f) << 4) + ((a2 & 0x3c) >> 2)); if(a3 < 64) output.writeByte(((a2 & 0x03) << 6) + a3); - + i += 4; } - + // Rewind & return decoded data output.position = 0; return output; diff --git a/com/haxepunk/tmx/TmxMap.hx b/com/haxepunk/tmx/TmxMap.hx index 88b5697..84786e6 100644 --- a/com/haxepunk/tmx/TmxMap.hx +++ b/com/haxepunk/tmx/TmxMap.hx @@ -46,6 +46,7 @@ class TmxMap public var layers:TmxOrderedHash; public var imageLayers:Map; public var objectGroups:TmxOrderedHash; + public static var patternsPath:String = "maps/"; public function new(data:MapData) { @@ -102,7 +103,7 @@ class TmxMap for (node in source.nodes.objectgroup) objectGroups.set(node.att.name, new TmxObjectGroup(node, this)); - // for (node in source.nodes.imagelayer) + //for (node in source.nodes.imagelayer) // imageLayers.set(node.att.name, new TmxImageLayer(node)); } @@ -133,7 +134,7 @@ class TmxMap } return null; } - + public function getGidProperty(gid:Int, property:String) { var last:TmxTileSet = null; diff --git a/com/haxepunk/tmx/TmxObject.hx b/com/haxepunk/tmx/TmxObject.hx index ca82890..cf91f9d 100644 --- a/com/haxepunk/tmx/TmxObject.hx +++ b/com/haxepunk/tmx/TmxObject.hx @@ -4,6 +4,14 @@ * For questions mail me at heardtheword@gmail.com ******************************************************************************/ package com.haxepunk.tmx; +import com.haxepunk.graphics.Image; +import com.haxepunk.graphics.TiledImage; +import com.haxepunk.masks.Masklist; +import com.haxepunk.masks.Polygon; +import com.haxepunk.math.Vector; +import flash.display.BitmapData; +import flash.geom.Point; +import flash.geom.Rectangle; import haxe.xml.Fast; import com.haxepunk.masks.Hitbox; @@ -20,11 +28,12 @@ class TmxObject public var custom:TmxPropertySet; public var shared:TmxPropertySet; public var shapeMask:Hitbox; + public var image:BitmapData; #if debug public var debug_graphic:com.haxepunk.graphics.Image; #end - + public function new(source:Fast, parent:TmxObjectGroup) { group = parent; @@ -32,23 +41,38 @@ class TmxObject type = (source.has.type) ? source.att.type : ""; x = Std.parseInt(source.att.x); y = Std.parseInt(source.att.y); - width = (source.has.width) ? Std.parseInt(source.att.width) : 0; - height = (source.has.height) ? Std.parseInt(source.att.height) : 0; - //resolve inheritence + width = (source.has.width) ? Std.parseInt(source.att.width) : parent.map.tileWidth; //When object hast tile association width/height are not set by Tiled + height = (source.has.height) ? Std.parseInt(source.att.height) : parent.map.tileHeight; //When object hast tile association width/height are not set by Tiled + //resolve inheritance shared = null; gid = -1; if(source.has.gid && source.att.gid.length != 0) //object with tile association? { + y -= parent.map.tileHeight; //BUG: Tiled uses wrong row number when object is graphic gid = Std.parseInt(source.att.gid); var set:TmxTileSet; for (set in group.map.tilesets) { shared = set.getPropertiesByGid(gid); - if(shared != null) + if (shared != null) + { + if (set.image != null) + { + var r:Rectangle = set.getRect(set.fromGid(gid)); + r.x = HXP.round(r.x, 0); + r.y = HXP.round(r.y, 0); + image = new BitmapData(set.tileWidth, set.tileHeight); + image.setVector(new Rectangle(0, 0, set.tileWidth, set.tileHeight), set.image.getVector(r)); + } break; + } } } - + if (shared != null && type == "" && shared.resolve("type") != null) + { + type = shared.resolve("type"); + } + //load properties var node:Xml; custom = new TmxPropertySet(); @@ -56,7 +80,26 @@ class TmxObject custom.extend(node); // create shape, cannot do ellipses, only circles - if(source.hasNode.ellipse){ + if (source.hasNode.polygon) + { + var points:Array = source.node.polygon.att.points.split(" "); + var pArr:Array = new Array(); + for (pS in points) { + pArr.push( x + Std.parseFloat(pS.split(",")[0])); + pArr.push( y + Std.parseFloat(pS.split(",")[1])); + } + shapeMask = com.haxepunk.masks.Polygon.createFromArray(pArr); +#if debug + var p = com.haxepunk.masks.Polygon.createFromArray(pArr); + p.x = x; + p.y = y; + debug_graphic = com.haxepunk.graphics.Image.createPolygon(p, 0xff0000, .6); + debug_graphic.x = x; + debug_graphic.y = y; +#end + + } + else if(source.hasNode.ellipse){ var radius = Std.int(((width < height)? width : height)/2); shapeMask = new com.haxepunk.masks.Circle(radius, x, y); @@ -65,9 +108,8 @@ class TmxObject debug_graphic.x = x; debug_graphic.y = y; #end - }else{ // rect + }else { // rect shapeMask = new com.haxepunk.masks.Hitbox(width, height, x, y); - #if debug debug_graphic = com.haxepunk.graphics.Image.createRect(width, height, 0xff0000, .6); debug_graphic.x = x; diff --git a/com/haxepunk/tmx/TmxObjectGroup.hx b/com/haxepunk/tmx/TmxObjectGroup.hx index fa89b8f..d37a2e1 100644 --- a/com/haxepunk/tmx/TmxObjectGroup.hx +++ b/com/haxepunk/tmx/TmxObjectGroup.hx @@ -18,26 +18,26 @@ class TmxObjectGroup public var visible:Bool; public var properties:TmxPropertySet; public var objects:Array; - + public function new(source:Fast, parent:TmxMap) { properties = new TmxPropertySet(); objects = new Array(); - + map = parent; name = source.att.name; x = (source.has.x) ? Std.parseInt(source.att.x) : 0; y = (source.has.y) ? Std.parseInt(source.att.y) : 0; - width = Std.parseInt(source.att.width); - height = Std.parseInt(source.att.height); + width = source.has.width ? Std.parseInt(source.att.width) : 1; + height = source.has.height ? Std.parseInt(source.att.height) : 1; visible = (source.has.visible && source.att.visible == "1") ? true : false; opacity = (source.has.opacity) ? Std.parseFloat(source.att.opacity) : 0; - + //load properties var node:Fast; for (node in source.nodes.properties) properties.extend(node); - + //load objects for (node in source.nodes.object) objects.push(new TmxObject(node, this)); diff --git a/com/haxepunk/tmx/TmxTileSet.hx b/com/haxepunk/tmx/TmxTileSet.hx index 6889221..3dcee3e 100644 --- a/com/haxepunk/tmx/TmxTileSet.hx +++ b/com/haxepunk/tmx/TmxTileSet.hx @@ -5,10 +5,13 @@ ******************************************************************************/ package com.haxepunk.tmx; +import com.haxepunk.Mask; +import com.haxepunk.masks.Masklist; import flash.display.BitmapData; import flash.geom.Rectangle; import flash.utils.ByteArray; import haxe.xml.Fast; +import openfl.Assets; abstract TileSetData(Fast) { @@ -27,6 +30,7 @@ abstract TileSetData(Fast) class TmxTileSet { private var _tileProps:Array; + private var _tileCollisions:Array>; private var _image:BitmapData; public var firstGID:Int; @@ -36,6 +40,9 @@ class TmxTileSet public var spacing:Int=0; public var margin:Int=0; public var imageSource:String; + public var offsetX:Int; + public var offsetY:Int; + public static var autoLoadImage:Bool = true; //available only after image has been assigned: public var numTiles:Int; @@ -47,40 +54,94 @@ class TmxTileSet var node:Fast, source:Fast; numTiles = 0xFFFFFF; numRows = numCols = 1; + offsetX = offsetY = 0; source = data; firstGID = (source.has.firstgid) ? Std.parseInt(source.att.firstgid) : 1; - // check for external source if (source.has.source) { + source = new Fast(Xml.parse(Assets.getText(TmxMap.patternsPath + source.att.source))); + source = source.node.tileset; + //trace(source.node.image); + } + + node = source.node.image; + imageSource = node.att.source; + name = source.att.name; + if (source.has.tilewidth) tileWidth = Std.parseInt(source.att.tilewidth); + if (source.has.tileheight) tileHeight = Std.parseInt(source.att.tileheight); + if (source.has.spacing) spacing = Std.parseInt(source.att.spacing); + if (source.has.margin) margin = Std.parseInt(source.att.margin); + if (source.hasNode.tileoffset) + { + offsetX = Std.parseInt(source.node.tileoffset.att.x); + offsetY = Std.parseInt(source.node.tileoffset.att.y); } - else // internal + + //read properties + _tileProps = new Array(); + _tileCollisions = new Array>(); + for (node in source.nodes.tile) { - var node:Fast = source.node.image; - imageSource = node.att.source; - - name = source.att.name; - if (source.has.tilewidth) tileWidth = Std.parseInt(source.att.tilewidth); - if (source.has.tileheight) tileHeight = Std.parseInt(source.att.tileheight); - if (source.has.spacing) spacing = Std.parseInt(source.att.spacing); - if (source.has.margin) margin = Std.parseInt(source.att.margin); - - //read properties - _tileProps = new Array(); - for (node in source.nodes.tile) + if (node.has.id) { - if (node.has.id) + var id:Int = Std.parseInt(node.att.id); + _tileProps[id] = new TmxPropertySet(); + for (prop in node.nodes.properties) + _tileProps[id].extend(prop); + + if (node.hasNode.objectgroup) { - var id:Int = Std.parseInt(node.att.id); - _tileProps[id] = new TmxPropertySet(); - for (prop in node.nodes.properties) - _tileProps[id].extend(prop); + var _og:Fast = node.node.objectgroup; + _tileCollisions[id] = new Array(); + for (og in _og.nodes.object) + { + var x = Std.parseFloat(og.att.x); + var y = Std.parseFloat(og.att.y); + var w = og.has.width ? Std.parseFloat(og.att.width) : tileWidth; + var h = og.has.height ? Std.parseFloat(og.att.height) : tileHeight; + + if (og.hasNode.polygon) + { + var points:Array = og.node.polygon.att.points.split(" "); + var pArr:Array = new Array(); + for (pS in points) { + pArr.push( x + Std.parseFloat(pS.split(",")[0])); + pArr.push( y + Std.parseFloat(pS.split(",")[1])); + } + _tileCollisions[id].push(new TmxCollisionObject(x, y, w, h, "poly", 0, pArr)); + } + else if (og.hasNode.ellipse) { + var radius = Std.int(((w < h)? w : h)/2); + _tileCollisions[id].push(new TmxCollisionObject(x, y, w, h, "circle", radius)); + }else { // rect + _tileCollisions[id].push(new TmxCollisionObject(x, y, w, h)); + } + } } } } + + if (autoLoadImage && imageSource != null && imageSource != "" ) + { + if (Assets.exists(imageSource, AssetType.IMAGE)) + { + image = Assets.getBitmapData(imageSource); + } + else if (Assets.exists(imageSource.substring(3), AssetType.IMAGE)) + { + image = Assets.getBitmapData(imageSource.substring(3)); + } + else + { + #if debug + trace("Cannot load image source", imageSource); + #end + } + } } public var image(get_image, set_image):BitmapData; @@ -128,6 +189,13 @@ class TmxTileSet public function getRect(id:Int):Rectangle { //TODO: consider spacing & margin - return new Rectangle((id % numCols) * tileWidth, (id / numCols) * tileHeight); + return new Rectangle( Std.int(id % numCols) * tileWidth , Std.int(id / numCols) * tileHeight, tileWidth, tileHeight); + } + + public function getCollisionsByGid(gid:Int):Array + { + if (_tileCollisions != null) + return _tileCollisions[gid]; + return null; } } diff --git a/com/haxepunk/tmx/TmxTilemap.hx b/com/haxepunk/tmx/TmxTilemap.hx new file mode 100644 index 0000000..fb6d70f --- /dev/null +++ b/com/haxepunk/tmx/TmxTilemap.hx @@ -0,0 +1,35 @@ +package com.haxepunk.tmx; + +import com.haxepunk.Graphic.TileType; +import com.haxepunk.graphics.Tilemap; +import flash.display.BitmapData; +import flash.geom.Point; + +/** + * ... + * @author Jero + */ +class TmxTilemap extends Tilemap +{ + public var offsetX:Int; + public var offsetY:Int; + + public function new(tileset:TileType, width:Int, height:Int, tileWidth:Int, tileHeight:Int, ?tileSpacingWidth:Int=0, ?tileSpacingHeight:Int=0, offsetX:Int=0, offsetY:Int=0) + { + this.offsetX = offsetX; + this.offsetY = offsetY; + super(tileset, width, height, tileWidth, tileHeight, tileSpacingWidth, tileSpacingHeight); + } + + override public function render(target:BitmapData, point:Point, camera:Point) + { + var p:Point = new Point(offsetX + point.x, offsetY + point.y); + super.render(target, p, camera); + } + + override public function renderAtlas(layer:Int, point:Point, camera:Point) + { + var p:Point = new Point(offsetX + point.x, offsetY + point.y); + super.renderAtlas(layer, p, camera); + } +} \ No newline at end of file