大地图探索,由Tiled出发

大地图实现,由各部分对Tiled的支持开始。


前言

项目中需要用到“大”地图玩法,来实现一些GVE、GVG玩法,根据游戏类型及玩法体量,选择最合适的TileMap来实现。
但眼光要不仅于此,由TileMap开始,向其他游戏类型的技术方案进行拓展了解。

通过本篇文章,可以了解:

  • Tiled地图编辑器的介绍
  • 其他大地图方案技术




Tiled

Tiled是一个编辑2D游戏地图的编辑器,可以帮助开发游戏的内容。

优点

  • 可快速制作关卡地图,策划可视化编辑,降低配置成本
  • 支持多种压缩格式,支持多层级设置,完全可作为关卡编辑器使用

标准

可导出的格式

  • [常用] TMX
  • CSV
  • GMX
  • JSON
  • JS
  • LUA

压缩模式

  • XML
  • CSV
  • BASE64
    • GZIP
    • ZLIB
    • Z标准

支持类型

  • 直90°
  • 斜45°
  • 斜45°交错
  • 六边形




概念

坐标系(OpenGL坐标系原点在左下角,需要转换)

  • 原点:左上角
  • 单位:瓦片数量(非像素)
  • x轴方向:自左向右
  • y轴方向:自上向下




瓦片锚点

  • 瓦片地图锚点默认为(0, 0),每个瓦片锚点默认为(0, 0),且可修改

遮挡关系

  • 地图层之间,存在 ZOrder,ZOrder大的在上层
  • 瓦片之间,渲染顺序为自左向右,自上向下

各元素

  • 属性
    • 所有的属性都是key-value键值对形式存储;除了指定的属性外还有自定义属性可灵活配置
    • 类型
      • 地图属性
      • 图层属性
      • 对象属性
      • 自定义属性
        • bool
        • color
        • float
        • int
        • string
        • file
  • 图层
    • 图块层
      • 规则图块拼接铺设
      • 为了便于编辑,支持一些刷子工具
        • 填充工具
          • 图章刷
            • 选择1块或多块图块绘制,
            • 按住shift可以绘制一条线段
            • 按住ctrl+shift可以绘制一个椭圆
          • 地形刷
            • 提供角落地形过度的编辑方式
            • 支持图章刷的shift和ctrl+shift规则
          • 王氏刷
            • 类似于地形刷,但可以编辑边和角
          • 填充
            • 将相邻的同图块全部替换成指定图块
          • 填充形状工具
            • 选择一块区域填充图块
            • 按住shift可以填充正方形和圆形
        • 选择工具(选择指定区域修改,不可修改其他区域)
          • 矩形选择
            • 自定义矩形区域
          • 魔术棒
            • 相连的同地块
          • 同地块选择
            • 地图内所有同地块
    • 对象层
      • 自定义对象的放置,支持形状/图片
    • 图像层
      • 前景后景图的设置
      • 管理
  • 对象
    • 在对象层可以放置对象,支持矩形、圆形、三角形、图片等等方式
    • 可以对对象进行旋转、放缩、翻转、修改大小等等
    • 可以在对象添加自定义属性,方便进行与游戏内逻辑关联
  • 图块集
    • 图块集是绘制图块层与对象的图片来源
      • 对于图块层的图块集,指定宽高,来截取使用,还支持设置边界值
      • 对于对象的图块集,每个图块都是自己一个完整的图片,方便整个绘制
    • 一个图块层只能绑定一张图块集






支持

Cocos2d

Cocos2d引擎对TileMap的支持

主要文件:

  • CCTMXTiledMap.h & CCTMXTiledMap.cpp
    • 主类,解析并绘制TileMap
  • CCTMXLayer.h & CCTMXLayer.cpp
    • TileMap图块层结构
  • CCTMXObjectGroup.h & CCTMXObjectGroup.cpp
    • TileMap对象层结构
  • CCTMXXMLParser.h & CCTMXXMLParser.cpp
    • 解析TileMap的工具类
  • CCFastTMXTiledMap.h & CCFastTMXTiledMap.cpp
    • v3.2新增方法,对应CCTMXTiledMap
  • CCFastTMXLayer.h & CCFastTMXLayer.cpp
    • v3.2新增方法,对应CCTMXLayer

引擎通过CCTMXXMLParser的相关方法解析地图,创建 TMXTiledMap 对象,该对象子节点都是 TMXLayer对象,对应TileMap的图块层,包含 TMXObjectGroup 对象数组,对应TileMap的对象层。
所创建出的地图,仅仅包含图块层的初始化与应用,并不会将对象创建出来,只是解析了数据进行存储。(使用图片对象,并不会在地图中创建出对象,依旧需要自己来获取对象层数据,进行创建)

