Files
ChildHood/tools/level-builder/builder/scene_builder.py
T
starmen1265 a3c38aa92a fix(level-builder): 锚点补偿+旋转修复+天空盒+解析器去重
- asset_mapping: 38种BSP资产实测锚点+pivot_offset_xy自动补偿
- scene_builder: Rotator改关键字参数修正旋转轴映射
- scene_builder: spawn前XY锚点补偿(支持Yaw旋转)
- scene_builder: 新增ExponentialHeightFog+大气效果后处理
- build_cmdline: 构建后自动添加雾+GameMode+DirectionalLight大气
- build_guide_parser: 去重+全局地面+旋转列名兼容+边界段范围修正
- BarrierWall实际尺寸修正(60×210非20×100)
- 搭建指南: 全局地面中心坐标+西边界旋转统一+院门改双开门
2026-06-08 19:05:50 +08:00

344 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""UE5 场景构建器。
在 UE5 编辑器的 Python 环境中运行。读取 JSON 中间数据,
使用 unreal Python API 创建 Actor、设置 Transform/材质/Outliner 文件夹。
使用方式:
在 UE5 编辑器 Output Log 中切换到 Python,执行:
exec(open(r"path/to/scene_builder.py").read())
build_level(r"path/to/level_v01.json")
"""
import json
import logging
import sys
from pathlib import Path
# 尝试导入 unreal,失败时给出提示
try:
import unreal
HAS_UNREAL = True
except ImportError:
HAS_UNREAL = False
# 将 level-builder 根目录加入 sys.path 以支持 config 导入
# 注意: 目录名含连字符 (level-builder),不能用 tools.level_builder 形式导入
_SCRIPT_DIR = Path(__file__).resolve().parent # builder/
_LB_DIR = _SCRIPT_DIR.parent # tools/level-builder/
if str(_LB_DIR) not in sys.path:
sys.path.insert(0, str(_LB_DIR))
from config.asset_mapping import MESH_ASSETS, get_pivot_compensation
from config.material_mapping import MATERIAL_PATHS
logger = logging.getLogger("level_builder.builder")
def build_level(json_path: str, clear_existing: bool = True, zones: list[str] = None):
"""从 JSON 数据构建 UE5 关卡。
Args:
json_path: 中间 JSON 文件路径。
clear_existing: 是否清除已有的 LVL_* Actor。
zones: 仅构建指定区域(如 ["Z1", "Z3"]),None 表示全部。
"""
if not HAS_UNREAL:
print("[错误] 未检测到 unreal 模块。此脚本必须在 UE5 编辑器 Python 环境中运行。")
return
# 加载数据
data = _load_json(json_path)
if not data:
return
actors = data.get("actors", [])
print(f"[LevelBuilder] 加载 {len(actors)} 个 Actor 定义")
# 可选:清除已有 LVL_* Actor
if clear_existing:
_clear_existing_actors()
# 过滤区域
if zones:
actors = [a for a in actors if a.get("zone_id") in zones or a.get("zone_id") == "GLOBAL"]
print(f"[LevelBuilder] 过滤为 {len(actors)} 个 Actor(区域: {zones})")
# 获取编辑器子系统
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
if not editor_subsystem:
print("[错误] 无法获取 EditorActorSubsystem")
return
# 统计
success = 0
failed = 0
build_log = []
for i, actor_def in enumerate(actors):
name = actor_def.get("actor_name", f"Actor_{i}")
asset_type = actor_def.get("asset_type", "static_mesh")
try:
if asset_type == "player_start":
actor = _spawn_player_start(editor_subsystem, actor_def)
elif asset_type == "directional_light":
actor = _spawn_directional_light(editor_subsystem, actor_def)
elif asset_type == "sky_light":
actor = _spawn_sky_light(editor_subsystem, actor_def)
elif asset_type == "point_light":
actor = _spawn_point_light(editor_subsystem, actor_def)
elif asset_type == "exp_height_fog":
actor = _spawn_exp_height_fog(editor_subsystem, actor_def)
else:
actor = _spawn_mesh_actor(editor_subsystem, actor_def)
if actor:
success += 1
build_log.append({"name": name, "status": "ok", **actor_def})
else:
failed += 1
build_log.append({"name": name, "status": "failed", "reason": "spawn returned None"})
except Exception as e:
failed += 1
error_msg = str(e)
build_log.append({"name": name, "status": "error", "reason": error_msg})
print(f" [错误] {name}: {error_msg}")
# 进度
if (i + 1) % 20 == 0:
print(f" 进度: {i + 1}/{len(actors)} ({success} 成功, {failed} 失败)")
print(f"\n[LevelBuilder] 构建完成!")
print(f" 成功: {success}")
print(f" 失败: {failed}")
print(f" 总计: {len(actors)}")
# 保存构建日志
log_path = Path(json_path).parent / "build_log.json"
with open(log_path, "w", encoding="utf-8") as f:
json.dump(build_log, f, ensure_ascii=False, indent=2)
print(f" 日志: {log_path}")
return build_log
def _load_json(path: str) -> dict:
"""加载 JSON 文件。"""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[错误] 无法加载 JSON: {e}")
return None
def _clear_existing_actors():
"""清除场景中所有 LVL_ 前缀的 Actor。"""
editor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
lvl_actors = [a for a in all_actors if a.get_actor_label().startswith("LVL_")]
if lvl_actors:
print(f"[LevelBuilder] 清除 {len(lvl_actors)} 个已有 LVL_* Actor...")
for a in lvl_actors:
unreal.EditorLevelLibrary.destroy_actor(a)
else:
print("[LevelBuilder] 无已有 LVL_* Actor 需清除")
def _spawn_mesh_actor(editor_subsystem, actor_def: dict):
"""生成 StaticMesh Actor。"""
asset_key = actor_def.get("asset_key", "")
if not asset_key or asset_key not in MESH_ASSETS:
print(f" [警告] {actor_def['actor_name']}: 资产 '{asset_key}' 未在映射表中找到,使用默认 Cube")
asset_key = "Cube"
mesh_path = MESH_ASSETS[asset_key]["path"]
static_mesh = unreal.load_asset(mesh_path)
if not static_mesh:
print(f" [警告] {actor_def['actor_name']}: 无法加载资产 '{mesh_path}',使用 Cube 占位")
# 使用 Cube 作为占位,但名称加 [MISSING] 前缀以醒目标记
cube_path = MESH_ASSETS["Cube"]["path"]
static_mesh = unreal.load_asset(cube_path)
if not static_mesh:
print(f" [错误] {actor_def['actor_name']}: 连 Cube 都无法加载")
return None
# 标记为缺失资产
actor_def = dict(actor_def) # 不修改原始 dict
actor_def["actor_name"] = f"[MISSING]{actor_def['actor_name']}"
actor_def["material"] = "MI_DevPink" # 醒目的粉色标记
# 生成 — 应用锚点补偿
loc = actor_def.get("location", [0, 0, 0])
scale = actor_def.get("scale", [1, 1, 1])
rot = actor_def.get("rotation", [0, 0, 0])
dx, dy = get_pivot_compensation(asset_key, scale, rot)
spawn_x = loc[0] - dx
spawn_y = loc[1] - dy
actor = editor_subsystem.spawn_actor_from_object(
static_mesh,
unreal.Vector(spawn_x, spawn_y, loc[2])
)
if not actor:
return None
# 设置 Transform
rot = actor_def.get("rotation", [0, 0, 0])
# 使用关键字参数避免位置参数顺序歧义(UE5.7 Rotator 位置参数非 P,Y,R 顺序)
actor.set_actor_rotation(
unreal.Rotator(pitch=rot[0], yaw=rot[1], roll=rot[2]), False
)
scale = actor_def.get("scale", [1, 1, 1])
actor.set_actor_scale3d(unreal.Vector(scale[0], scale[1], scale[2]))
# 设置材质
material_name = actor_def.get("material", "")
if material_name and material_name in MATERIAL_PATHS:
mat_path = MATERIAL_PATHS[material_name]
material = unreal.load_asset(mat_path)
if material:
mesh_comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if mesh_comp:
mesh_comp.set_material(0, material)
# 设置名称和文件夹
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_player_start(editor_subsystem, actor_def: dict):
"""生成 PlayerStart Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.PlayerStart,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
rot = actor_def.get("rotation", [0, 0, 0])
actor.set_actor_rotation(
unreal.Rotator(pitch=rot[0], yaw=rot[1], roll=rot[2]), False
)
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_directional_light(editor_subsystem, actor_def: dict):
"""生成 DirectionalLight Actor。"""
loc = actor_def.get("location", [0, 0, 500])
actor = editor_subsystem.spawn_actor_from_class(
unreal.DirectionalLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
rot = actor_def.get("rotation", [-45, 315, 0])
actor.set_actor_rotation(
unreal.Rotator(pitch=rot[0], yaw=rot[1], roll=rot[2]), False
)
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_sky_light(editor_subsystem, actor_def: dict):
"""生成 SkyLight Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.SkyLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_point_light(editor_subsystem, actor_def: dict):
"""生成 PointLight Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.PointLight,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
return actor
def _spawn_exp_height_fog(editor_subsystem, actor_def: dict):
"""生成 ExponentialHeightFog Actor。"""
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_class(
unreal.ExponentialHeightFog,
unreal.Vector(loc[0], loc[1], loc[2])
)
if actor:
actor.set_actor_label(actor_def["actor_name"])
folder = actor_def.get("folder", "")
if folder:
actor.set_folder_path(folder)
# 设置雾密度
fog_comp = actor.get_component_by_class(unreal.ExponentialHeightFogComponent)
if fog_comp:
fog_density = actor_def.get("fog_density", 0.02)
fog_comp.set_editor_property("fog_density", fog_density)
return actor
def setup_post_build():
"""关卡后处理:设置 GameMode、启用大气雾等。
在 build_level 之后调用。
"""
if not HAS_UNREAL:
return
# 1. 设置关卡 GameMode — 使用 BP_ThirdPersonGameMode
try:
# load_asset 用不带 _C 的资产路径
gm_asset_path = "/Game/ThirdPerson/Blueprints/BP_ThirdPersonGameMode.BP_ThirdPersonGameMode"
gm_class_path = gm_asset_path + "_C"
gm_asset = unreal.load_asset(gm_asset_path)
if gm_asset:
all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
for a in all_actors:
if a.get_class().get_name() == "WorldSettings":
# game_mode_override 接受 UClass,通过 SoftClassPath 设置
soft_ref = unreal.SoftClassPath(gm_class_path)
a.set_editor_property("game_mode_override", soft_ref)
print(f"[LevelBuilder] ✅ GameMode 已设置: {gm_class_path}")
break
else:
print(f"[LevelBuilder] ⚠️ GameMode 资产未找到: {gm_asset_path}")
except Exception as e:
print(f"[LevelBuilder] ⚠️ 设置 GameMode 失败: {e}")
# 2. 启用 DirectionalLight 的大气效果
try:
all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
for a in all_actors:
if "DirectionalLight" in a.get_class().get_name():
dl_comp = a.get_component_by_class(unreal.DirectionalLightComponent)
if dl_comp:
dl_comp.set_editor_property("atmosphere_sun_light", True)
print(f"[LevelBuilder] ✅ DirectionalLight 大气效果已启用")
break
except Exception as e:
print(f"[LevelBuilder] ⚠️ DirectionalLight 设置失败: {e}")