Files
EVEngine/docs/usr/modules/building.md
T
西风逍遥游 7820703cfa feat(building): 统一 Grid 抽象 + 3D 放置 + 多样式 Tilemap 打通
对照 Unity Grid/Tilemap 与 Grid Placement System 架构补齐建筑放置能力:

- 新增 grid 模块:GridConfig(rectangle/hexagon/isometric/staggered/iso-z-as-y、cellGap、stagger、XY/XZ 平面轴)+ 投影/占地旋转纯函数;map::TileProjection 委托复用,公开 API 不变。
- PlacementWorld 支持可选 GridConfig、多通道占用(channel)、bindTileLayer + GID 地形懒解析、XZ 平面 3D 放置、surface 表面接口(内置 plane)。
- BuildingDefinition 新增 channel/renderMode/visual2d/visual3d;rotationMode 支持 hex(60°)。
- 新增 buildingfx 渲染桥:按定义生成/同步 Renderable2D/3D、鬼影+占地光标、2D/3D 网格可视化。
- 新增 PlacementSession(放置/拆除/旋转/指针更新)。
- 测试:grid/building/buildingfx 新用例(REQUIRE 级)+ map/sceneloader 回归;示例 building-tilemap(等距/六角)、building-3d(XZ 地面)。
- 顺带修复 HEAD 上 sceneloader 未迁移 SceneLink 新 API 导致的编译失败(SceneLoader.cpp / test/sceneloader.cpp)。
2026-08-18 17:41:01 +08:00

6.0 KiB
Raw Blame History

建筑放置 — building

面向策略 / RTS / 经营模拟(ETS)的建筑放置框架:数据驱动定义、格子占用世界、鬼影预览、 可插拔校验与吸附规则。设计见 建筑放置系统设计。

可运行示例:examples/building(make building)。

入口

building <- eve.Building();
building.registerBuildingsFromJson(jsonText);
local world = building.newWorld(64, 64, 32.0); // 宽、高、格子像素/世界单位
local ghost = building.newGhost();

最小示例

