Files
ChildHood/tools/input-setup/setup_input_assets.py
T
starmen1265 82fb409a9e chore: 更新审查记忆和输入资产创建脚本
- reviewer记忆: 新增P-009审查通过记录
- setup_input_assets.py: 重写为多方案容错架构(模板复制+Factory备选)

Task: P-009, P-011
2026-06-11 00:54:06 +08:00

331 lines
11 KiB
Python

"""UE5 编辑器 Python 脚本:创建 v0.1 角色控制所需的 InputAction 资产并更新 IMC。
使用方式:
方式1 — 在编辑器 Python 控制台运行:
py "E:/MiceCola/work/ChildHood/tools/input-setup/setup_input_assets.py"
方式2 — 通过 setup_input.bat 启动编辑器执行
功能:
1. 创建 3 个 InputAction 资产:IA_Interact, IA_Cancel, IA_SwitchInteract
2. 尝试更新 IMC_Default:添加 E / ESC / Tab 按键映射
3. 保存所有修改的资产
"""
import os
import sys
import traceback
# ---- 检查 unreal 模块 ----
try:
import unreal
except ImportError:
print("[InputSetup] FATAL: unreal 模块不可用,必须在 UE5 编辑器环境中运行")
sys.exit(1)
# ---- 资产路径配置 ----
IA_PACKAGE = "/Game/Input/Actions"
IMC_PATH = "/Game/Input/IMC_Default"
TEMPLATE_IA = f"{IA_PACKAGE}/IA_Look" # 用已存在的 IA 作为复制模板
# 新增 InputAction 定义(key_name 为字符串,运行时解析)
NEW_INPUT_ACTIONS = [
{"name": "IA_Interact", "desc": "E 键交互输入动作", "key_name": "E"},
{"name": "IA_Cancel", "desc": "ESC 取消输入动作", "key_name": "Escape"},
{"name": "IA_SwitchInteract", "desc": "Tab 切换交互目标", "key_name": "Tab"},
]
# 按键名 → UE5 Key 对象的多种尝试路径
KEY_RESOLVE_ORDER = ["E", "Escape", "Tab"]
def resolve_key(key_name: str):
"""尝试多种方式从 UE5 Python API 获取 Key 对象。
UE5 各版本的 Python 绑定对 Key 的暴露方式不同,
这里依次尝试多种路径,找到可用的那一个。
"""
attempts = [
# 方式1: unreal.Key 枚举属性 (如 unreal.Key.E, unreal.Key.Escape)
lambda: getattr(unreal.Key, key_name),
# 方式2: unreal.Keys 类 (部分版本用这个)
lambda: getattr(unreal.Keys, key_name),
# 方式3: 全大写 (如 ESCAPE, TAB)
lambda: getattr(unreal.Key, key_name.upper()),
lambda: getattr(unreal.Keys, key_name.upper()),
# 方式4: KEY_ 前缀 (如 KEY_E, KEY_ESCAPE)
lambda: getattr(unreal.Key, f"KEY_{key_name.upper()}"),
lambda: getattr(unreal.Keys, f"KEY_{key_name.upper()}"),
# 方式5: 构造函数 (部分版本支持从字符串构造)
lambda: unreal.Key(key_name),
]
for i, attempt in enumerate(attempts, 1):
try:
key = attempt()
if key is not None:
print(f"[InputSetup] Key('{key_name}') 解析成功 (方式{i})")
return key
except Exception:
continue
print(f"[InputSetup] Key('{key_name}') 所有解析方式均失败")
return None
def asset_exists(asset_path: str) -> bool:
"""检查资产是否已存在"""
return unreal.EditorAssetLibrary.does_asset_exist(asset_path)
def create_input_action_by_duplicate(name: str, desc: str) -> object:
"""通过复制已有 IA 模板创建新 InputAction 资产。
这是最可靠的方式——不依赖 Factory 类的 Python 绑定。
"""
asset_path = f"{IA_PACKAGE}/{name}"
if asset_exists(asset_path):
print(f"[InputSetup] 已存在,跳过: {asset_path}")
return unreal.load_asset(asset_path)
# 确保目标目录存在
if not unreal.EditorAssetLibrary.does_directory_exist(IA_PACKAGE):
unreal.EditorAssetLibrary.make_directory(IA_PACKAGE)
# 复制模板资产
new_asset = unreal.EditorAssetLibrary.duplicate_asset(TEMPLATE_IA, asset_path)
if new_asset:
print(f"[InputSetup] 复制创建成功: {asset_path} — {desc}")
return new_asset
print(f"[InputSetup] 复制创建失败: {name}")
return None
def create_input_action_by_factory(name: str, desc: str) -> object:
"""通过 AssetTools + Factory 创建 InputAction(备用方案)。"""
asset_path = f"{IA_PACKAGE}/{name}"
if asset_exists(asset_path):
return unreal.load_asset(asset_path)
try:
factory = unreal.InputActionFactory()
except Exception:
print("[InputSetup] InputActionFactory 不可用")
return None
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
if not asset_tools:
return None
# create_asset 的 package_path 需要去掉 /Game/ 前缀
new_asset = asset_tools.create_asset(
name,
IA_PACKAGE[5:], # "Input/Actions"
unreal.InputAction,
factory
)
if new_asset:
print(f"[InputSetup] Factory 创建成功: {asset_path} — {desc}")
return new_asset
def create_input_action(name: str, desc: str) -> object:
"""创建 InputAction 资产,依次尝试多种方法。"""
asset_path = f"{IA_PACKAGE}/{name}"
# 已存在则直接加载
if asset_exists(asset_path):
print(f"[InputSetup] 已存在: {asset_path}")
return unreal.load_asset(asset_path)
print(f"[InputSetup] 尝试创建: {name} — {desc}")
# 方法1: 复制已有模板(最可靠)
result = create_input_action_by_duplicate(name, desc)
if result:
return result
# 方法2: Factory 创建
result = create_input_action_by_factory(name, desc)
if result:
return result
print(f"[InputSetup] ERROR: 所有创建方法均失败: {name}")
return None
def try_update_imc(created_ias: dict) -> bool:
"""尝试更新 IMC_Default 添加按键映射。
返回 True 表示成功,False 表示需要手动操作。
由于 UE5 Python API 对 Enhanced Input 的暴露有限,
此功能可能失败——失败时提供手动操作指引。
"""
print(f"[InputSetup] 加载 IMC: {IMC_PATH}")
if not asset_exists(IMC_PATH):
print(f"[InputSetup] ERROR: IMC 不存在: {IMC_PATH}")
return False
imc = unreal.load_asset(IMC_PATH)
if not imc:
print(f"[InputSetup] ERROR: 无法加载 IMC")
return False
# 读取当前映射数量
try:
current_mappings = imc.get_editor_property("mappings")
print(f"[InputSetup] 当前映射数: {len(current_mappings)}")
except Exception as e:
print(f"[InputSetup] 无法读取 mappings 属性: {e}")
return False
# 解析所有需要的 Key
key_map = {}
for ia_info in NEW_INPUT_ACTIONS:
if ia_info["name"] not in created_ias:
continue
key = resolve_key(ia_info["key_name"])
if key is None:
print(f"[InputSetup] 无法解析按键: {ia_info['key_name']},跳过 IMC 自动更新")
return False
key_map[ia_info["name"]] = key
# 尝试添加映射
success_count = 0
for ia_info in NEW_INPUT_ACTIONS:
name = ia_info["name"]
if name not in created_ias or name not in key_map:
continue
ia = created_ias[name]
key = key_map[name]
try:
new_mapping = unreal.InputMapping()
new_mapping.set_editor_property("action", ia)
km = unreal.KeyMapping()
km.set_editor_property("key", key)
# 尝试设置触发器(可能不支持)
try:
km.set_editor_property("triggers", [unreal.InputTriggerDown()])
except Exception:
pass # 触发器使用默认值
new_mapping.set_editor_property("key_mappings", [km])
current_mappings.append(new_mapping)
imc.set_editor_property("mappings", current_mappings)
success_count += 1
print(f"[InputSetup] 映射已添加: {name} → {ia_info['key_name']}")
except Exception as e:
print(f"[InputSetup] 映射添加失败: {name} — {e}")
return False
if success_count == len(created_ias):
# 保存 IMC
unreal.EditorAssetLibrary.save_asset(IMC_PATH)
print(f"[InputSetup] IMC 已保存")
return True
return False
def main():
print("=" * 60)
print("[InputSetup] v0.1 角色控制 Input 资产创建脚本")
print("=" * 60)
# ---- Step 1: 创建 InputAction 资产 ----
print("\n[InputSetup] Step 1/4: 创建 InputAction 资产")
print(f"[InputSetup] 模板: {TEMPLATE_IA}")
if not asset_exists(TEMPLATE_IA):
print(f"[InputSetup] ERROR: 模板 IA 不存在: {TEMPLATE_IA}")
print("[InputSetup] 可用 IA 资产:")
# 尝试列出 Actions 目录下的资产作为替代
try:
assets = unreal.EditorAssetLibrary.list_assets(IA_PACKAGE)
for a in assets:
print(f"[InputSetup] {a}")
except Exception:
pass
return False
created_ias = {}
for ia_info in NEW_INPUT_ACTIONS:
ia = create_input_action(ia_info["name"], ia_info["desc"])
if ia:
created_ias[ia_info["name"]] = ia
else:
print(f"[InputSetup] WARNING: {ia_info['name']} 创建失败")
if not created_ias:
print("[InputSetup] ERROR: 没有成功创建任何 IA")
return False
# ---- Step 2: 保存新创建的 IA 资产 ----
print(f"\n[InputSetup] Step 2/4: 保存资产 ({len(created_ias)} 个)")
for name in created_ias:
asset_path = f"{IA_PACKAGE}/{name}"
unreal.EditorAssetLibrary.save_asset(asset_path)
print(f"[InputSetup] 已保存: {asset_path}")
# ---- Step 3: 尝试更新 IMC ----
print(f"\n[InputSetup] Step 3/4: 更新 IMC_Default")
imc_ok = False
try:
imc_ok = try_update_imc(created_ias)
except Exception as e:
print(f"[InputSetup] IMC 更新异常: {e}")
traceback.print_exc()
if not imc_ok:
print("[InputSetup] ⚠️ IMC 自动更新未成功,需要手动操作(见下方指引)")
# ---- Step 4: 验证 ----
print(f"\n[InputSetup] Step 4/4: 验证")
all_ok = True
for ia_info in NEW_INPUT_ACTIONS:
asset_path = f"{IA_PACKAGE}/{ia_info['name']}"
exists = asset_exists(asset_path)
status = "✅" if exists else "❌"
print(f"[InputSetup] {status} {ia_info['name']}")
if not exists:
all_ok = False
# ---- 总结 ----
print("\n" + "=" * 60)
if imc_ok:
print("[InputSetup] ✅ 全部完成(IA 创建 + IMC 更新)")
print("[InputSetup] ⚠️ 仍需手动:BP_ChildHoodCharacter → Details → Input → 绑定 IA 属性")
else:
print("[InputSetup] ⚠️ IA 资产已创建,IMC 需手动配置")
print()
print("[InputSetup] === 手动操作指引 ===")
print()
print("1. 打开 IMC_Default (Content/Input/IMC_Default)")
print(" 添加 3 个映射:")
for ia_info in NEW_INPUT_ACTIONS:
print(f" - {ia_info['key_name']} → {ia_info['name']}")
print()
print("2. 打开 BP_ChildHoodCharacter (Content/BP/Character/)")
print(" Details → Input → 设置以下属性:")
print(" - Interact Action → IA_Interact")
print(" - Cancel Action → IA_Cancel")
print(" - Switch Interact Action → IA_SwitchInteract")
print()
print("=" * 60)
return True
if __name__ == "__main__":
success = main()
if not success:
sys.exit(1)