Files
ChildHood/tools/level-builder/builder/scene_builder.py
T
starmen1265 0f96b79dba feat(tools): 白模关卡自动生成Python脚本——Level Builder
两阶段流水线:搭建指南Markdown → JSON → UE5 Actor
- 解析器:4种表格变体+范围展开+坐标表达式计算
- 配置:37种BSP资产映射+13种材质+7区域定义
- 验证:265个Actor全部通过,0错误
- Builder:UE5 Python API自动放置Actor+材质+Outliner
- 报告:自动生成搭建报告Markdown

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:03:45 +08:00

257 lines
8.5 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
# 将项目根目录加入 sys.path 以支持 config 导入
_SCRIPT_DIR = Path(__file__).resolve().parent
_TOOLS_DIR = _SCRIPT_DIR.parent.parent.parent
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
from tools.level_builder.config.asset_mapping import MESH_ASSETS
from tools.level_builder.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)
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}'")
return None
# 生成
loc = actor_def.get("location", [0, 0, 0])
actor = editor_subsystem.spawn_actor_from_object(
static_mesh,
unreal.Vector(loc[0], loc[1], loc[2])
)
if not actor:
return None
# 设置 Transform
rot = actor_def.get("rotation", [0, 0, 0])
actor.set_actor_rotation(unreal.Rotator(rot[0], rot[1], rot[2]))
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(rot[0], rot[1], rot[2]))
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(rot[0], rot[1], rot[2]))
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