跳转到内容

瓦片地图

Estella 在 C++/WebAssembly 内核里渲染瓦片图层,按图集纹理合批并以分块(chunk)存储。 直接在编辑器里绘制关卡,用 Tilemap 组件从 Tiled 文件 加载整张地图,或用 Tilemaps 资源自己构建与编辑图层——包括多图集图层、逐瓦片动画和 无限流式地图。

有三个入口。编辑器的瓦片画笔面板以可视方式创作图层,背后是 .estileset 图集资源。 Tilemap 组件把实体指向一个 Tiled JSON 文件,加载时为每个图层生成一个 TilemapLayer 实体。Tilemaps 资源是用代码创建与编辑图层的命令式 API。三种方式 最终都落到同一个运行时图层模型。瓦片 id 是从 1 开始的(0 = 空)。正交、等距、交错 (staggered)、六边形地图都是一等公民——可以直接在编辑器里以任意朝向创作,也可以从 Tiled 文件导入,无需额外设置。

Estella 编辑器里,瓦片画笔面板与一张等距地图并排

选中一个 TilemapLayer,瓦片画笔就在左侧打开——上面是工具,下面是图集调色板。直接在视口里作画。

选中带 TilemapLayer 组件的实体,瓦片画笔面板会带着图集调色板打开。选一个工具, 直接在视口里绘制——每一笔都有印章虚影(stamp ghost)预览:

工具 作用
画笔 B 绘制当前印章;拖动连续作画。Alt+点击临时取色。
橡皮 E 清除瓦片。
矩形 U 用印章图案填充拖出的矩形。Shift 锁正方,Alt 只画边框。
椭圆 O 拖出包围盒,填充其内切椭圆。Shift 锁正圆,Alt 只画圆环。
直线 L 沿直线铺设印章。Shift 约束到水平 / 垂直 / 45°。
油漆桶 G 泛洪填充相连的同类瓦片区域。
框选 M 框选一块区域——mod + C / X 复制 / 剪切,Delete 清空,mod + V 作为画笔粘贴;从选区内部拖动可整块移动(单一 undo 步,Esc 取消)。
吸管 I 从图层上已有的瓦片取印章。
地形 T 自动拼接画笔——绘制地形并自动修正邻居。

绘制工具激活期间,H / V 翻转印章,R 旋转 90°。按 QWEsc 离开绘制模式, 键位交还给变换工具。

瓦片画笔面板特写——工具行与瓦片调色板

画笔面板特写:工具行(画笔、橡皮、矩形、椭圆、直线、油漆桶、框选、吸管、地形)、当前图集标签,以及你用来盖章的瓦片调色板。

  • 多瓦片印章 —— 在调色板上拖动,把一整块瓦片抓成一个印章。调色板也支持键盘: 方向键移动选择,Shift + 方向键扩成印章。
  • 图章库 —— 工具行下方的书签按钮把当前画笔存进按项目持久化的图章库;点击章片 即可换回该图案,× 删除。相同图案自动去重。
  • 随机模式 —— 打开骰子按钮(或在绘制时按 D),每个落笔格从印章里随机取一个瓦片, 而不是重复图案——给草地、碎石等做变化最快的方式。采样按图集编辑器属性模式里每个瓦片 的 概率 权重加权(默认 1;0 = 永不散布)。
  • 图层条 —— 场景里有多个瓦片图层时,面板顶部的图层条切换活动图层,并可逐层切换 可见性与锁定;拖动图层章片可以重排图层顺序。

朝向是每个 TilemapLayer 自己的属性,而不是导入痕迹。新建瓦片地图对话框在最开始就 选好它——正交、等距(菱形)、交错(交错等距)或六边形——以及适用时的交错 轴/起始行与六边形边长;之后 Inspector 编辑同一组字段。视口完全跟随图层:网格覆盖层、 画笔虚影、悬停/选区高亮和所有绘制工具在菱形或六边形网格上的表现与方格完全一致。

