diff --git a/.claude/agent-memory/game-reviewer/MEMORY.md b/.claude/agent-memory/game-reviewer/MEMORY.md index 2c4138d..ba5874d 100644 --- a/.claude/agent-memory/game-reviewer/MEMORY.md +++ b/.claude/agent-memory/game-reviewer/MEMORY.md @@ -2,3 +2,4 @@ - [Gitea Token Permission](gitea-token-permission.md) — reviewer token 仅可评论,标签操作需用 PM token - [P-006 Review](p006-review-note.md) — Issue #17 白模搭建审查通过记录 +- [P-009 Review](p009-review-note.md) — Issue #20 收集系统程序设计审查通过记录 diff --git a/.claude/agent-memory/game-reviewer/p009-review-note.md b/.claude/agent-memory/game-reviewer/p009-review-note.md new file mode 100644 index 0000000..94ea98e --- /dev/null +++ b/.claude/agent-memory/game-reviewer/p009-review-note.md @@ -0,0 +1,19 @@ +--- +name: p009-review-note +description: Issue #20 P-009 收集系统程序设计审查通过记录 +metadata: + type: project +--- + +## P-009 收集系统程序设计审查 (2026-06-10) + +- Issue: #20 +- 审查类型: 程序设计(质量门 2) +- 产出文件: `docs/technical/收集系统-技术方案.md` +- 结果: **通过** + +### 审查要点 +- 技术方案文档路径实际为中文文件名 `收集系统-技术方案.md`(非 Issue 中标注的 `collection-system-tech.md`) +- 执行案文件名也是中文:`收集系统-执行案.md`(非 `collection-system-execution.md`) +- 6 个检查维度全部通过:执行案覆盖度、美术资源需求、架构清晰度、数据流、技术风险、工作量与拆分建议 +- 标签操作:移除了 gate:art-fail(30) 和 status:in-review(14),添加了 review:pass(33) diff --git a/tools/input-setup/setup_input_assets.py b/tools/input-setup/setup_input_assets.py index 69982a5..3a54f17 100644 --- a/tools/input-setup/setup_input_assets.py +++ b/tools/input-setup/setup_input_assets.py @@ -1,20 +1,19 @@ """UE5 编辑器 Python 脚本:创建 v0.1 角色控制所需的 InputAction 资产并更新 IMC。 -由 setup_input.bat 通过 UE5 的 -ExecutePythonScript 参数调用。 -在 UE5 编辑器 Python 环境中运行。 +使用方式: + 方式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 按键映射 + 2. 尝试更新 IMC_Default:添加 E / ESC / Tab 按键映射 3. 保存所有修改的资产 - -环境变量(由 setup_input.bat 设置): - INPUT_PROJECT_ROOT — 项目根目录绝对路径 """ import os import sys -from pathlib import Path +import traceback # ---- 检查 unreal 模块 ---- try: @@ -23,36 +22,54 @@ except ImportError: print("[InputSetup] FATAL: unreal 模块不可用,必须在 UE5 编辑器环境中运行") sys.exit(1) -# ---- 项目根目录 ---- -_project_root_str = os.environ.get("INPUT_PROJECT_ROOT", "") -if _project_root_str: - PROJECT_ROOT = Path(_project_root_str) -else: - print("[InputSetup] FATAL: INPUT_PROJECT_ROOT 环境变量未设置") - sys.exit(1) - # ---- 资产路径配置 ---- IA_PACKAGE = "/Game/Input/Actions" IMC_PATH = "/Game/Input/IMC_Default" +TEMPLATE_IA = f"{IA_PACKAGE}/IA_Look" # 用已存在的 IA 作为复制模板 -# 新增 InputAction 定义 -NEW_INPUT_ACTIONS = { - "IA_Interact": { - "description": "E 键交互输入动作", - "key": unreal.Key.E, - "triggers": [unreal.InputTriggerDown()], - }, - "IA_Cancel": { - "description": "ESC 取消输入动作", - "key": unreal.Key.ESCAPE, - "triggers": [unreal.InputTriggerDown()], - }, - "IA_SwitchInteract": { - "description": "Tab 切换交互目标输入动作", - "key": unreal.Key.TAB, - "triggers": [unreal.InputTriggerDown()], - }, -} +# 新增 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: @@ -60,161 +77,249 @@ def asset_exists(asset_path: str) -> bool: return unreal.EditorAssetLibrary.does_asset_exist(asset_path) -def create_input_action(name: str, description: str) -> unreal.InputAction: - """创建 InputAction 资产""" +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}") + print(f"[InputSetup] 已存在,跳过: {asset_path}") return unreal.load_asset(asset_path) - # 确保目录存在 - unreal.EditorAssetLibrary.make_directory(IA_PACKAGE) + # 确保目标目录存在 + if not unreal.EditorAssetLibrary.does_directory_exist(IA_PACKAGE): + unreal.EditorAssetLibrary.make_directory(IA_PACKAGE) - # 使用 AssetTools 创建 - asset_tools = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem) - if asset_tools: - # 尝试通过 EditorAssetSubsystem 创建 + # 复制模板资产 + 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() - new_asset = asset_tools.create_asset(name, IA_PACKAGE, unreal.InputAction, factory) - if new_asset: - print(f"[InputSetup] 创建成功: {asset_path} — {description}") - return new_asset + except Exception: + print("[InputSetup] InputActionFactory 不可用") + return None - # 备用方案:使用 Python BPLib 或直接构造 - print(f"[InputSetup] 尝试备用方案创建: {name}") - factory = unreal.InputActionFactory() - new_asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset( - name, IA_PACKAGE[5:], # 去掉 /Game/ 前缀 + 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] 备用方案创建成功: {asset_path}") - return new_asset + print(f"[InputSetup] Factory 创建成功: {asset_path} — {desc}") + return new_asset - print(f"[InputSetup] ERROR: 无法创建 {name}") + +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 add_key_mapping_to_imc(imc: unreal.InputMappingContext, ia: unreal.InputAction, key: unreal.Key): - """向 IMC 添加按键映射""" - mapping = unreal.InputMapping() - mapping.set_editor_property("action", ia) +def try_update_imc(created_ias: dict) -> bool: + """尝试更新 IMC_Default 添加按键映射。 - key_mapping = unreal.KeyMapping() - key_mapping.set_editor_property("key", key) - key_mapping.set_editor_property("scale", 1.0) - - # 设置触发器 - # key_mapping 的 triggers 由 IMC 内部管理 - - # 使用 IMC 的 API 添加映射 - try: - # UE5.7: 使用 mappings 属性 - existing_mappings = imc.get_editor_property("mappings") - new_mapping = unreal.InputMapping() - new_mapping.set_editor_property("action", ia) - - km = unreal.KeyMapping() - km.set_editor_property("key", key) - km.set_editor_property("scale", 1.0) - - # 设置 triggers - triggers = [unreal.InputTriggerDown()] - km.set_editor_property("triggers", triggers) - new_mapping.set_editor_property("key_mappings", [km]) - - existing_mappings.append(new_mapping) - imc.set_editor_property("mappings", existing_mappings) - return True - except Exception as e: - print(f"[InputSetup] 添加映射失败: {e}") - return False - - -def main(): - print("=" * 50) - print("[InputSetup] v0.1 角色控制 Input 资产创建脚本") - print(f"[InputSetup] 项目: {PROJECT_ROOT}") - print("=" * 50) - - # ---- Step 1: 创建 InputAction 资产 ---- - print("\n[InputSetup] Step 1: 创建 InputAction 资产") - created_ias = {} - for name, config in NEW_INPUT_ACTIONS.items(): - ia = create_input_action(name, config["description"]) - if ia: - created_ias[name] = ia - else: - print(f"[InputSetup] WARNING: {name} 创建失败,后续 IMC 映射将跳过此 IA") - - if not created_ias: - print("[InputSetup] ERROR: 没有成功创建任何 IA,终止") - return False - - # ---- Step 2: 保存新创建的 IA 资产 ---- - print("\n[InputSetup] Step 2: 保存新创建的资产") - for name, ia in created_ias.items(): - asset_path = f"{IA_PACKAGE}/{name}" - unreal.EditorAssetLibrary.save_asset(asset_path) - print(f"[InputSetup] 已保存: {asset_path}") - - # ---- Step 3: 更新 IMC_Default ---- - print(f"\n[InputSetup] Step 3: 更新 IMC: {IMC_PATH}") + 返回 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}") + print(f"[InputSetup] ERROR: IMC 不存在: {IMC_PATH}") return False imc = unreal.load_asset(IMC_PATH) if not imc: - print(f"[InputSetup] ERROR: 无法加载 IMC: {IMC_PATH}") + print(f"[InputSetup] ERROR: 无法加载 IMC") return False # 读取当前映射数量 try: current_mappings = imc.get_editor_property("mappings") - print(f"[InputSetup] 当前 IMC 映射数: {len(current_mappings)}") + print(f"[InputSetup] 当前映射数: {len(current_mappings)}") except Exception as e: - print(f"[InputSetup] WARNING: 无法读取映射: {e}") - current_mappings = [] + print(f"[InputSetup] 无法读取 mappings 属性: {e}") + return False - # 添加新映射 - for name, config in NEW_INPUT_ACTIONS.items(): - if name not in created_ias: + # 解析所有需要的 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 = config["key"] - success = add_key_mapping_to_imc(imc, ia, key) - if success: - print(f"[InputSetup] 已添加映射: {name} → {key}") + 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: 映射添加失败: {name}") + print(f"[InputSetup] WARNING: {ia_info['name']} 创建失败") - # ---- Step 4: 保存 IMC ---- - print("\n[InputSetup] Step 4: 保存 IMC") - unreal.EditorAssetLibrary.save_asset(IMC_PATH) - print(f"[InputSetup] 已保存: {IMC_PATH}") + if not created_ias: + print("[InputSetup] ERROR: 没有成功创建任何 IA") + return False - # ---- Step 5: 验证 ---- - print("\n[InputSetup] Step 5: 验证") - try: - updated_mappings = imc.get_editor_property("mappings") - print(f"[InputSetup] 更新后 IMC 映射数: {len(updated_mappings)}") - except: - pass - - # 验证 IA 资产 - for name in NEW_INPUT_ACTIONS: + # ---- Step 2: 保存新创建的 IA 资产 ---- + print(f"\n[InputSetup] Step 2/4: 保存资产 ({len(created_ias)} 个)") + for name in created_ias: asset_path = f"{IA_PACKAGE}/{name}" - exists = asset_exists(asset_path) - print(f"[InputSetup] {name}: {'✅ 存在' if exists else '❌ 不存在'}") + unreal.EditorAssetLibrary.save_asset(asset_path) + print(f"[InputSetup] 已保存: {asset_path}") - print("\n" + "=" * 50) - print("[InputSetup] 完成!请在 UE5 编辑器中验证。") - print("[InputSetup] 注意:需要在 BP_ChildHoodCharacter 中将新增 IA 属性指向对应资产") - print("=" * 50) + # ---- 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