building <- eve.Building();
building.registerBuildingsFromJson(@"
[
  {\"id\":\"house\",\"displayName\":\"House\",\"footprintW\":2,\"footprintH\":2,
   \"tags\":[\"house\"],\"cost\":{\"wood\":20}},
  {\"id\":\"road\",\"footprintW\":1,\"footprintH\":1,\"tags\":[\"road\"]}
]
");

world <- building.newWorld(32, 32, 32.0);
world.setId("town");
world.fillTerrain(1);

ghost <- building.newGhost();
ghost.setBuildingId("house");
ghost.setFromWorld(world, mouseX, mouseY); // 按定义 snapMode 吸附
if (ghost.validate(world)) {
    local id = world.placeGhost(ghost);
    print("placed " + id + "\n");
} else {
    print("cannot place: " + ghost.getReason() + "\n");
}

任务导向示例

1. 沿道路摆摊(邻接标签)

定义里设 "requireAdjacentTag":"road",先铺 road,再放摊位;失败原因为 adjacency_tag。

2. 码头只能建在水域

地形语义由 world.setTerrain(x,y,semantic) 写入;定义 "requireTerrain":[2]。 陆地格会得到 terrain_mismatch。

3. 自定义“金币不足”校验(C++)

PlacementSystem::registerValidateRule("needGold",
    [](const PlacementWorld&, const PlacementQuery&, std::string* reason) {
        if (playerGold < 10) { if (reason) *reason = "not_enough_gold"; return false; }
        return true;
    });

建筑定义 "validateRule":"needGold";脚本侧只需选用该规则名。

API 快查

Building(模块)

方法 说明
registerBuildingsFromJson(json) 批量注册定义,返回成功数
clearBuildingDefinitions() 清空定义表
getBuildingDefinitionCount() / hasBuildingDefinition(id) 查询
getBuildingDisplayName / Category / FootprintW / FootprintH 定义字段
getBuildingSnapMode / RotationMode / ValidateRule 策略名
buildingHasTag / getBuildingExtra / getBuildingCost 标签与扩展
newWorld(w,h,cellSize) / newGhost() 工厂
hasValidateRule / hasSnapRule 内置+已注册规则
clearChangeEvents / getChangeEvent* 变更事件 poll

PlacementWorld

方法 说明
setOrigin / setCellSize / worldToCell* / cellToWorld* 坐标
fillTerrain / setTerrain / getTerrain 地形语义
canPlace / canPlaceReason / placeAt / placeAtWorld / placeGhost 放置
removeBuilding / moveBuilding / clearBuildings 拆除与移动
getOccupant / isCellEmpty / getBuilding* 查询

Ghost

方法 说明
setBuildingId / setCell / setFromWorld / setRotationDeg / rotateBy 姿态
validate(world) / isValid / getReason 校验结果

生命周期注意

  • 将 PlacementWorld / Ghost 保存在全局或实体组件中;不要每帧 newWorld。
  • 变更事件不会自动清空;批量操作后调用 clearChangeEvents 或按帧 poll。
  • 自定义 validate/snap/hook 只能在 C++ 注册;脚本通过字符串策略名选用。
  • 模块不扣资源、不画鬼影;cost 仅作数据提示,由游戏在 hook 或脚本中处理。

多样式网格 / 3D / Tilemap(2026-08-18 起)

坐标换算统一走 grid 模块(纯数学),PlacementWorld 缺省为正交 2D,旧脚本零改动。

网格布局

world.setGridLayout("isometric");          // rectangle | hexagon | isometric | staggered | isometric-z-as-y
world.setGridPlane("xz");                  // xy(默认)| xz(3D:第二轴 -> 世界 Z)
world.setCellGap(2.0, 2.0);
world.setStagger("y", "odd");
world.setHexSideLength(14.0);

绑定 Tilemap(复用投影 + GID 地形)

world.bindTileLayer(layer);                // 同时复用 layer 的 orientation/stagger/hex
world.setTerrainGid(2, 2);                 // GID 2 -> 地形语义 2(水域)
world.setTerrainGidMapJson("{\"1\":1,\"2\":2}");  // 或整表

绑定后 getTerrain 从 GID 懒解析;setTerrain 手动覆盖优先。

3D 放置(XZ 平面 + 表面接口)

world.setGridPlane("xz");
ghost.setFromSurface(world, "plane", hitX, hitZ);   // 内置平面表面;高度 setPlaneSurfaceHeight
ghost.setFromWorld3D(world, worldX, worldY, worldZ); // 直接喂真实世界坐标
world.placeAtWorld3D("house", x, y, z, rot);

物理射线 / 高度场等表面由游戏在 C++ 侧 registerSurface 实现;引擎只提供接口 + plane。

通道(同格叠放)

定义 "channel":"floor" / "channel":"furniture",同一格允许跨通道建筑并存; getOccupantInChannel(channel, x, y) / isCellEmptyInChannel 按通道查询。

视觉双形态与渲染桥(eve.BuildingFx)

fx <- eve.BuildingFx();
fx.attach(world);
fx.sync(world);                        // 按定义 renderMode/visual2d/visual3d 生成/同步/销毁视觉
fx.updateGhost(world, ghost);          // 每帧:鬼影 + 占地光标(绿/红)
fx.setGridVisible(world, true);
fx.drawGrid2D(world, gfx);             // 2D 网格叠加(渲染循环中调用)
fx.drawGrid3D(world, gfx, 0.01);       // 3D 平面线框

放置会话(eve.Building.newSession)

session <- building.newSession();
session.startPlacement(world, "house");
session.updateFromSurface(world, "plane", hitX, hitZ);  // 或 updateFromWorld / updateFromWorld3D
session.setMode("remove");
session.execute();                     // place 放置 / remove 拆除