新建瓦片地图对话框——朝向选择器与图集列表

新建瓦片地图对话框在最开始就选好朝向与图集。

同一张地图画在六边形与交错网格上:

视口里的六边形瓦片地图

六边形——网格、画笔虚影和所有工具都跟随六边形布局。

视口里的交错等距瓦片地图

交错等距——隔行平移半格。

TilemapLayer 字段 取值 说明
orientation Orthogonal / Isometric / Staggered / Hexagonal 图层的网格布局。
staggerAxis Y / X 交错与六边形:Y 平移隔行,X 平移隔列。
staggerIndex Odd / Even 哪些行/列带半格平移。
hexSideLength px 仅六边形:平边长度(0 = 规则尖顶六边形,即 tileHeight / 2)。

任意朝向的 Tiled 地图导入到同一组字段——包括交错地图的 staggeraxis / staggerindex,导入后的落格与 Tiled 里所见完全一致。examples/ 里有三个绘制好的 演示:等距海岛、交错拼布和尖顶六边形战棋地图。

Tilemap 组件通过它唯一的字段加载一个 .tmj 地图(Tiled 的 JSON 格式——在 Tiled 里 用文件 → 导出为 → JSON):

import { defineSystem, Commands, Tilemap } from 'esengine';
const loadLevel = defineSystem([Commands()], (cmds) => {
cmds.spawn().insert(Tilemap, { source: 'assets/maps/level.tmj' });
});
Tilemap 字段 类型 默认 说明
source asset '' Tiled 地图文件(tilemap 资源)。

网格 tileset(单张图集图)、外部 .tsj tileset、以及 image-collection tileset (Tiled 的“图片集合”——每个瓦片一张松散图片)都能加载。collection 瓦片在加载时 折叠进一张网格图集,渲染上与手工图集 tileset 完全一致;每张图片必须与地图瓦片 尺寸一致(否则硬错误并给出修法)。

每个图层(从 Tiled 加载,或你自己生成)携带它的视觉元数据。渲染器实时读取这些,所以动画化 tintColor / opacity 没有延迟。

属性 类型 默认 说明
cellSize Vec2 {32, 32} 瓦片尺寸(世界单位)。
originOffset Vec2 {0, 0} 图层原点偏移。
tileset asset 图集纹理(单图集图层)。
tilesetColumns number 1 图集图册的列数。
tilesetRows number 1 图集图册的行数。
renderLayer number 0 决定绘制顺序的排序层。
tintColor Color {1,1,1,1} 叠乘到每个瓦片上。可动画。
opacity number 1 图层透明度,0..1。可动画。
parallaxFactor Vec2 {1, 1} 相对相机的视差滚动系数。
visible boolean true 显示 / 隐藏图层。

外加上文的朝向字段——orientationstaggerAxisstaggerIndexhexSideLength

生成一个实体,初始化一个图层,然后填充它:

import { defineSystem, Commands, Res, Transform, Tilemaps } from 'esengine';
const build = defineSystem([Commands(), Res(Tilemaps)], (cmds, tilemap) => {
const layer = cmds.spawn().insert(Transform, { position: { x: 0, y: 0, z: 0 } }).id();
tilemap.initLayer(layer, 32, 32, 16, 16); // 32×32 tiles, 16px cells
tilemap.setTiles(layer, new Uint16Array(32 * 32)); // bulk load
tilemap.setTile(layer, 5, 3, 7); // one tile (x, y, id)
tilemap.fillRect(layer, 0, 0, 32, 1, 2); // a row of tile 2
});
tilemap.getTile(layer, 5, 3); // read a tile id
tilemap.flipTile(layer, 5, 3, true, false, false); // flipH, flipV, flipDiagonal
tilemap.rotateTile(layer, 5, 3, 90); // degrees
tilemap.tileToWorld(layer, 5, 3, 0, 0); // → { x, y }
tilemap.worldToTile(layer, wx, wy, 0, 0); // → { x, y }