创建流程

  1. 【外部调用】通过 TMXTiledMap:create / TMXTiledMap:createWithXML 方法,使用TMX格式或XML格式文件创建地图对象
  2. 【解析】通过SAXParser解析地图文件,生成TMXMapInfo对象
    • 这一步是将地图数据解析成自己的数据结构
    • 类型
      • TMXTilesetInfo,图块集数据结构
      • TMXLayerInfo,图块层数据结构
      • TMXMapInfo,地图数据结构
  3. 【构建】通过解析的数据结构,构建地图对象
    • 这一步是根据数据创建具体对象,包含加载图集,创建每个图片等
    • 类型
      • 简单的地图属性,直接赋值
      • 对象层数据,直接赋值
      • 图块层数据,需要配合图块集数据与地图数据,共同创建出TMXLayer对象(一个SpriteBatchNode对象)

重要枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 地图方向枚举
enum
{
TMXOrientationOrtho, // 正常
TMXOrientationHex, // 六角交错
TMXOrientationIso, // 45度
TMXOrientationStaggered, // 等角交错
};

// 栅格轴方向(对于六边形,X-边朝上,Y-点朝上)
enum
{
TMXStaggerAxis_X, // X
TMXStaggerAxis_Y, // Y
};

// 栅格类型
enum
{
TMXStaggerIndex_Odd, // 奇数
TMXStaggerIndex_Even, // 偶数
};

TMXTiledMap系列

TMXTiledMap负责解析并绘制TMX地图。

  • 每个图块层都被解析并创建为TMXLayer对象(一个SpriteBatchNode子类,若层被设置为不可见则不会被解析创建),可以在运行期间通过层名或层级(从0开始)获取TMXLayer对象。
  • 每个对象层都被解析并创建为TMXObjectGroup对象,可以在运行期间通过层名来获取TMXObjectGroup对象。
  • 每个属性都被存储为key-value键值对,每个对象都提供获取属性的方法。

限制

  • 每个图块层只支持一个图块集
  • 不支持嵌入式图片
  • 仅支持XML格式(不支持JSON格式)

TMXLayer代表TileMap的图块层对象

  • 是SpriteBatchNode子类,默认情况下使用TextureAtlas渲染图块
  • 如果在运行时修改图块,该图块会成为一个Sprite,否则不会创建Sprite对象
    • 创建的Sprite可以支持旋转、放缩、移动等API
  • 通过设置图块层不同属性来启用不同特性
    • cc_vertexz,OpenGL深度测试
    • cc_alpha_func,OpenGL透明度测试


FastTMXTiledMap系列

大部分与TMXTiledMap差不多,主要区别在于FastTMXTiledMap系列的图块层对象并非继承自SpriteBatchNode,而是继承自Node;
老版TMXTiledMap借助于SpriteBatchNode对同图集图片合批做绘制优化处理,新版FastTMXTiledMap重写绘制方法,使用PrimitiveCommand渲染指令,在绘制时根据相机可见性,规划矩形区域,只对矩形区域内的顶点进行批量上传,从而节省了绘制的顶点数。
性能上,Draw Call是一样的,但是FastTMXTiledMap GL Verts(顶点数)更少,绘制效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void TMXLayer::draw(Renderer *renderer, const Mat4& transform, uint32_t flags)
{
updateTotalQuads();

bool isViewProjectionUpdated = true;
auto visitingCamera = Camera::getVisitingCamera();
auto defaultCamera = Camera::getDefaultCamera();
if (visitingCamera == defaultCamera) {
isViewProjectionUpdated = visitingCamera->isViewProjectionUpdated();
}

if( flags != 0 || _dirty || _quadsDirty || isViewProjectionUpdated)
{
Size s = Director::getInstance()->getVisibleSize();
auto rect = Rect(Camera::getVisitingCamera()->getPositionX(),
Camera::getVisitingCamera()->getPositionY(),
s.width,
s.height);

Mat4 inv = transform;
inv.inverse();
rect = RectApplyTransform(rect, inv);

updateTiles(rect);
updateIndexBuffer();
updatePrimitives();
_dirty = false;
}

if(_renderCommands.size() < static_cast<size_t>(_primitives.size()))
{
_renderCommands.resize(_primitives.size());
}

int index = 0;
for(const auto& iter : _primitives)
{
if(iter.second->getCount() > 0)
{
auto& cmd = _renderCommands[index++];
cmd.init(iter.first, _texture->getName(), getGLProgramState(), BlendFunc::NORMAL, iter.second, _modelViewTransform, flags);
renderer->addCommand(&cmd, this);
}
}
}

