"""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}")