参考用LayaAir引擎解析Tiled Map地图
1.必须是CSV格式,不支持Tiled Map地图为Base64的图块层格式。
2.导出为json格式
3.修改图集路径
打开刚刚保存的orthogonal.json,搜索关键字"image"我们会发现默认的image路径位于Tiled安装目录中。如图4所示。
路径在Tiled安装目录中肯定是不行的,所以,我们需要先将这个图片(buch-outdoor.png)复制到项目目录,与之前保存的orthogonal.json同级,如图5所示。
然后将orthogonal.json中的image路径修改为相对路径"image":"buch-outdoor.png"
但是我在做这一步时,发现我的json文件中tileset指向的没有image,而是一个source:
"tilesets":[
{
"firstgid":1,
"source":"Tile01.tsx"
},
{
"firstgid":561,
"source":"Tile02.tsx"
}
估计是新建图块时设置有问题,重新建图块,如图,勾选嵌入地图即可
{
"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.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.缩放
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所示。
开启合并图层时,图层属性内可添加layer属性,运行时将会将相邻的layer属性相同的图层进行合并以提高性能。例如,我们将块层2与块层3的分组名称设置为layaAir,那么名为layaAir的图层,开启enableMergeLayer后,会合并到同一个图层。操作如图14-2所示。
11.移除被覆盖的格子
如果下层的格子被遮挡,并且遮挡地块并不是透明的,那么被遮挡的部分直接移除而不被渲染,可以提高性能。移除被覆盖的开启方式为:tMap.removeCoveredTile = true;//移除被非透明地块覆盖的部分
Tips:如果开启后,需要对移除的部分进行操作,是不可能的。所以开启该功能前要确认,不再对移除部分进行操作。
如果在Tiled Map中没有对图块设置type属性,那么即便开启了removeCoveredTile ,也是无效的。所以,开启之前,需要先在TiledMap编辑器中,为图块新增自定义属性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);
}
参考一下这两个方法源码的API说明:
/**
* 通过屏幕坐标来获取选中格子的索引
* @param screenX 屏幕坐标x
* @param screenY 屏幕坐标y
* @param result 把计算好的格子坐标,放到此对象中
* @return
*/
public function getTilePositionByScreenPos
(screenX:Number, screenY:Number, result:Point = null):Boolean {}
/**
* 通过地图坐标得到屏幕坐标
* @param tileX 格子坐标X
* @param tileY 格子坐标Y
* @param screenPos 把计算好的屏幕坐标数据,放到此对象中
*/
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集->图块动画编辑器
双击图块即可添加到左侧,但是没看懂怎么把添加上去的帧删掉……
看一下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 = 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 = _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中。
/**
* 加入一个动画显示对象到此动画中
* @param aniName //显示对象的名字
* @param sprite //显示对象
*/
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 = 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的成员移除,它会再回来。只好用透明图占位。我总觉得这种方法不科学。求更科学的套路。
首先,来看看访问自定义属性的调用API
/**
* 得到tile自定义属性
* @param index 地图块索引
* @param id 具体的TileSetID
* @param name 属性名称
* @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开始的。
也就是说,要获取一个格子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 = 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吗?不要问我能不能,因为我已经在做 - 杀意来袭