但是,在项目使用中,如果将地图挂载到3D相机上,会有裁剪问题,暂时未解决。



优化

通过Tiled编辑器,可以导出TMX格式文件或XML文件,引擎直接对它解析。

但依旧有一些不足

  • 并非会解析所有字段
    • 比如,对象旋转后的属性是无法解析出来的
  • 对对象层数据只解析不处理

在实际使用时,为了性能优化与业务需求,在导出TMX文件,又通过python脚本,进行二次加工。

通过python脚本,主要处理:

  • 修改GID,所用到的图片都放在一个图集中,然后修改映射关系
  • 计算对象在引擎显示的屏幕坐标(空间换时间,这样就不需要在使用时计算了)
  • 属性转移
    • 有些属性 旋转、翻转 等,不会被引擎解析
    • 将这些属性转为自定义属性,便于解析

还有一些想法,还未实施

  • 分离图块层与对象层
    • 原因:
      • Tiled编辑器,可以导出lua格式文件
      • cocos2d引擎解析对象层数据,但不绘制,浪费解析性能
    • 方案:
      • Tiled编辑器导出tmx文件,只包含图块层,给引擎解析与绘制
      • Tiled编辑器导出lua文件,只包含对象层,给业务代码解析与绘制



Unity

之前Unity不支持使用Tiled,通过插件来支持

后来,Unity官方支持了 瓦片地图 相关功能

  1. Unity 2017.2 版中添加了瓦片地图
  2. Unity 2018.2 中添加了六边形瓦片地图 (Hexagonal Tilemap) 功能
  3. Unity 2018.3 中添加了等距瓦片地图 (Isometric Tilemap) 功能
  4. Unity 2020.1 2D Tilemap Editor 不再随 Editor 安装过程一起安装,而是必须从 Package Manager 下载

推荐教程:
Unity Tilemap模块全攻略



服务器

默认文件是tmx格式,tmx实际上就是xml格式文件,可以当成xml进行解析。

标签类型

  • map,存储tilemap的主要信息,朝向、宽高、格子数及大小等
  • tileset,存储tilemap的图片资源信息,图片资源ID(GID)的映射关系
  • layer,图块层
    • data,图块层内图块位置及GID对应信息,可以被压缩
  • objectgroup,对象层
    • ojbect,每个对象的属性信息键值对
  • imagelayer,图像层

TMX样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.3.3" orientation="hexagonal" renderorder="right-down" width="40" height="20" tilewidth="92" tileheight="75" infinite="0" hexsidelength="46" staggeraxis="x" staggerindex="odd" nextlayerid="6" nextobjectid="12">
<tileset firstgid="1" name="tile1" tilewidth="92" tileheight="75" tilecount="1" columns="1">
<image source="guandu/tile1.png" width="92" height="75"/>
</tileset>
<tileset firstgid="2" name="daying" tilewidth="173" tileheight="114" tilecount="1" columns="1">
<image source="guandu/daying.png" width="173" height="114"/>
</tileset>
<tileset firstgid="3" name="jianta2" tilewidth="81" tileheight="156" tilecount="1" columns="1">
<image source="guandu/jianta2.png" width="81" height="156"/>
</tileset>
<imagelayer id="3" name="图像图层 1" offsetx="-500" offsety="-250">
<image source="guandu/stratagem_img_bg.jpg" width="2048" height="2048"/>
</imagelayer>
<imagelayer id="5" name="图像图层 2" offsetx="1548" offsety="-250">
<image source="guandu/stratagem_img_bg.jpg" width="2048" height="2048"/>
</imagelayer>
<layer id="1" name="base_layer" width="40" height="20">
<data encoding="base64" compression="zlib">
eJztw0ENAAAMA6Grf9NzsRckrJqqqurDA5vdAyE=
</data>
</layer>
<objectgroup id="2" name="object_bottom_1">
<object id="4" gid="3" x="733.333" y="508" width="81" height="156">
<properties>
<property name="png" value="jianta2"/>
</properties>
</object>
<object id="7" gid="2" x="1004" y="609" width="173" height="114"/>
<object id="11" gid="3" x="1318" y="534" width="81" height="156"/>
</objectgroup>
</map>

Tiled编辑器也支持导出为json格式,方便服务器解析。
标签同xml的差不多,也就格式上会有些变化。