一个图层可以从多个图集绘制。给它一张 slot 表——每个 slot 把一个 firstId 区间映射到一张 纹理及其列数(每张纹理一次 draw call):

tilemap.setTilesets(layer, [
{ firstId: 1, textureHandle: grassTex, columns: 8 },
{ firstId: 256, textureHandle: propsTex, columns: 8, margin: 4, spacing: 8 },
]);

可选的 margin(第一个 tile 前的边距)和 spacing(tile 之间的间隙)以像素为单位,用于描述 单元之间留白的图集——这样每个 tile 只采样自己的纹素,不会渗到相邻 tile。两者默认为 0,所以 无间隙图集无需设置;Tiled / .estileset 导入器会自动从图集自身的 margin/spacing 填入。

动画瓦片在图集编辑器里创作:打开 .estileset,把模式切到动画,点击要动画的 瓦片,再依次点击其他图集瓦片追加帧。工具栏下方的条带里有实时循环预览、逐帧时长输入 (毫秒)和删除/清空操作;带动画的瓦片在图集里有 ▶ 标记。帧数据保存进 .estileset, 所有使用该图集的图层都会自动播放——编辑器视口和运行时皆然,无需一行代码。

图集编辑器的动画模式——某个瓦片的帧条带与逐帧时长

动画模式:选一个瓦片(这里是 #34),点击图集瓦片追加帧,并以毫秒设置每帧时长。条带作为实时预览循环播放。

程序化图层用 setTileAnimation 从代码做同样的事——插件每帧推进它:

tilemap.setTileAnimation(layer, 10, [
{ tileId: 10, duration: 100 }, // durations in ms
{ tileId: 11, duration: 100 },
]);

对于大型或流式世界,用无限图层并逐块写入;exportChunks / importChunks 序列化稀疏的 分块数据:

tilemap.initInfiniteLayer(layer, 16, 16);
tilemap.setChunkTiles(layer, chunkX, chunkY, tiles, 32, 32);

图集编辑器的地形模式——带 Grass / Sand / Water 颜色的角 Wang 集

地形模式里的角 Wang 集:定义地形颜色(Grass / Sand / Water),再给每个瓦片的四角上色——之后 T 画笔据此从邻居解析出正确的瓦片。

地形让 T 画笔绘制的是含义而不是具体瓦片——你说“这里是草地”,解析器自动挑出 边缘与邻居吻合的那块瓦片。地形在图集编辑器的地形模式里创作;一个地形集使用 两种模型之一:

  • 邻接(edge / corner-blob) —— 经典单地形模型:每个瓦片声明它的哪些边 (16 瓦片集)或边+角(47 瓦片 blob 集)属于该地形,画笔绘制地形时自动修正 邻居过渡。
  • 角 Wang(多地形) —— Tiled/Godot 的现代模型:地形集携带一个颜色调色板 (草、沙、水……),每个瓦片给自己的四个各指定一种颜色。一个集就能混合多种 地形,画笔在半格角网格上绘制颜色——点在四块瓦片之间,解析器按角色精确匹配 重排这四块(最近失配兜底),得到平滑的 marching-squares 边界。图集编辑器负责 颜色调色板(添加 / 重命名 / 改色 / 删除)与逐瓦片的角:瓦片的每个四分之一区域就是 对应角的点击目标——单击或拖动笔画绘制当前颜色,右键擦除;画笔面板列出可绘制的颜色。

画笔的地形工具按地形集的模型自动分派。examples/ 里有一个角 Wang 混合海岛演示。

引擎的一等图集资源是 .estileset——它引用一张图集纹理,并携带逐瓦片的碰撞、动画、 地形/自动拼接(auto-tile)数据与自定义键值属性。

图集编辑器的碰撞模式——形状标签、修饰、斜坡预设,以及图集上的碰撞覆盖

图集编辑器的碰撞模式:选形状(盒 / 多边形 / 圆)、切换单向 / 传感器并设置摩擦 / 弹性 / 密度、放置斜坡预设——绿色覆盖标出每个瓦片的碰撞体。

逐瓦片碰撞远不止实心方块。在图集编辑器的 碰撞 模式里,你可以为每个瓦片编辑:

  • 形状——整格 方块(可拖刷)、内切 圆形(可拖刷),或经顶点编辑器的自由 多边形;一键 坡度 / 半砖预设 盖章常用斜坡(拖动可连续铺一排)。
  • 修饰符,叠加在任何画出的形状上:单向(可从下方跳上的平台,固定实心朝上法线)、 传感器(只发重叠事件、无碰撞响应),以及物理材质覆盖(摩擦 / 弹性 / 密度)。

普通实心方块在生成时会贪心合并成大块静态碰撞体;带形状、单向、传感器或材质的瓦片各自 生成一个碰撞体,并按瓦片翻转标志重定向几何、与渲染严格一致。场景视口会把所选图层的完整 碰撞画成覆盖层(显示标志 → 瓦片碰撞)——与 Play 生成的完全一致。

Tiled 地图享受同等待遇:在 Tiled 瓦片碰撞编辑器 里画的形状(矩形、椭圆、多边形) 导入到同一模型——整格矩形保持可合并的实心方块,其余每瓦片各生成一个碰撞体;瓦片属性 oneway / sensor / friction / restitution / density 应用相同修饰符。旧的 collision=true 瓦片属性仍标记普通实心方块。

不是每个关卡都用图集。当你的美术是一张背景大图——手绘的房间、导出的插画——你仍然 希望角色能撞到墙、地板和斜坡。碰撞层画的正是这些:一个什么都不渲染TilemapLayer,只在背后的图上盖碰撞格子。

碰撞层就是一个普通瓦片层,只不过它的图集引用是哨兵值 builtin:collision 而非 .estileset。这一处改动把它路由到一套固定的碰撞笔刷,于是它复用你已经熟悉的整条绘制 管线——笔刷 / 矩形 / 填充工具、分块存储、翻转、贪心合并盒——没有第二套系统要学。播放时 所画的格子生成静态碰撞体,与逐瓦片碰撞完全一样。

Create → 2D → Collision Layer(或 New Collision Layer 命令)创建一个,预置 32px 网格,并打开画笔面板——此时调色板是碰撞笔刷而非图集。

碰撞笔刷调色板——实心、斜坡、半砖、单向、触发器,以及逐层的摩擦力与弹性字段

碰撞调色板取代了图集:挑 实心、一个斜坡半砖单向 ↑触发器,然后在背景上刷。摩擦力弹性 字段为整个图层设一份物理材质。

笔刷:

笔刷 格子 用途
Solid(实心) 整格盒 墙、地板、天花板。会被贪心合并成大的静态碰撞体。
Ramp ◢ / ◣(斜坡) 三角 向右 / 向左升的 45° 斜坡。
Half(半砖:下 / 上 / 左 / 右) 半格盒 台沿、矮墙、薄地板。
One-way ↑(单向) 顶部实心盒 可跳穿平台——上方实心、下方可穿过。翻转即可改朝向(天花板、墙)。
Trigger(触发器) 传感器盒 非实心区域,触发重叠事件——见 标记与触发区

像画瓦片一样画它们;碰撞叠加层画出的正是播放时会生成的东西。每个碰撞层还能携带自己的 物理材质(摩擦力 / 弹性 / 密度),作用于它生成的每个格子——在 Inspector 里设置。

游戏代码通过与任意绘制层相同的碰撞查询读取碰撞层——isTileSolidtileCollisionAttileCollisionAtWorld——所以下游完全不必知道这些格子来自内置调色板。

Tiled 的对象层(出生点、触发器、区域形状)随地图一起加载。现代的读法是 Query(Marker):加载时点对象汇聚成 Marker 实体、形状对象汇聚成 Trigger Area 区域——正是你手工创建的那些实体——于是一条查询就能读到对象,无论它来自 Tiled 还是 Estella 编辑器。

原始的组数据仍可用地图的源路径经 getTilemapSource 查询——每个组携带它的对象,含位置、 尺寸、type(Tiled 的 class)、shape 和自定义 properties:

import { getTilemapSource } from 'esengine';
const src = getTilemapSource('assets/maps/level.tmj');
for (const group of src?.objectGroups ?? []) {
for (const obj of group.objects) {
if (obj.type === 'spawn') spawnPlayer(obj.x, obj.y);
}
}

携带瓦片引用(gid——在 Tiled 里盖到对象层上的瓦片)的对象会渲染为地图的精灵 子实体,并遵守 Tiled 的水平、垂直和对角翻转标志。

名字为 collision(大小写不敏感)或携带 collision=true 属性的对象组还会在播放模式 生成静态碰撞体——矩形、椭圆、多边形、折线各自变成对应的 Box2D 形状,与瓦片碰撞走相同的 origin / 每单位像素 / 播放模式生命周期。

方法 说明
initLayer(layer, cols, rows, tileW, tileH) 创建一个固定尺寸的图层。
initInfiniteLayer(layer, tileW, tileH) 创建一个分块、无界的图层。
setTiles(layer, Uint16Array) 批量加载一个固定图层的瓦片。
setTile(layer, x, y, id) / getTile(layer, x, y) 写 / 读一个瓦片。
fillRect(layer, x, y, w, h, id) 用瓦片 id 填充一个矩形。
flipTile(layer, x, y, h, v, d) 翻转一个瓦片(水平 / 垂直 / 对角)。
rotateTile(layer, x, y, degrees) 旋转一个瓦片(90° 步进)。
tileToWorld(layer, tx, ty, ox, oy) / worldToTile(layer, wx, wy, ox, oy) 坐标转换。
setTilesets(layer, slots) 指定多图集 slot 表。
setTileAnimation(layer, tileId, frames) 动画化一个瓦片 id。
setChunkTiles(layer, cx, cy, tiles, w, h) 写一个块(无限图层)。
exportChunks(layer) / importChunks(layer, data) 序列化 / 恢复分块数据。
setTint(layer, color) / setVisible(layer, on) / setRenderProps(...) 实时视觉覆盖。
setTileProperty(...) / getTileProperty(...) 逐瓦片自定义键值属性。
setGridType(layer, type) / setHexParams(...) 朝向(正交 / 等距 / 六边形)。
destroyLayer(layer) 销毁图层。

瓦片碰撞查询直接查已解析的碰撞数据(无需物理射线):tileCollisionAt(layer, x, y) 返回该瓦片解析后的形状+修饰符(或 null);isTileSolid(layer, x, y) 对任何非传感器碰撞 为 true(单向平台也算——用 .oneWay 特殊处理);tileCollisionAtWorld(...) 以世界像素寻址。

  • 手工关卡在编辑器里绘制(或用 Tilemap + .tmj 从 Tiled 导入);程序化或运行时 生成的地图用 Tilemaps API。
  • 按图集合批:每图层的图集纹理越少,draw call 越少;把瓦片打成图集。
  • 批量写入setTiles / setChunkTiles,而不是循环里逐个 setTile
  • 流式世界用无限图层;只写玩家附近的块。
  • 碰撞与瓦片动画.estileset 里编辑,保持数据驱动并在重新导入后仍然保留。
  • 标记与触发区 —— 把玩法点位和触发区作为可查询的真实体铺在关卡上。
  • 物理 —— 瓦片碰撞体参与物理世界。
  • 资源 —— 加载 .tmj 地图与 .estileset 图集。
  • 场景 —— 瓦片图层随场景序列化。