258 lines
8.6 KiB
Python
258 lines
8.6 KiB
Python
"""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
|
||
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)
|
||
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]), 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(rot[0], rot[1], 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(rot[0], rot[1], 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
|