Laya|Laya TileMap 系列二 如何在Laya中使用

参考用LayaAir引擎解析Tiled Map地图
1.必须是CSV格式,不支持Tiled Map地图为Base64的图块层格式。
2.导出为json格式

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
image.png
3.修改图集路径
打开刚刚保存的orthogonal.json,搜索关键字"image"我们会发现默认的image路径位于Tiled安装目录中。如图4所示。
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
图4
路径在Tiled安装目录中肯定是不行的,所以,我们需要先将这个图片(buch-outdoor.png)复制到项目目录,与之前保存的orthogonal.json同级,如图5所示。
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
图5
然后将orthogonal.json中的image路径修改为相对路径"image":"buch-outdoor.png"
但是我在做这一步时,发现我的json文件中tileset指向的没有image,而是一个source:

"tilesets":[ { "firstgid":1, "source":"Tile01.tsx" }, { "firstgid":561, "source":"Tile02.tsx" }

估计是新建图块时设置有问题,重新建图块,如图,勾选嵌入地图即可

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
image.png
{ "columns":19, "firstgid":1209, "image":"..\/..\/tiled-windows-64bit-snapshot\/examples\/Tile03.PNG", "imageheight":480, "imagewidth":640, "margin":1, "name":"Tile03", "spacing":1, "tilecount":266, "tileheight":32, "tilewidth":32 }

4.创建TiledMap地图

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
laya.map.TiledMap类中的createMap方法
//初始化舞台 Laya.init(Laya.Browser.width, Laya.Browser.height, Laya.WebGL); //创建TiledMap实例 var tMap = new Laya.TiledMap(); //创建Rectangle实例,视口区域 var viewRect: Laya.Rectangle = new Laya.Rectangle (0, 0, Laya.Browser.width, Laya.Browser.height); //创建TiledMap地图 tMap.createMap("res/TiledMap/orthogonal.json", viewRect, null);

5.缩放
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
通过动图9-3,从原图比例的0.1到2倍的缩放变化效果,来进一步理解scale属性的中心点 Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
在laya.map.TiledMap类中的setViewPortPivotByScale()方法可以设置视口的中心点
private tMap: Laya.TiledMap; constructor() { //初始化舞台 Laya.init(Laya.Browser.width, Laya.Browser.height, Laya.WebGL); //创建TiledMap实例 this.tMap = new Laya.TiledMap(); //创建Rectangle实例,视口区域 var viewRect: Laya.Rectangle = new Laya.Rectangle (0, 0, Laya.Browser.width, Laya.Browser.height); //创建TiledMap地图 this.tMap.createMap("res/TiledMap/orthogonal.json" , viewRect, Laya.Handler.create(this, this.onMapLoaded)); } private onMapLoaded(): void { //设置缩放中心点为视口的左上角 this.tMap.setViewPortPivotByScale(0, 0); //将原地图放大2倍 this.tMap.scale = 2; }

6.拖动地图,需要用到moveViewPort()(移动视口)方法和changeViewPort()(改变视口大小)方法
private tMap: Laya.TiledMap; private MapX: number = 0; private MapY: number = 0; private mLastMouseX: number; private mLastMouseY: number; constructor() { //初始化舞台 Laya.init(Laya.Browser.width, Laya.Browser.height, Laya.WebGL); //创建TiledMap实例 this.tMap = new Laya.TiledMap(); //创建Rectangle实例,视口区域 var viewRect: Laya.Rectangle = new Laya.Rectangle (0, 0, Laya.Browser.width, Laya.Browser.height); //创建TiledMap地图 this.tMap.createMap("res/TiledMap/orthogonal.json" , viewRect, Laya.Handler.create(this, this.onMapLoaded)); } private onMapLoaded(): void { //设置缩放中心点为视口的左上角 this.tMap.setViewPortPivotByScale(0, 0); //将原地图放大2倍 this.tMap.scale = 2; Laya.stage.on(Laya.Event.RESIZE, this, this.resize); Laya.stage.on(Laya.Event.MOUSE_DOWN, this, this.mouseDown); Laya.stage.on(Laya.Event.MOUSE_UP, this, this.mouseUp); this.resize(); } /** * 移动地图视口 */ private mouseMove(): void { var moveX: number = this.MapX - (Laya.stage.mouseX - this.mLastMouseX); var moveY: number = this.MapY - (Laya.stage.mouseY - this.mLastMouseY) //移动地图视口 this.tMap.moveViewPort(moveX, moveY); } private mouseUp(): void { this.MapX = this.MapX - (Laya.stage.mouseX - this.mLastMouseX); this.MapY = this.MapY - (Laya.stage.mouseY - this.mLastMouseY); Laya.stage.off(Laya.Event.MOUSE_MOVE, this, this.mouseMove); } private mouseDown(): void { this.mLastMouseX = Laya.stage.mouseX; this.mLastMouseY = Laya.stage.mouseY; Laya.stage.on(Laya.Event.MOUSE_MOVE, this, this.mouseMove); } /** *改变视口大小 *重设地图视口区域 */ private resize(): void { //改变视口大小 this.tMap.changeViewPort(this.MapX, this.MapY , Laya.Browser.width, Laya.Browser.height); }

7.销毁地图
当Tiled Mapa不再使用的时候,需要使用destroy()方法进行销毁,回收被占用的内存。tMap.destroy();
8.开启和关闭自动缓存
LayaAir引擎使用TiledMap时,默认会将没有动画的地块自动缓存起来,并且缓存类型默认为normal。
//自动缓存没有动画的地块 tMap.autoCache = true; //自动缓存的类型,地图较大时建议使用normal tMap.autoCacheType = "normal"; //消除缩放导致的缝隙,也就是去黑边,1.7.7版本新增的优化属性 tMap.antiCrack = true;

以上的代码属性是引擎的默认值,在多数情况下,保持默认值即可,无需额外设置。那么为什么要再介绍一遍呢?因为有的时候,缓存后的Tiled地图会出现黑边(缝隙)。尽管在1.7.7版本新增了antiCrack属性,可以消除绝大多数因normal缓存导致的黑边。但如果偶现的黑边问题仍未得到解决时。可以通过关闭自动缓存来解决黑边(缝隙)问题。
9.设置缓存区块大小
TiledMap地图都是由一个个单元区块拼接组成。如果缓存时保持原大小,当小图区块很多时会对性能产生影响。因此建议开启缓存区块设置,并将缓存区块的大小设置为512像素左右,必须保持原小图区块的整数倍。
例如,本文示例中的单图区块大小为1616,那么缓存区块可以设置 16的32倍,即为512512。如果单图是1515,缓存可区块可以设置为510510(34倍),以此类推,尽量在原区块整数倍的前提下,设置在512左右。推荐为512*512。
缓存区块的设置需要在createMap(创建地图)的时候设置。设置第四个参数gridSize,示例如下:
//为第二个参数创建Rectangle实例,视口区域 var viewRect:Rectangle = new Rectangle(0, 0, Browser.width, Browser.height); //为第四个参数gridSize创建一个512*512大小的Point对象实例 var gridSize:Point = new Point(512, 512); //创建TiledMap地图 tMap.createMap("res/TiledMap/orthogonal.json" ,viewRect, Handler.create(this,onMapLoaded), null, gridSize)

10.开启合并图层
当TiledMap里有多个图层时,开启合并图层的属性enableMergeLayer,可以将图层合并,会对性能有所提高。开启的方式为:tMap.enableMergeLayer = true; 需要注意的是,如果需要对合并前的图层进行操作,那就不能直接合并。因为合并后会导致无法对合并前的图层进行操作。
如果没有在TiledMap里将图层分组,那么图层合并时,会将所有图层合并到一起。因此,需要分为多个图层并分别操作时。可以在TiledMap里将图层分组。打开TiledMap地图编辑器,选中要分组的图层,在图层的自定义属性栏,添加一个名为layer的string类型属性。操作如图14-1所示。

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
图14-1
开启合并图层时,图层属性内可添加layer属性,运行时将会将相邻的layer属性相同的图层进行合并以提高性能。例如,我们将块层2与块层3的分组名称设置为layaAir,那么名为layaAir的图层,开启enableMergeLayer后,会合并到同一个图层。操作如图14-2所示。
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
图14-2
11.移除被覆盖的格子
如果下层的格子被遮挡,并且遮挡地块并不是透明的,那么被遮挡的部分直接移除而不被渲染,可以提高性能。移除被覆盖的开启方式为:tMap.removeCoveredTile = true; //移除被非透明地块覆盖的部分
Tips:如果开启后,需要对移除的部分进行操作,是不可能的。所以开启该功能前要确认,不再对移除部分进行操作。
如果在Tiled Map中没有对图块设置type属性,那么即便开启了removeCoveredTile ,也是无效的。所以,开启之前,需要先在TiledMap编辑器中,为图块新增自定义属性type,并将设置为1。

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
在图块面板中,点击图块编辑,打开图块地形编辑面板
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
在图块地形编辑面板内,选中地形,在自定义属性栏,点击+号图标,添加int类型的type属性
Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
完成添加后,设置type属性值为1
只要是自定义属性type设置为1的地形,当removeCoveredTile开启后。被遮挡不可见时都可以被移除,以提高性能。
12.设置tiledMap宽高
参考分享:TiledMap设置viewport后黑屏问题!
tiledMap默认是没有宽高的,所以我们在设置viewport的时候,要为tiledMap设置一个宽高,否则tiledMap将可能会被裁剪掉
//切记:设置tiledMap的宽高,需要在地图创建完成之后 private function onLoaded():void { var sp:Sprite=tiledMap.mapSprite() as Sprite; //为tiledMap整个地图的显示容器设置宽高 sp.size(800,800); }

13.屏幕坐标和地图坐标转换
参考官方示例 等角地图
private function onStageClick(e:*=null):void { var p:Point = new Point(0, 0); layer.getTilePositionByScreenPos(Laya.stage.mouseX, Laya.stage.mouseY, p); layer.getScreenPositionByTilePos(Math.floor(p.x), Math.floor(p.y), p); sprite.pos(p.x, p.y); }private function mapLoaded(e:*=null):void { layer = tiledMap.getLayerByIndex(0); var radiusX:Number = 32; var radiusY:Number = Math.tan(180 / Math.PI * 30) * radiusX; var color:String = "#FF7F50"; sprite = new Sprite(); sprite.graphics.drawLine(0, 0, -radiusX, radiusY, color); sprite.graphics.drawLine(0, 0, radiusX, radiusY, color); sprite.graphics.drawLine(-radiusX, radiusY, 0, radiusY * 2, color); sprite.graphics.drawLine(radiusX, radiusY, 0, radiusY * 2, color); Laya.stage.addChild(sprite); }

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
效果就是有个红框,鼠标点哪,红框就在相应的图块里出现
参考一下这两个方法源码的API说明:
/** * 通过屏幕坐标来获取选中格子的索引 * @paramscreenX 屏幕坐标x * @paramscreenY 屏幕坐标y * @paramresult 把计算好的格子坐标,放到此对象中 * @return */ public function getTilePositionByScreenPos (screenX:Number, screenY:Number, result:Point = null):Boolean {}/** * 通过地图坐标得到屏幕坐标 * @paramtileX 格子坐标X * @paramtileY 格子坐标Y * @paramscreenPos 把计算好的屏幕坐标数据,放到此对象中 */ public function getScreenPositionByTilePos (tileX:Number, tileY:Number, screenPos:Point = null):void {}

14.动画
laya官方回复是,不支持Laya的ani动画放置到tiledMap中,参考可以用laya的ani动画系统动态插入到tiledmap的tiled中吗
参考官方示例 带动画的地图
Tiled地图编辑器 Tiled Map Editor 的使用(二)动画效果
在最新版本的tiled map editor中,需要进入图块文件,然后在主菜单->Tile集->图块动画编辑器

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
image.png
双击图块即可添加到左侧,但是没看懂怎么把添加上去的帧删掉……
看一下tmx中对应的标签:

没有看到Frame Duration:500ms在哪里,另外动画的三帧,也不是按1秒为间隔来播放的。第1帧的仙人掌,到第2帧的石头特别快。
最终,导出的json是这样的:
"tiles":[ { "id":29, "properties":[ { "name":"isCanPass", "type":"bool", "value":true }] }, { "animation":[ { "duration":1000, "tileid":30 }, { "duration":2000, "tileid":31 }, { "duration":3000, "tileid":38 }], "id":31, "properties":[ { "name":"isCanPass", "type":"bool", "value":false }] }],

在TiledMap.as的源码中,可以看到onJsonComplete中关于动画的解析:
//动画数据 var tTiles:* = tileset.tiles; if (tTiles) { for (var p:*in tTiles) { var tAnimation:Array = tTiles[p].animation; if (tAnimation) { var tAniData:TileMapAniData = https://www.it610.com/article/new TileMapAniData(); _animationDic[p] = tAniData; tAniData.image = tileset.image; for (var j:int = 0; j < tAnimation.length; j++) { var tAnimationItem:Object = tAnimation[j]; tAniData.mAniIdArray.push(tAnimationItem.tileid); tAniData.mDurationTimeArray.push(tAnimationItem.duration); } } } }class TileMapAniData { public var mAniIdArray:Array = []; public var mDurationTimeArray:Array = []; public var mTileTexSetArr:Array = []; public var image:*; }

会把duration、tileid存到TileMapAniData这个类型的相应数组里去,tAniData.image = tileset.image; 取到的是"image":"tmw_desert_spacing.png"这个属性。
/** * 初始化地图 */ private function initMap():void { var i:int, n:int; for (var p:*in _animationDic) { var tAniData:TileMapAniData = https://www.it610.com/article/_animationDic[p]; var gStart:int; gStart = _texutreStartDic[tAniData.image]; var tTileTexSet:TileTexSet = getTexture(parseInt(p) + gStart); if (tAniData.mAniIdArray.length> 0) { tTileTexSet.textureArray = []; tTileTexSet.durationTimeArray = tAniData.mDurationTimeArray; tTileTexSet.isAnimation = true; tTileTexSet.animationTotalTime = 0; for (i = 0, n = tTileTexSet.durationTimeArray.length; i < n; i++) { tTileTexSet.animationTotalTime += tTileTexSet.durationTimeArray[i]; } for (i = 0, n = tAniData.mAniIdArray.length; i < n; i++) { var tTexture:TileTexSet = getTexture(tAniData.mAniIdArray[i] + gStart); tTileTexSet.textureArray.push(tTexture); } } }

在initMap中,把属性都放到TileTexSet中。
/** * 加入一个动画显示对象到此动画中 * @paramaniName //显示对象的名字 * @paramsprite//显示对象 */ public function addAniSprite(aniName:String, sprite:TileAniSprite):void { if (animationTotalTime == 0) { return; } if (_aniDic == null) { _aniDic = {}; } if (_spriteNum == 0) { //每3帧刷新一下吧,每帧刷新可能太耗了 Laya.timer.frameLoop(3, this, animate); _preFrameTime = Browser.now(); _frameIndex = 0; _time = 0; _interval = 0; } _spriteNum++; _aniDic[aniName] = sprite; if (textureArray && _frameIndex < textureArray.length) { var tTileTextureSet:TileTexSet = textureArray[_frameIndex]; drawTexture(sprite, tTileTextureSet); } } /** * 把动画画到所有注册的SPRITE上 */ private function animate():void { if (textureArray && textureArray.length > 0 && durationTimeArray && durationTimeArray.length > 0) { var tNow:Number = Browser.now(); _interval = tNow - _preFrameTime; _preFrameTime = tNow; if (_interval > animationTotalTime) { _interval = _interval % animationTotalTime; } _time += _interval; var tTime:int = durationTimeArray[_frameIndex]; while (_time > tTime) { _time -= tTime; _frameIndex++; if (_frameIndex >= durationTimeArray.length || _frameIndex >= textureArray.length) { _frameIndex = 0; } var tTileTextureSet:TileTexSet = textureArray[_frameIndex]; var tSprite:TileAniSprite; for (var p:* in _aniDic) { tSprite = _aniDic[p]; drawTexture(tSprite, tTileTextureSet); } tTime = durationTimeArray[_frameIndex]; } } }

可以看出,每3帧会刷新一下animate方法。相当于不停地去检查_interval,然后累加到_time变量上。然后把_time一直和durationTimeArray数组中的时间戳去比较,如果达到了,就进入while循环更新一下帧图片。
在while循环中,会把_time减掉这个时间戳,并且会把时间戳更新到下一个节点。这样就明确了,durationTimeArray这个数组中的值,确实是时间的间隔,而不是时间轴。上面导出的资源,间隔是1000,2000,3000。这就意味着动画间隔越来越长。如果需要每秒切换1帧,都改成1000即可。
但是,当前版本下,laya仍然播放不出动画。经过代码检查,应该是版本不匹配导致的。在TiledMap中的onJsonComplete方法中,_animationDic[p] = tAniData; 是有问题的,因为现在的导出数据是这样的:
"tiles":[ { "animation":[ { "duration":1000, "tileid":30 }, { "duration":1000, "tileid":31 }, { "duration":1000, "tileid":38 }], "id":31 }],

可以看到,现在的p实际上是0,真正的值是存在了与animation同级的id属性上,所以改成这样,_animationDic[tTiles[p].id] = tAniData; ,就能运行了:
//动画数据 var tTiles:* = tileset.tiles; if (tTiles) { for (var p:*in tTiles) { var tAnimation:Array = tTiles[p].animation; if (tAnimation) { var tAniData:TileMapAniData = https://www.it610.com/article/new TileMapAniData(); //_animationDic[p] = tAniData; _animationDic[tTiles[p].id] = tAniData; tAniData.image = tileset.image; for (var j:int = 0; j < tAnimation.length; j++) { var tAnimationItem:Object = tAnimation[j]; tAniData.mAniIdArray.push(tAnimationItem.tileid); tAniData.mDurationTimeArray.push(tAnimationItem.duration); } } } }

15.图块的自定义属性访问,截止到2018.5.16号,当前1.78.beta版本仍然是错的,关于动画的操纵也需要关注
参考TiledMap格子上的自定义数据问题
都2018年了,TS依然没有这个接口。
我用ts直接objdata.properties["R"]来读取。直接F5调试勉强可以用。
发布重新编译ts会报错。希望能尽快提供直接访问的方法。
需要:
1 枚举图层的全部成员。我现在直接用Layer._objDic来强行读取的。
2 getObjectDataByName("").type 也需要访问。
3 getObjectDataByName().properties需要公开访问。
4 tiledMap的动画貌似跟Laya的动画实现方法不一样。不知如何Stop.如何替换。比如,我想实现一个人物奔跑到停止,转身,巡逻。求演示。目前我是用Laya.Animation做的动画,找到目标GridSprite然后addChild实现的。把原有的GridSprite的成员移除,它会再回来。只好用透明图占位。我总觉得这种方法不科学。求更科学的套路。
【Laya|Laya TileMap 系列二 如何在Laya中使用】首先,来看看访问自定义属性的调用API
/** * 得到tile自定义属性 * @paramindex地图块索引 * @paramid具体的TileSetID * @paramname属性名称 * @return */ public function getTileProperties(index:int, id:int, name:String):* { if (_tileProperties[index] && _tileProperties[index][id]) { return _tileProperties[index][id][name]; } return null; }

第一个参数,地图块索引,注释写得并不清楚,根据导出的json和相关代码来观察,指的是在json导出的tilesets数组中,占的索引,当然是从0开始的。

Laya|Laya TileMap 系列二 如何在Laya中使用
文章图片
这个顺序在原始的tmx中也能看出来
也就是说,要获取一个格子tile的自定义属性,首先要知道它是属于哪个地块的。
第二个参数,就是具体的TileSetID,当然如前文所述,这个ID并不是从1开始的,而是从0开始。而layers保存的data却是从1开始的,所以如果是点击地块,去获取相应的自定义属性,注意需要减1。
第三个参数是自定义属性的名称,根据这个key返回value值。
那么我们看一下json中保存自定义属性的部分:
"tilesets":[ { ... "tiles":[ { "id":29, "properties":[ { "name":"isCanPass", "type":"bool", "value":true }, { "name":"tileType", "type":"int", "value":1 }] }, { "animation":[ { "duration":1000, "tileid":30 }, { "duration":1000, "tileid":31 }, { "duration":1000, "tileid":38 }], "id":31 }, { "id":39, "properties":[ { "name":"isCanPass", "type":"bool", "value":false }] }], ...

这里以构建第一个图块的自定义数据为例,根据return _tileProperties[index][id][name]; ,构建数据结构如下:
_tileProperties = {}; _tileProperties[0] = {}; _tileProperties[0] = {}; _tileProperties[0][29] = {}; _tileProperties[0][29]["isCanPass"] = true; _tileProperties[0][29]["tileType"] = 1; _tileProperties[0][39] = {}; _tileProperties[0][39]["isCanPass"] = false;

在onJsonComplete方法中,我们发现因为版本问题,代码并不是这样去构建数据结构的。
var tArray:Array = tJsonData.tilesets; var tileset:*; var tTileSet:TileSet; var i:int = 0; for (i = 0; i < tArray.length; i++) { tileset = tArray[i]; tTileSet = new TileSet(); tTileSet.init(tileset); if (tTileSet.properties && tTileSet.properties.ignore) continue; _tileProperties[i] = tTileSet.tileproperties;

上面的i确实是从0开始循环的,代表着获取方法中getTileProperties第一个参数。但是它指向的内容是错误的,在TileSet的init方法中,只是简单粗暴地一句话处理:
//自定义属性 tileproperties = data.tileproperties;

可是现在的版本里,json里没有tileproperties。我们可以模仿解析动画animation的代码来写:
var tTiles:* = tileset.tiles; if (tTiles) { for (var p:*in tTiles) { var tAnimation:Array = tTiles[p].animation; if (tAnimation) { var tAniData:TileMapAniData = https://www.it610.com/article/new TileMapAniData(); //_animationDic[p] = tAniData; 这一句也改了,见上面动画部分 _animationDic[tTiles[p].id] = tAniData; tAniData.image = tileset.image; for (var j:int = 0; j < tAnimation.length; j++) { var tAnimationItem:Object = tAnimation[j]; tAniData.mAniIdArray.push(tAnimationItem.tileid); tAniData.mDurationTimeArray.push(tAnimationItem.duration); } } //下面这部分是我自己添加的: //检查tiles中,有properties属性的object var properties:Array = tTiles[p].properties; if(properties){ //构造_tileProperties[0][29] = {}; _tileProperties[i][tTiles[p].id] = {}; //把properties中的属性都放进去 for (var k:int = 0; k < properties.length; j++) { _tileProperties[i][tTiles[p].id][properties[k].name] = properties[k].value; } } } }

js版本的代码如下:
//检查tiles中,有properties属性的object var properties = tTiles[p].properties; if(properties){ //构造_tileProperties[0][29] = {}; this._tileProperties[i][tTiles[p].id] = {}; //把properties中的属性都放进去 for (var k = 0; k < properties.length; k++) { this._tileProperties[i][tTiles[p].id][properties[k].name] = properties[k].value; } }

当然在这段 代码前,要先把_tileProperties[i]给初始化:
// this._tileProperties[i]=tTileSet.tileproperties; this._tileProperties[i]={};

使用以下代码测试一下,发现成功了:
console.log("test:",this.tMap.getTileProperties(0,29,"isCanPass")); //test:true console.log("test:",this.tMap.getTileProperties(0,39,"isCanPass")); //test:false

16.如何隐藏tiledMap中指定的GridSprite
17._showGridList
参考tiledmap 可以在图块层的指定格子上添加Sprite吗
LayaAir能做RPG吗?不要问我能不能,因为我已经在做 - 杀意来袭

    推荐阅读