a3c38aa92a
- 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) - 搭建指南: 全局地面中心坐标+西边界旋转统一+院门改双开门
344 lines
12 KiB
Python
344 lines
12 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, 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}")
|