Skip to content

地图渲染

引言——LibGDX 如何渲染图片?

LibGDX 中的图片通过 SpriteBatch 显示在屏幕上:它用 GL 绑定纹理将其载入内存,然后可以非常快速地显示。 实际渲染非常快,但绑定过程很慢。 因此,理想情况下我们希望绑定次数尽可能少,即每个纹理应包含尽可能多的图片。 这就是我们把图片编译(ImagePacker.packImages())成大型 PNG 的原因。

然而,由于不同芯片组等的限制,这些图片最大只能到 2048×2048 像素,而游戏包含的图片比单个这么大的方块能容纳的还多。 我们的做法是按类别(以及渲染时的接近程度)分组。 'android' 文件夹包含 Images,还有各种子类别——Images.Flags、Images.Tech 等。 每个子类别都被编译成 'android/assets' 文件夹中的独立 PNG 文件。

渲染时,主要的时间消耗在于重新绑定纹理。因此我们需要小心地把重新绑定次数(即"在不同类别之间切换")降到最低。

分层

每个地图地块由多个图层组成,为了视觉清晰,每个图层必须等所有地块都渲染完后再渲染下一层。 例如,我们不希望一个地块的单位精灵被另一个地块的改良设施盖住。

这个分层在 TileGroupMap 中完成:我们把所有地块的各个部分取出来,按图层分离,然后全部加入一个大的 group。

这也有巨大的性能优势:例如各城市按钮中的文字和建造图片直到最后才渲染,因此按城市数量切换纹理,而不是每个地块都切换。 这也意味着,添加了自定义地形集或单位精灵的模组比"渲染整个地块"的方式性能更好——我们先渲染所有地形,再渲染所有改良设施,依此类推。 所以如果我的地形集提供了全部地形,那么在渲染完成前它都不会被换出。

理解 TileGroup

从技术角度看,TileGroup 创建图层——但实际渲染时,我们会把它们搬到大地图上。为什么?

首先,我们有两种使用场景——渲染单个地块,和渲染整张地图。文明百科和地图编辑器需要渲染没有底层地图的单个地块。 最简单的做法是把它们组合起来,所以 TileGroup 就是带全部图层的单个地块。

但如上所述,整张地图不能这么做——我们需要在进入 B 层之前渲染完所有地块的 A 层。 所以我们也以同样的方式创建图层,但把图片"偷"到大地图上。

TileLayer 子类是普通 Kotlin 类(不是场景图 actor)。它们持有自己地块的 Image 对象,并知道地块的位置。 当 TileGroup 独立使用时,其图片直接加入 TileGroup。 当放入 TileGroupMap 时,图片被移入共享的 TileMapLayer 容器——每种图层类型一个容器,每个容器持有该图层所有地块的扁平图片列表。

TileGroup(独立)               TileGroupMap(完整地图)
  └── 地形图片                     ├── TileMapLayer<TileLayerTerrain>
  └── 边界图片                     │     └── 所有地块的全部地形图片
  └── …                           ├── TileMapLayer<TileLayerBorders>
                                   ├── …
                                   └── tileGroupLayer(TileGroup,仅用于点击检测)

调试

Android Studio 内置的 CPU profiler 非常适合这个任务。 在 Android 设备上启动游戏,打开一局游戏,开始录制 CPU,把屏幕移动一会儿,然后停止录制。 在线程列表中选择 "GL Thread",把可视化方式改为火焰图。你就能看到渲染时间实际花在哪了。

你可以在这里找到各种可测试的存档——这个就是一个很拥挤的例子。