JSON样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
{ "compressionlevel":-1,
"height":20,
"hexsidelength":46,
"infinite":false,
"layers":[
{
"id":3,
"image":"guandu\/stratagem_img_bg.jpg",
"name":"\u56fe\u50cf\u56fe\u5c42 1",
"offsetx":-500,
"offsety":-250,
"opacity":1,
"type":"imagelayer",
"visible":true,
"x":0,
"y":0
},
{
"id":5,
"image":"guandu\/stratagem_img_bg.jpg",
"name":"\u56fe\u50cf\u56fe\u5c42 2",
"offsetx":1548,
"offsety":-250,
"opacity":1,
"type":"imagelayer",
"visible":true,
"x":0,
"y":0
},
{
"compression":"zlib",
"data":"eJztw0ENAAAMA6Grf9NzsRckrJqqqurDA5vdAyE=",
"encoding":"base64",
"height":20,
"id":1,
"name":"base_layer",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":40,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"object_bottom_1",
"objects":[
{
"gid":3,
"height":156,
"id":4,
"name":"",
"properties":[
{
"name":"png",
"type":"string",
"value":"jianta2"
}],
"rotation":0,
"type":"",
"visible":true,
"width":81,
"x":733.333,
"y":508
},
{
"gid":2,
"height":114,
"id":7,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":173,
"x":1004,
"y":609
},
{
"gid":3,
"height":156,
"id":11,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":81,
"x":1318,
"y":534
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":12,
"orientation":"hexagonal",
"renderorder":"right-down",
"staggeraxis":"x",
"staggerindex":"odd",
"tiledversion":"1.3.3",
"tileheight":75,
"tilesets":[
{
"columns":1,
"firstgid":1,
"image":"guandu\/tile1.png",
"imageheight":75,
"imagewidth":92,
"margin":0,
"name":"tile1",
"spacing":0,
"tilecount":1,
"tileheight":75,
"tilewidth":92
},
{
"columns":1,
"firstgid":2,
"image":"guandu\/daying.png",
"imageheight":114,
"imagewidth":173,
"margin":0,
"name":"daying",
"spacing":0,
"tilecount":1,
"tileheight":114,
"tilewidth":173
},
{
"columns":1,
"firstgid":3,
"image":"guandu\/jianta2.png",
"imageheight":156,
"imagewidth":81,
"margin":0,
"name":"jianta2",
"spacing":0,
"tilecount":1,
"tileheight":156,
"tilewidth":81
}],
"tilewidth":92,
"type":"map",
"version":1.2,
"width":40
}




拓展

主流大地形渲染

TileMap

利用一些小图片拼出一个大地图

优点

  • 结构简单,方便优化
  • 加载整个TileMap,图片都在一个图集中,可以批量绘制,地图加载压力轻

缺点

  • 只能满足特定视距下LOD的细节程度
  • 重复感很强,不容易做大结构和氛围

游戏

  • 率土之滨
  • 文明6移动版


多层纹理混合

利用一张Mask图的多通道,控制多张贴图进行纹理混合

基于高度的纹理混合shader

优点

  • 制作简单,性能压力小

缺点

  • 可混合的通道及贴图数有限,难以做出丰富的效果

游戏

  • 万国觉醒


Virtual Texture

通过 Indirection Map,将贴图载入Physical Texture。

浅谈Virtual Texture

优点

  • 效果细节丰富
  • 美术自由度高
  • 内存和DrawCall可控

缺点

  • 手机端效果不明确

游戏

  • 一些端游
  • 一些主机游戏


Texture Array

存储一个数组,每个元素是一张图

优点

  • 减少bind次数
  • 适合层与层之间的颜色混合

缺点

  • 严格要求尺寸、格式
  • OpenGL ES 3.0以上才支持



程序化工具选型

Substance

程序化贴图常用工具
可在一定程度上满足贴图需求,但无法生成模型、数据表等资源,需要第三方工具


Houdini

行业程序化流程常用工具,移动游戏上 吃鸡类游戏、《原神》等都有所使用。

特点

  • 基于节点
  • 非破坏式流程,快速迭代
  • 操作属性,可将任意信息存储在几何中
  • 定制化程度高,可以单靠编程完成整个流程
  • 处理数据流,外部仅关心数据输入与输出



大地图技术

除了上面的渲染技术与工具,还有一些大地图数据处理上的一些技术。
比如:

  • 精度问题处理
    • 区域划分
    • 节点中转
    • 坐标转换
  • LOD(level of detail)
  • Streaming
  • Caching
  • AOI(Area Of Interest)

更详细内容,可跳转知乎:开放世界游戏中的大地图背后有哪些实现技术?





参考资料: