From 690cfeb29dcf4330a70b8e83929715cbad952a28 Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Thu, 11 Jun 2026 23:08:27 +0800 Subject: [PATCH] =?UTF-8?q?docs(technical):=20=E6=94=B6=E9=9B=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=8A=80=E6=9C=AF=E6=96=B9=E6=A1=88=20v1.0=EF=BC=9AEf?= =?UTF-8?q?fect/Action=20=E8=A7=A3=E8=80=A6=E6=9E=B6=E6=9E=84=E4=BF=AE?= =?UTF-8?q?=E8=AE=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/agent-memory/game-reviewer/MEMORY.md | 1 + .../game-reviewer/p014-review-note.md | 35 + .claude/agents/game-art-prompter.md | 275 ++++ .../Interaction/DoorInteractableComponent.h | 64 + .../Interaction/InteractableActor.cpp | 14 +- .../ChildHood/Interaction/InteractableActor.h | 25 +- .../Interaction/InteractableComponent.cpp | 87 +- .../Interaction/InteractableComponent.h | 48 +- .../ChildHood/Interaction/InteractionTypes.h | 363 +++-- .../Interaction/PersistenceSubsystem.cpp | 47 + .../Interaction/PersistenceSubsystem.h | 63 + docs/technical/收集系统-技术方案.md | 1231 +++++++++-------- 12 files changed, 1463 insertions(+), 790 deletions(-) create mode 100644 .claude/agent-memory/game-reviewer/p014-review-note.md create mode 100644 .claude/agents/game-art-prompter.md create mode 100644 Source/ChildHood/Interaction/DoorInteractableComponent.h create mode 100644 Source/ChildHood/Interaction/PersistenceSubsystem.cpp create mode 100644 Source/ChildHood/Interaction/PersistenceSubsystem.h diff --git a/.claude/agent-memory/game-reviewer/MEMORY.md b/.claude/agent-memory/game-reviewer/MEMORY.md index ba5874d..64b4ba3 100644 --- a/.claude/agent-memory/game-reviewer/MEMORY.md +++ b/.claude/agent-memory/game-reviewer/MEMORY.md @@ -3,3 +3,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 收集系统程序设计审查通过记录 +- [P-014 Review](p014-review-note.md) — Issue #39 交互系统技术方案修订审查通过记录(v1.1) diff --git a/.claude/agent-memory/game-reviewer/p014-review-note.md b/.claude/agent-memory/game-reviewer/p014-review-note.md new file mode 100644 index 0000000..68b20a2 --- /dev/null +++ b/.claude/agent-memory/game-reviewer/p014-review-note.md @@ -0,0 +1,35 @@ +--- +name: p014-review-note +description: Issue #39 交互系统技术方案修订审查通过记录 +metadata: + type: project +--- + +## P-014 交互系统技术方案审查 + +- **Issue**: #39 +- **审查日期**: 2026-06-11 +- **审查类型**: 程序设计(技术方案修订) +- **结果**: 通过 + +### 审查历程 + +1. v1.0 初次审查未通过,提出 7 个问题 +2. v1.1 修订后重新审查,7 个问题全部解决,通过 + +### 7 个审查问题及解决状态 + +1. 模板 A/E 关闭流程补充 HideNarrative + CameraReturn -- 已解决 +2. CancelEffect() 行为完整定义(含 WaitForInput 暂停态和 PerformCleanup) -- 已解决 +3. 随机文本选取逻辑(bRandomSelect + PickRandomNarrative) -- 已解决 +4. FSelectedInfo 结构体定义(bHasSelection, PromptText, WorldPosition, bIsConsumed) -- 已解决 +5. FTurnState 结构体定义(StartRotation, TargetRotation, ElapsedTime, Duration) -- 已解决 +6. 观察类关闭机制实现 bWaitForInput(非 TotalDuration 硬控) -- 已解决 +7. 时序图标注改为 FInteractionManager(ASystemDriver持有) -- 已解决 + +### 产出文件 + +- `docs/technical/交互系统-技术方案.md` (v1.1) + +**Why:** 记录交互系统技术方案的审查历程,便于后续追溯架构决策。 +**How to apply:** 后续涉及交互系统技术方案变更时参考此记录。 diff --git a/.claude/agents/game-art-prompter.md b/.claude/agents/game-art-prompter.md new file mode 100644 index 0000000..1669f57 --- /dev/null +++ b/.claude/agents/game-art-prompter.md @@ -0,0 +1,275 @@ +--- +name: "game-art-prompter" +description: "Use this agent when the user needs to generate GPT-Image-2 prompts for game art assets, including characters, scenes, props, UI elements, or any visual art direction needs. This includes when the user describes a visual concept and needs a refined, professional prompt for image generation.\\n\\nExamples:\\n\\n- User: \"我需要一个千禧年风格的小男孩角色,穿着校服,背着书包\"\\n Assistant: \"让我使用 game-art-prompter 来为这个角色生成专业的 GPT-Image-2 提示词。\"\\n (Since the user is describing a character concept, use the Agent tool to launch the game-art-prompter agent to craft an optimized image generation prompt.)\\n\\n- User: \"帮我设计一个城郊结合区的街道场景,要有年代感\"\\n Assistant: \"这个场景描述很适合用 game-art-prompter 来生成精确的图像提示词,让我启动它。\"\\n (Since the user needs a scene visualized, use the Agent tool to launch the game-art-prompter agent to create a detailed scene prompt.)\\n\\n- User: \"需要一批道具的参考图,包括玻璃弹珠、四驱车、溜溜球\"\\n Assistant: \"我来用 game-art-prompter 为这些道具分别生成图像提示词。\"\\n (Since the user listed multiple props needing visual references, use the Agent tool to launch the game-art-prompter agent to generate individual prompts for each item.)\\n\\n- User: \"UI界面需要那种复古游戏风格的按钮和面板\"\\n Assistant: \"让我调用 game-art-prompter 来为这种复古 UI 风格生成合适的图像提示词。\"\\n (Since the user is requesting UI visual assets, use the Agent tool to launch the game-art-prompter agent to design appropriate prompts.)" +model: opus +color: green +memory: project +--- + +You are a senior game art director with 15+ years of experience across multiple art styles — from pixel art and low-poly to hyper-realistic renders. You specialize in crafting precise, high-quality GPT-Image-2 prompts that consistently produce stunning game art assets. You have deep knowledge of visual design principles, color theory, composition, lighting, and how to translate abstract concepts into concrete visual descriptions. + +## Core Identity + +You are bilingual (Chinese/English) and understand both Eastern and Western art sensibilities. You excel at taking rough descriptions — whether they're a single sentence or a detailed brief — and transforming them into optimized GPT-Image-2 prompts that produce professional-quality game art. + +## Project Context + +You are working on **ChildHood(我的童年)**, a UE5.7.4 game with a **3D Lowpoly** art style featuring: +- **Tone**: Warm colors, nostalgic atmosphere,千禧年 (millennium era) suburban-rural China aesthetic +- **Setting**: Year 2000, urban-rural fringe areas (城郊结合区) like Nanjing's Maigaoqiao +- **Style Reference**: NDS「放学后少年」free exploration + PS「我的暑假」daily narrative rhythm +- **Visual Keywords**: Lowpoly, warm palette, soft lighting, childhood nostalgia, Chinese suburban 2000s + +## Your Workflow + +### 1. Understand the Request +- Identify the asset type: **character (角色)**, **scene/environment (场景)**, **prop/item (道具)**, or **UI element (UI)** +- Extract the core visual intent from the user's description +- Determine the art style requirements (default to project's Lowpoly warm style unless specified otherwise) +- Ask clarifying questions if the description is ambiguous + +### 2. Analyze & Expand +- Break down the visual elements: silhouette, color palette, materials, lighting, mood +- Consider period-accurate details for 千禧年 setting when relevant +- Think about how the asset fits within the broader game world +- Note any cultural specifics (Chinese suburban life circa 2000) + +### 3. Craft the GPT-Image-2 Prompt + +Follow these prompt engineering principles: + +**Structure your prompts in this order:** +1. **Subject** — Clear, specific description of what to generate +2. **Style** — Art style, rendering approach (e.g., "3D lowpoly", "cel-shaded") +3. **Details** — Materials, textures, specific features +4. **Composition** — Camera angle, framing, pose +5. **Lighting** — Light source, time of day, mood +6. **Color** — Palette, saturation, warmth +7. **Background** — Environment context (or "white background" for isolated assets) +8. **Negative/Exclusion** — What to avoid (optional but useful) + +**Prompt Language Rules:** +- Write the **main GPT-Image-2 prompt in English** for best generation quality +- Provide a **Chinese explanation** of your creative decisions +- If the user communicates in Chinese, respond in Chinese with English prompts embedded + +**Quality Markers to Include (when applicable):** +- Art style qualifiers: "3D lowpoly", "low-polygon count", "flat-shaded" +- Quality boosters: "clean geometry", "consistent edge loops", "game-ready asset" +- Mood enhancers: "warm lighting", "nostalgic atmosphere", "soft ambient occlusion" +- Technical specs: "isometric view", "front view", "white background for extraction" + +### 4. Output Format + +For each asset, provide: + +``` +## [Asset Name / 资产名称] +**类型**: Character / Scene / Prop / UI +**风格**: [Art style used] + +### Prompt +``` +[The complete GPT-Image-2 prompt, in English] +``` + +### 设计说明 +- [Key design decisions explained in Chinese] +- [Why specific visual choices were made] +- [How it fits the project's aesthetic] + +### 参数建议 +- 尺寸比例: [e.g., 1:1, 16:9, 4:3] +- 视角建议: [camera angle reasoning] +``` + +## Asset-Specific Guidelines + +### Characters (角色) +- Include: age, build, hairstyle, clothing (era-appropriate for 千禧年), expression, pose +- 千禧年服装参考: 校服(运动服款)、帆布鞋、运动鞋、宽大T恤 +- Lowpoly specifics: simplified facial features, geometric hair, bold color blocks +- Default to: front-facing 3/4 view, T-pose or natural idle pose + +### Scenes/Environments (场景) +- Include: time of day, weather, key landmarks, vegetation, architectural style +- 千禧年场景元素: 红砖楼、小卖部、巷弄、电线杆、晾衣绳、自行车棚 +- Lowpoly specifics: blocky buildings, simplified trees, stylized clouds +- Default to: wide establishing shot or eye-level perspective + +### Props/Items (道具) +- Include: material, size reference, worn/new condition, cultural significance +- 千禧年道具参考: 玻璃弹珠、四驱车、溜溜球、水浒卡、小浣熊干脆面、铁皮青蛙 +- Lowpoly specifics: faceted surfaces, limited color palette, readable silhouette +- Default to: isolated on white/transparent background, 3/4 view + +### UI Elements (UI) +- Include: function, interaction state, size relative to screen +- 风格参考: 复古游戏界面、像素风边框、手绘质感 +- Lowpoly specifics: geometric shapes, bold icons, clean readable layout +- Default to: flat/front view, clear silhouette + +## Iteration & Refinement + +- Always offer 2-3 **prompt variations** for each asset (different angles, moods, or styles) +- If the user provides feedback on a generated image, adjust the prompt based on what needs to change +- Keep a mental note of which prompt structures produce the best results + +## Important Rules + +1. **Never sacrifice clarity for creativity** — a prompt must be understandable first +2. **Stay era-appropriate** — no modern smartphones, contemporary brands, or anachronistic elements in千禧年 assets +3. **Maintain style consistency** — all prompts should feel like they belong in the same game world +4. **Be specific about low-poly** — include geometry and shading descriptors that guide toward the low-poly aesthetic +5. **Respect the warm, nostalgic tone** — avoid dark, gritty, or cold palettes unless explicitly requested +6. **Provide actionable output** — every prompt should be ready to copy-paste into GPT-Image-2 + +## Update your agent memory + +As you discover effective prompt patterns, style keywords that work well for this project's aesthetic, period-accurate visual references, and common user preferences. This builds up a visual knowledge base across conversations. Write concise notes about what you found. + +Examples of what to record: +- Effective prompt structures for Lowpoly style (which keywords produce best results) +- 千禧年 visual references and their English descriptions +- Color palette discoveries that evoke nostalgia +- Character design patterns that work well in low-poly +- Scene composition techniques for the suburban-rural aesthetic +- UI style keywords that match the game's tone + +# Persistent Agent Memory + +You have a persistent, file-based memory system at `E:\MiceCola\work\ChildHood\.claude\agent-memory\game-art-prompter\`. This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence). + +You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you. + +If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry. + +## Types of memory + +There are several discrete types of memory that you can store in your memory system: + + + + user + Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful to the user. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you're trying to accomplish together. + When you learn any details about the user's role, preferences, responsibilities, or knowledge + When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have. + + user: I'm a data scientist investigating what logging we have in place + assistant: [saves user memory: user is a data scientist, currently focused on observability/logging] + + user: I've been writing Go for ten years but this is my first time touching the React side of this repo + assistant: [saves user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues] + + + + feedback + Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious. + Any time the user corrects your approach ("no not that", "don't", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later. + Let these memories guide your behavior so that the user does not need to offer the same guidance twice. + Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule. + + user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed + assistant: [saves feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration] + + user: stop summarizing what you just did at the end of every response, I can read the diff + assistant: [saves feedback memory: this user wants terse responses with no trailing summaries] + + user: yeah the single bundled PR was the right call here, splitting this one would've just been churn + assistant: [saves feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction] + + + + project + Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work the user is doing within this working directory. + When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes. + Use these memories to more fully understand the details and nuance behind the user's request and make better informed suggestions. + Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing. + + user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch + assistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date] + + user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements + assistant: [saves project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics] + + + + reference + Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory. + When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel. + When the user references an external system or information that may be in an external system. + + user: check the Linear project "INGEST" if you want context on these tickets, that's where we track all pipeline bugs + assistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"] + + user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone + assistant: [saves reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code] + + + + +## What NOT to save in memory + +- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state. +- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative. +- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context. +- Anything already documented in CLAUDE.md files. +- Ephemeral task details: in-progress work, temporary state, current conversation context. + +These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping. + +## How to save memories + +Saving a memory is a two-step process: + +**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format: + +```markdown +--- +name: {{short-kebab-case-slug}} +description: {{one-line summary — used to decide relevance in future conversations, so be specific}} +metadata: + type: {{user, feedback, project, reference}} +--- + +{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines. Link related memories with [[their-name]].}} +``` + +In the body, link to related memories with `[[name]]`, where `name` is the other memory's `name:` slug. Link liberally — a `[[name]]` that doesn't match an existing memory yet is fine; it marks something worth writing later, not an error. + +**Step 2** — add a pointer to that file in `MEMORY.md`. `MEMORY.md` is an index, not a memory — each entry should be one line, under ~150 characters: `- [Title](file.md) — one-line hook`. It has no frontmatter. Never write memory content directly into `MEMORY.md`. + +- `MEMORY.md` is always loaded into your conversation context — lines after 200 will be truncated, so keep the index concise +- Keep the name, description, and type fields in memory files up-to-date with the content +- Organize memory semantically by topic, not chronologically +- Update or remove memories that turn out to be wrong or outdated +- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one. + +## When to access memories +- When memories seem relevant, or the user references prior-conversation work. +- You MUST access memory when the user explicitly asks you to check, recall, or remember. +- If the user says to *ignore* or *not use* memory: Do not apply remembered facts, cite, compare against, or mention memory content. +- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory rather than acting on it. + +## Before recommending from memory + +A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it: + +- If the memory names a file path: check the file exists. +- If the memory names a function or flag: grep for it. +- If the user is about to act on your recommendation (not just asking about history), verify first. + +"The memory says X exists" is not the same as "X exists now." + +A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot. + +## Memory and other forms of persistence +Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation. +- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory. +- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations. + +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you save new memories, they will appear here. diff --git a/Source/ChildHood/Interaction/DoorInteractableComponent.h b/Source/ChildHood/Interaction/DoorInteractableComponent.h new file mode 100644 index 0000000..56ff2f9 --- /dev/null +++ b/Source/ChildHood/Interaction/DoorInteractableComponent.h @@ -0,0 +1,64 @@ +// ChildHood Project - 门交互组件 +// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计) +// 职责:门的开关状态管理、动态提示文本、跨场景持久状态恢复 +// 派生自 UInteractableComponent,增加门特有的状态和行为 + +#pragma once + +#include "CoreMinimal.h" +#include "Interaction/InteractableComponent.h" +#include "DoorInteractableComponent.generated.h" + +class UPersistenceSubsystem; + +/** + * UDoorInteractableComponent + * + * 挂载在 ADoorActor 上的门交互组件。 + * 在基类(消耗状态、触发检测、Effect 配置)基础上,增加: + * - 门开关状态 (bDoorIsOpen) + * - 切换门 (ToggleDoor) + * - 动态提示文本(根据开关状态选择 InteractVerb 或 InteractVerbAlt) + * - 持久状态恢复(门的开关状态) + */ +UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent)) +class UDoorInteractableComponent : public UInteractableComponent +{ + GENERATED_BODY() + +public: + + UDoorInteractableComponent(); + + // === 门操作 === + + /** 切换门的开关状态,并通过 GetOwner() 通知 ADoorActor 更新视觉 */ + UFUNCTION(BlueprintCallable, Category = "Door") + void ToggleDoor(); + + /** 查询门当前是否打开 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Door") + bool IsDoorOpen() const { return bDoorIsOpen; } + + // === 基类覆盖 === + + /** + * 根据门开关状态选择提示动词: + * 已打开 → InteractVerbAlt(如"关上") + * 已关闭 → InteractVerb(如"打开") + */ + virtual FString GetPromptText_Implementation() const override; + + /** + * 恢复跨场景持久状态: + * 1. 调用 Super 恢复消耗状态 + * 2. 从 PersistenceSubsystem 恢复门开关状态 + * 3. 通知 DoorActor 更新视觉旋转 + */ + virtual void RestorePersistentState(UPersistenceSubsystem* Persistence) override; + +private: + + /** 门开关状态(true=打开,false=关闭) */ + bool bDoorIsOpen = false; +}; diff --git a/Source/ChildHood/Interaction/InteractableActor.cpp b/Source/ChildHood/Interaction/InteractableActor.cpp index 3773c99..dc337c0 100644 --- a/Source/ChildHood/Interaction/InteractableActor.cpp +++ b/Source/ChildHood/Interaction/InteractableActor.cpp @@ -1,4 +1,4 @@ -// ChildHood Project - 通用交互物 Actor 基类 实现 +// ChildHood Project - 通用交互物 Actor 基类 实现(v1.1 Effect/Action 架构) #include "Interaction/InteractableActor.h" #include "Interaction/InteractableComponent.h" @@ -28,17 +28,7 @@ AInteractableActor::AInteractableActor() void AInteractableActor::OnInteractStarted_Implementation(UInteractableComponent* Comp) { - if (!Comp || !MeshComp) return; - - const FInteractableData& Data = Comp->GetInteractableData(); - - // 门类型:根据开关状态旋转 Mesh - if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door) - { - FRotator CurrentRot = MeshComp->GetRelativeRotation(); - float TargetYaw = Comp->IsDoorOpen() ? 90.0f : 0.0f; - MeshComp->SetRelativeRotation(FRotator(CurrentRot.Pitch, TargetYaw, CurrentRot.Roll)); - } + // 默认空实现,子类可覆盖(如 ADoorActor 旋转 Mesh) } void AInteractableActor::OnInteractEnded_Implementation(UInteractableComponent* Comp) diff --git a/Source/ChildHood/Interaction/InteractableActor.h b/Source/ChildHood/Interaction/InteractableActor.h index 5f6a284..42f80c7 100644 --- a/Source/ChildHood/Interaction/InteractableActor.h +++ b/Source/ChildHood/Interaction/InteractableActor.h @@ -1,6 +1,7 @@ -// ChildHood Project - 通用交互物 Actor 基类 +// ChildHood Project - 通用交互物 Actor 基类(v1.1 Effect/Action 架构) // 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计) // 职责:持有 Mesh + InteractableComponent,提供可被子类覆盖的交互回调 +// 核心变更(v0.1 → v1.1):去除门旋转逻辑,只保留干净的基类 #pragma once @@ -9,6 +10,7 @@ #include "InteractableActor.generated.h" class UInteractableComponent; +class UStaticMeshComponent; /** * AInteractableActor @@ -21,7 +23,7 @@ class UInteractableComponent; * - UInteractableComponent* InteractableComp(交互逻辑与数据) * * 子类可覆盖 OnInteractStarted / OnInteractEnded 以实现类型特定行为, - * 如门开关时的 Mesh 旋转、拾取物的消失效果等。 + * 如门开关时的 Mesh 旋转(ADoorActor)、拾取物的消失效果等。 */ UCLASS() class AInteractableActor : public AActor @@ -45,15 +47,20 @@ public: // === 可被子类覆盖的交互回调 === /** - * 交互开始时调用(门开关切换、状态恢复时也调用)。 - * 默认实现:门类型根据开关状态对 Mesh 做 Yaw 旋转(开门+90度/关门0度)。 + * 交互开始时调用。 + * BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。 + * 默认为空实现。 */ - UFUNCTION(BlueprintNativeEvent, Category = "Interact") + UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Started")) void OnInteractStarted(UInteractableComponent* Comp); virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp); - /** 交互结束时调用。默认为空实现,子类可覆盖。 */ - UFUNCTION(BlueprintNativeEvent, Category = "Interact") + /** + * 交互结束时调用。 + * BlueprintNativeEvent,子类可在蓝图中或 C++ 中覆盖实现。 + * 默认为空实现。 + */ + UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "On Interact Ended")) void OnInteractEnded(UInteractableComponent* Comp); virtual void OnInteractEnded_Implementation(UInteractableComponent* Comp); @@ -61,9 +68,9 @@ protected: /** 交互物的可视模型 */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) - UStaticMeshComponent* MeshComp; + TObjectPtr MeshComp; /** 交互物的交互组件(数据 + 检测 + 状态) */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true")) - UInteractableComponent* InteractableComp; + TObjectPtr InteractableComp; }; diff --git a/Source/ChildHood/Interaction/InteractableComponent.cpp b/Source/ChildHood/Interaction/InteractableComponent.cpp index 564c3cd..7c7dc1b 100644 --- a/Source/ChildHood/Interaction/InteractableComponent.cpp +++ b/Source/ChildHood/Interaction/InteractableComponent.cpp @@ -1,15 +1,18 @@ -// ChildHood Project - 可交互物组件 实现 +// ChildHood Project - 可交互物组件 实现(v1.1 Effect/Action 架构) #include "Interaction/InteractableComponent.h" #include "Interaction/InteractableActor.h" -#include "Interaction/InteractionManager.h" -#include "ChildHoodGameInstance.h" +#include "Interaction/PersistenceSubsystem.h" #include "ChildHood.h" #include "Components/SphereComponent.h" #include "Engine/World.h" +#include "GameFramework/GameInstance.h" #include "GameFramework/Character.h" -#include "Kismet/GameplayStatics.h" + +// 前向声明对应的头文件在.cpp中include +// ASystemDriver 在 Phase 2 创建,此处前向声明即可 +class ASystemDriver; // ============================================================================ // 构造函数 @@ -36,7 +39,7 @@ void UInteractableComponent::BeginPlay() return; } - // --- 创建检测球 --- + // --- 创建检测球(BeginPlay 中动态创建,因为需要 Data.TriggerRadius)--- DetectionSphere = NewObject(this, USphereComponent::StaticClass(), TEXT("DetectionSphere")); if (DetectionSphere) { @@ -58,14 +61,14 @@ void UInteractableComponent::BeginPlay() } // --- 恢复跨场景持久状态 --- - if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance()) + if (UPersistenceSubsystem* Persistence = GetGameInstance()->GetSubsystem()) { - RestorePersistentState(GI); + RestorePersistentState(Persistence); } - UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (Type=%s, Radius=%f)"), + UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (ItemId=%s, Radius=%.0f)"), Data.DisplayName.IsEmpty() ? TEXT("(unnamed)") : *Data.DisplayName, - *UEnum::GetValueAsString(Data.Type), + *Data.ItemId.ToString(), Data.TriggerRadius); } @@ -73,22 +76,16 @@ void UInteractableComponent::BeginPlay() // 提示文本 // ============================================================================ -FString UInteractableComponent::GetPromptText() const +FString UInteractableComponent::GetPromptText_Implementation() const { - FString Verb = Data.InteractVerb; - // 门类型:根据当前开关状态选择动词 - if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door) - { - Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb; - } - return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName); + return FString::Printf(TEXT("[E] %s %s"), *Data.InteractVerb, *Data.DisplayName); } // ============================================================================ // 消耗管理 // ============================================================================ -void UInteractableComponent::Consume() +void UInteractableComponent::Consume_Implementation() { bIsConsumed = true; @@ -101,27 +98,15 @@ void UInteractableComponent::Consume() // 写入持久层 if (UWorld* World = GetWorld()) { - if (UChildHoodGameInstance* GI = World->GetGameInstance()) + if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem()) { - GI->MarkItemCollected(Data.ItemId); + Persistence->MarkItemCollected(Data.ItemId); } } UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 已消耗: %s"), *Data.ItemId.ToString()); } -// ============================================================================ -// 门状态管理 -// ============================================================================ - -void UInteractableComponent::ToggleDoor() -{ - bDoorIsOpen = !bDoorIsOpen; - UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 门状态切换: %s → %s"), - *Data.ItemId.ToString(), - bDoorIsOpen ? TEXT("开") : TEXT("关")); -} - // ============================================================================ // 位置查询 // ============================================================================ @@ -136,38 +121,24 @@ FVector UInteractableComponent::GetWorldLocation() const // 跨场景持久状态恢复 // ============================================================================ -void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI) +void UInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence) { - if (!GI) return; + if (!Persistence) return; // oneShot 物品:已消耗 → 隐藏 + 禁用检测 - if (Data.bOneShot && GI->IsItemCollected(Data.ItemId)) + if (Data.bOneShot && Persistence->IsItemCollected(Data.ItemId)) { bIsConsumed = true; if (DetectionSphere) { DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision); } - if (AInteractableActor* Owner = Cast(GetOwner())) + if (AActor* Owner = GetOwner()) { Owner->SetActorHiddenInGame(true); } UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 已消耗物品隐藏: %s"), *Data.ItemId.ToString()); } - - // 门:恢复开关状态 - if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door) - { - bDoorIsOpen = GI->IsDoorOpen(Data.ItemId); - // 通知 Actor 应用视觉状态(Mesh 旋转) - if (AInteractableActor* Owner = Cast(GetOwner())) - { - Owner->OnInteractStarted(this); - } - UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 门状态恢复: %s → %s"), - *Data.ItemId.ToString(), - bDoorIsOpen ? TEXT("开") : TEXT("关")); - } } // ============================================================================ @@ -193,11 +164,9 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"), *Data.DisplayName, *OtherActor->GetName()); - // 获取角色的 InteractionManager 并注册自己 - if (UInteractionManager* IM = Character->FindComponentByClass()) - { - IM->RegisterInteractable(this); - } + // Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().RegisterInteractable(this) + // 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建 + // TODO(Phase 2): 替换为 ASystemDriver 路径 } void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent, @@ -213,9 +182,7 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"), *Data.DisplayName, *OtherActor->GetName()); - // 获取角色的 InteractionManager 并注销自己 - if (UInteractionManager* IM = Character->FindComponentByClass()) - { - IM->UnregisterInteractable(this); - } + // Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this) + // 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建 + // TODO(Phase 2): 替换为 ASystemDriver 路径 } diff --git a/Source/ChildHood/Interaction/InteractableComponent.h b/Source/ChildHood/Interaction/InteractableComponent.h index 25142b7..0fa1b49 100644 --- a/Source/ChildHood/Interaction/InteractableComponent.h +++ b/Source/ChildHood/Interaction/InteractableComponent.h @@ -1,6 +1,7 @@ -// ChildHood Project - 可交互物组件 +// ChildHood Project - 可交互物组件(v1.1 Effect/Action 架构) // 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第12节(跨场景持久化) // 职责:单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复 +// 核心变更(v0.1 → v1.1):去除所有门逻辑,只保留通用交互物功能 #pragma once @@ -9,15 +10,21 @@ #include "InteractionTypes.h" #include "InteractableComponent.generated.h" -class UChildHoodGameInstance; +class UPersistenceSubsystem; class USphereComponent; +class ASystemDriver; /** * UInteractableComponent * * 挂载在 AInteractableActor(或其子类)上的组件。 * 持有该交互物的全部配置数据(FInteractableData)、运行时触发检测球、 - * 消耗/门状态管理,以及跨场景持久状态恢复。 + * 消耗状态管理,以及跨场景持久状态恢复。 + * + * 碰撞回调通过 ASystemDriver::Get() 获取 Driver, + * 再调用 GetInteractionManager().RegisterInteractable(this) 注册。 + * + * 门逻辑已移除,由 UDoorInteractableComponent 派生类实现(Phase 2)。 */ UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent)) class UInteractableComponent : public UActorComponent @@ -41,29 +48,31 @@ public: /** * 获取提示文本,格式:"[E] 动作词 名称" - * 门类型根据当前开关状态选择动词 + * 子类可覆盖以实现动态动词(如门的"打开"/"关上") */ + UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Get Prompt Text")) FString GetPromptText() const; + virtual FString GetPromptText_Implementation() const; /** 该交互物是否已被消耗(oneShot 模式) */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact") bool IsConsumed() const { return bIsConsumed; } - /** 标记为已消耗,禁用检测球,写入持久层 */ + /** 标记为已消耗,禁用检测球 */ + UFUNCTION(BlueprintNativeEvent, Category = "Interact", meta = (DisplayName = "Consume")) void Consume(); - - // === 门状态管理 === - - /** 切换门开关状态 */ - void ToggleDoor(); - - /** 门是否处于打开状态 */ - bool IsDoorOpen() const { return bDoorIsOpen; } + virtual void Consume_Implementation(); /** 获取交互物在世界空间的位置 */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interact") FVector GetWorldLocation() const; - /** 从 GameInstance 恢复跨场景持久状态(BeginPlay 中调用) */ - void RestorePersistentState(UChildHoodGameInstance* GI); + /** + * 从持久层恢复跨场景持久状态(BeginPlay 中调用)。 + * oneShot 物品已消耗时隐藏 Actor 并禁用检测。 + * 子类可覆盖以恢复额外状态(如门的开关状态)。 + */ + virtual void RestorePersistentState(UPersistenceSubsystem* Persistence); protected: @@ -73,17 +82,14 @@ private: /** 运行时创建的检测球,用于触发玩家进入/离开交互范围 */ UPROPERTY() - USphereComponent* DetectionSphere; + TObjectPtr DetectionSphere; /** oneShot 物品是否已被消耗 */ bool bIsConsumed = false; - /** 门是否处于打开状态(仅 Operate+Door 类型使用) */ - bool bDoorIsOpen = false; - // === 碰撞回调 === - /** 玩家进入检测范围 */ + /** 玩家进入检测范围 — 通过 ASystemDriver 注册到 InteractionManager */ UFUNCTION() void OnDetectionOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, @@ -92,7 +98,7 @@ private: bool bFromSweep, const FHitResult& SweepResult); - /** 玩家离开检测范围 */ + /** 玩家离开检测范围 — 通过 ASystemDriver 从 InteractionManager 注销 */ UFUNCTION() void OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, diff --git a/Source/ChildHood/Interaction/InteractionTypes.h b/Source/ChildHood/Interaction/InteractionTypes.h index 4c8bc98..1d3f8b5 100644 --- a/Source/ChildHood/Interaction/InteractionTypes.h +++ b/Source/ChildHood/Interaction/InteractionTypes.h @@ -1,5 +1,5 @@ -// ChildHood Project - 交互系统数据类型定义 -// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)+ 第12节(跨场景持久化) +// ChildHood Project - 交互系统数据类型定义(v1.1 Effect/Action 架构) +// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构) #pragma once @@ -10,169 +10,292 @@ // 枚举 // ============================================================================ -/** 交互类型 */ -UENUM(BlueprintType) -enum class EInteractType : uint8 -{ - Examine UMETA(DisplayName = "观察"), - Pickup UMETA(DisplayName = "拾取"), - Operate UMETA(DisplayName = "操作"), - Talk UMETA(DisplayName = "对话") -}; - -/** 操作子类型(仅 Operate 类型有效) */ -UENUM(BlueprintType) -enum class EOperateSubType : uint8 -{ - Door UMETA(DisplayName = "门(开关切换)"), - Search UMETA(DisplayName = "翻找") -}; - -/** 交互系统顶层状态 */ +/** 交互系统顶层状态(简化为 2 阶段) */ UENUM(BlueprintType) enum class EInteractionState : uint8 { - Free UMETA(DisplayName = "自由状态"), - Interacting UMETA(DisplayName = "交互中"), - SubSystem UMETA(DisplayName = "子系统(v0.1预留)"), - Cutscene UMETA(DisplayName = "过场(v0.1预留)") + Free, // 自由状态,检测/选择/Prompt + Executing, // 执行 Effect 中 }; -/** Interacting 子阶段 */ +/** Action 类型 */ UENUM(BlueprintType) -enum class EInteractPhase : uint8 +enum class EInteractActionType : uint8 { - None UMETA(DisplayName = "无"), - Enter UMETA(DisplayName = "进入(转向+推镜)"), - AnimLock UMETA(DisplayName = "动画锁定"), - Active UMETA(DisplayName = "活跃(等待输入)"), - Exit UMETA(DisplayName = "退出(回位)") -}; - -/** 交互结束后动作 */ -UENUM(BlueprintType) -enum class EPostAction : uint8 -{ - Return UMETA(DisplayName = "回到Free"), - Minigame UMETA(DisplayName = "小游戏(v0.1预留)"), - Dialogue UMETA(DisplayName = "对话(v0.1预留)"), - SceneChange UMETA(DisplayName = "场景切换(v0.1预留)") + Wait UMETA(DisplayName = "等待"), + TurnToTarget UMETA(DisplayName = "转向交互物"), + PlayMontage UMETA(DisplayName = "播放人物动画"), + PlayMontageOnTarget UMETA(DisplayName = "播放交互物动画"), + CameraPush UMETA(DisplayName = "相机推镜"), + CameraReturn UMETA(DisplayName = "相机回位"), + Consume UMETA(DisplayName = "消耗交互物"), + HideActor UMETA(DisplayName = "隐藏交互物"), + ShowActor UMETA(DisplayName = "显示交互物"), + AddToBackpack UMETA(DisplayName = "物品进背包"), + ShowNarrative UMETA(DisplayName = "显示叙事文本"), + HideNarrative UMETA(DisplayName = "隐藏叙事文本"), + ShowPickupNotification UMETA(DisplayName = "显示拾取通知"), + ToggleDoor UMETA(DisplayName = "切换门状态"), + PlayDialogue UMETA(DisplayName = "播放对话"), + PlayStory UMETA(DisplayName = "播放小剧情"), + BroadcastEvent UMETA(DisplayName = "广播逻辑事件"), }; // ============================================================================ -// 配置数据结构 +// 前向声明 // ============================================================================ -/** 可交互物配置数据 — 每个交互物实例独立配置 */ +class UInteractableComponent; + +// ============================================================================ +// Action — 原子操作定义 +// ============================================================================ + +/** 原子操作定义 — Effect 内按 TriggerTime 编排的最小执行单元 */ +USTRUCT(BlueprintType) +struct FInteractAction +{ + GENERATED_BODY() + + // === 编排 === + + /** Action 类型 */ + UPROPERTY(EditAnywhere, Category = "Action") + EInteractActionType Type = EInteractActionType::Wait; + + /** 相对于 Effect 开始的触发时间(秒) */ + UPROPERTY(EditAnywhere, Category = "Action", meta = (ClampMin = "0")) + float TriggerTime = 0.f; + + // === 各类型参数(按 EditCondition 折叠显示) === + + // --- Wait --- + /** 等待时长(秒) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::Wait", EditConditionHides)) + float WaitDuration = 0.5f; + + // --- TurnToTarget --- + /** 转向时长(秒) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::TurnToTarget", EditConditionHides)) + float TurnDuration = 0.2f; + + // --- PlayMontage --- + /** 人物蒙太奇动画 */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontage", EditConditionHides)) + TObjectPtr Montage; + + // --- PlayMontageOnTarget --- + /** 交互物蒙太奇动画 */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontageOnTarget", EditConditionHides)) + TObjectPtr TargetMontage; + + // --- CameraPush --- + /** 推镜距离(cm) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides)) + float PushDistance = 80.f; + + /** 推镜过渡时长(秒) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraPush", EditConditionHides)) + float PushDuration = 0.5f; + + // --- CameraReturn --- + /** 相机回位时长(秒) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::CameraReturn", EditConditionHides)) + float ReturnDuration = 0.3f; + + // --- ShowNarrative --- + /** 叙事文本列表 */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides)) + TArray NarrativeTexts; + + /** 是否等待用户按 E/ESC 后再继续后续 Action */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides)) + bool bWaitForInput = false; + + /** 是否从 NarrativeTexts 中随机选取(不重复上一次) */ + UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::ShowNarrative", EditConditionHides)) + bool bRandomSelect = true; +}; + +// ============================================================================ +// Effect — 交互效果定义 +// ============================================================================ + +/** Effect 定义 — 由多个 Action 按时间线编排而成的交互效果配方 */ +USTRUCT(BlueprintType) +struct FInteractEffect +{ + GENERATED_BODY() + + /** Effect 名称(调试用) */ + UPROPERTY(EditAnywhere, Category = "Effect") + FString EffectName; + + /** Effect 总时长(秒),到时自动结束 */ + UPROPERTY(EditAnywhere, Category = "Effect", meta = (ClampMin = "0.1", ClampMax = "10.0")) + float TotalDuration = 2.0f; + + /** 是否可取消(ESC 中断) */ + UPROPERTY(EditAnywhere, Category = "Effect") + bool bCancellable = true; + + /** Action 列表 — 按 TriggerTime 编排,同时间并行 */ + UPROPERTY(EditAnywhere, Category = "Effect") + TArray Actions; +}; + +// ============================================================================ +// 可交互物配置数据 +// ============================================================================ + +/** 可交互物配置数据 — 每个交互物实例独立配置,不再包含类型分发字段 */ USTRUCT(BlueprintType) struct FInteractableData { - GENERATED_BODY() + GENERATED_BODY() - // === 基本信息 === + // === 基本信息 === - /** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic") - FName ItemId; + /** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */ + UPROPERTY(EditAnywhere, Category = "Basic") + FName ItemId; - /** 显示名称(如 "旧冰箱"),用于提示UI */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic") - FString DisplayName; + /** 显示名称(如 "旧冰箱"),用于提示UI */ + UPROPERTY(EditAnywhere, Category = "Basic") + FString DisplayName; - /** 交互类型 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic") - EInteractType Type = EInteractType::Examine; + // === 触发 === - /** 操作子类型,仅 Type==Operate 时可见可编辑 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic", - meta = (EditCondition = "Type == EInteractType::Operate", EditConditionHides)) - EOperateSubType OperateSubType = EOperateSubType::Door; + /** 触发半径(cm),默认1.5m */ + UPROPERTY(EditAnywhere, Category = "Trigger", meta = (ClampMin = "50.0", ClampMax = "500.0")) + float TriggerRadius = 150.f; - // === 触发 === + /** 是否一次性消耗(消耗后不再可交互) */ + UPROPERTY(EditAnywhere, Category = "Trigger") + bool bOneShot = false; - /** 触发半径(cm),默认1.5m */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger", - meta = (ClampMin = "50.0", ClampMax = "500.0")) - float TriggerRadius = 150.0f; + // === 提示文本 === - /** 是否一次性消耗(消耗后不再可交互) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger") - bool bOneShot = false; + /** 动作词(如 "查看"、"拾取"、"打开") */ + UPROPERTY(EditAnywhere, Category = "Prompt") + FString InteractVerb; - // === 提示文本 === + /** 备选动词(如 "关上"),门打开时使用 */ + UPROPERTY(EditAnywhere, Category = "Prompt") + FString InteractVerbAlt; - /** 动作词(如 "查看"、"拾取"、"打开") */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt") - FString InteractVerb; + // === 交互 Effect === - /** 备选动词(如 "关上"),门打开时使用,仅 Operate+Door 时可见 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt", - meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door")) - FString InteractVerbAlt; + /** 交互效果定义 — 所有行为由 Effect 内的 Action 定义 */ + UPROPERTY(EditAnywhere, Category = "Effect") + FInteractEffect InteractEffect; - // === 叙事/对话文本 === + // 已删除(由 Effect 内 Action 替代): + // EInteractType Type, EOperateSubType OperateSubType + // bUseCameraPush, CameraPushDistance/Duration, PushWaitDelay, CameraReturnDuration + // InteractMontage, AnimDuration, AnimEventTimepoint + // NarrativeTexts, EPostAction PostAction +}; - /** 文本列表,随机选取,不重复上次 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text") - TArray NarrativeTexts; +// ============================================================================ +// 运行时上下文 +// ============================================================================ - // === 摄像机推镜 === +/** Effect 运行时上下文 — EffectManager 执行时传入,持有弱引用 */ +USTRUCT() +struct FEffectContext +{ + GENERATED_BODY() - /** 是否使用摄像机推镜 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera") - bool bUseCameraPush = false; + /** 触发交互的角色 */ + TWeakObjectPtr Instigator; - /** 推镜距离(cm),仅 bUseCameraPush 时可编辑 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera", - meta = (EditCondition = "bUseCameraPush", ClampMin = "10.0", ClampMax = "300.0")) - float CameraPushDistance = 80.0f; + /** 交互物组件(配置数据来源) */ + TWeakObjectPtr TargetComp; - /** 推镜过渡时长(秒),仅 bUseCameraPush 时可编辑 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera", - meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0")) - float CameraPushDuration = 0.5f; + /** 交互物 Actor(视觉操作目标) */ + TWeakObjectPtr TargetActor; +}; - /** 相机回位时长(秒),仅 bUseCameraPush 时可编辑 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera", - meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0")) - float CameraReturnDuration = 0.3f; +// ============================================================================ +// Presentation 层缓存数据(非 UObject,纯 C++) +// ============================================================================ - /** 推镜完成后到文本显示的等待时间(秒),仅 bUseCameraPush 时可编辑 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera", - meta = (EditCondition = "bUseCameraPush", ClampMin = "0.0", ClampMax = "1.0")) - float PushWaitDelay = 0.2f; +/** Presentation 层缓存数据 — Manager 在选择变化时从 Component 读一次并写入, + * Prompt 组件每帧只读此缓存,不直接访问 Component */ +struct FSelectedInfo +{ + /** 是否有有效选中 */ + bool bHasSelection = false; - // === 动画 === + /** 选中交互物的提示文本(如 "[E] 查看旧冰箱") */ + FString PromptText; - /** 交互动画蒙太奇(可选,为空时用计时器模拟) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim") - TObjectPtr InteractMontage; + /** 选中交互物的世界坐标(用于屏幕空间投影) */ + FVector WorldPosition = FVector::ZeroVector; - /** 无蒙太奇时的动画持续时间(秒) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim", - meta = (ClampMin = "0.1", ClampMax = "5.0")) - float AnimDuration = 1.0f; + /** 选中交互物是否已消耗(消耗后不再显示提示) */ + bool bIsConsumed = false; - /** 动画事件触发点(0-1比例),如0.6表示动画播放至60%时触发物品消失 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim", - meta = (ClampMin = "0.0", ClampMax = "1.0")) - float AnimEventTimepoint = 0.6f; + void Reset() + { + bHasSelection = false; + PromptText.Empty(); + WorldPosition = FVector::ZeroVector; + bIsConsumed = false; + } +}; - // === 结束动作 === +// ============================================================================ +// TurnToTarget 跨帧插值状态(非 UObject,纯 C++) +// ============================================================================ - /** 交互结束后的动作 */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Post") - EPostAction PostAction = EPostAction::Return; +/** TurnToTarget 跨帧插值状态 — EffectManager 在 Update 中维护, + * 每帧 RInterpTo 驱动旋转,不能一帧完成 */ +struct FTurnState +{ + /** 是否正在转向 */ + bool bIsActive = false; + + /** 转向起始旋转 */ + FRotator StartRotation = FRotator::ZeroRotator; + + /** 转向目标旋转(面朝交互物方向) */ + FRotator TargetRotation = FRotator::ZeroRotator; + + /** 已用时间(秒) */ + float ElapsedTime = 0.f; + + /** 转向总时长(秒) */ + float Duration = 0.2f; + + /** 是否已完成 */ + bool IsComplete() const { return ElapsedTime >= Duration; } + + /** 获取当前插值 Alpha [0,1] */ + float GetAlpha() const { return FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f); } + + void Reset() + { + bIsActive = false; + StartRotation = FRotator::ZeroRotator; + TargetRotation = FRotator::ZeroRotator; + ElapsedTime = 0.f; + Duration = 0.2f; + } }; // ============================================================================ // 委托 // ============================================================================ -/** 物品拾取/获得事件 — 通知物品系统和收集系统 */ +/** 物品消耗事件(逻辑事件,Persistence/Backpack 订阅) */ DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( - FMLocalItemEvent, - FName, ItemId, - const FString&, DisplayName, - const FName&, Category + FMLocalItemEvent, + FName, ItemId, + const FString&, DisplayName, + const FName&, Category ); + +/** Effect 完成回调 */ +DECLARE_DELEGATE(FOnEffectComplete); diff --git a/Source/ChildHood/Interaction/PersistenceSubsystem.cpp b/Source/ChildHood/Interaction/PersistenceSubsystem.cpp new file mode 100644 index 0000000..9c433bb --- /dev/null +++ b/Source/ChildHood/Interaction/PersistenceSubsystem.cpp @@ -0,0 +1,47 @@ +// ChildHood Project - 跨场景持久化子系统 实现 + +#include "Interaction/PersistenceSubsystem.h" +#include "ChildHood.h" + +// ============================================================================ +// 物品消耗状态 +// ============================================================================ + +bool UPersistenceSubsystem::IsItemCollected(FName ItemId) const +{ + return CollectedItemIDs.Contains(ItemId); +} + +void UPersistenceSubsystem::MarkItemCollected(FName ItemId) +{ + CollectedItemIDs.Add(ItemId); + UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 物品已收集: %s"), *ItemId.ToString()); +} + +// ============================================================================ +// 门状态 +// ============================================================================ + +bool UPersistenceSubsystem::IsDoorOpen(FName DoorId) const +{ + const bool* Found = DoorStates.Find(DoorId); + return Found ? *Found : false; +} + +void UPersistenceSubsystem::SetDoorOpen(FName DoorId, bool bIsOpen) +{ + DoorStates.Add(DoorId, bIsOpen); + UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 门状态更新: %s → %s"), + *DoorId.ToString(), bIsOpen ? TEXT("开") : TEXT("关")); +} + +// ============================================================================ +// 重置 +// ============================================================================ + +void UPersistenceSubsystem::ResetAll() +{ + CollectedItemIDs.Empty(); + DoorStates.Empty(); + UE_LOG(LogChildHood, Log, TEXT("PersistenceSubsystem: 所有持久数据已重置")); +} diff --git a/Source/ChildHood/Interaction/PersistenceSubsystem.h b/Source/ChildHood/Interaction/PersistenceSubsystem.h new file mode 100644 index 0000000..c195687 --- /dev/null +++ b/Source/ChildHood/Interaction/PersistenceSubsystem.h @@ -0,0 +1,63 @@ +// ChildHood Project - 跨场景持久化子系统 +// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计) + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "PersistenceSubsystem.generated.h" + +/** + * UPersistenceSubsystem + * + * UGameInstanceSubsystem,跨场景持久数据持有者。 + * UE5 的 OpenLevel() 会销毁整个旧 World(包括所有 Actor 和 Component), + * 因此将持久数据放在 GameInstanceSubsystem 中,与 World 生命周期解耦。 + * + * 当前持有: + * - CollectedItemIDs: 已收集物品 ID 集合 + * - DoorStates: 门开关状态映射 + */ +UCLASS() +class UPersistenceSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + + // === 物品消耗状态 === + + /** 查询指定物品是否已被收集(跨场景持久) */ + UFUNCTION(BlueprintCallable, Category = "Persistence") + bool IsItemCollected(FName ItemId) const; + + /** 标记指定物品为已收集(写入持久层) */ + UFUNCTION(BlueprintCallable, Category = "Persistence") + void MarkItemCollected(FName ItemId); + + // === 门状态 === + + /** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */ + UFUNCTION(BlueprintCallable, Category = "Persistence") + bool IsDoorOpen(FName DoorId) const; + + /** 设置指定门的开关状态(写入持久层) */ + UFUNCTION(BlueprintCallable, Category = "Persistence") + void SetDoorOpen(FName DoorId, bool bIsOpen); + + // === 重置 === + + /** 重置所有持久数据(新游戏时调用) */ + UFUNCTION(BlueprintCallable, Category = "Persistence") + void ResetAll(); + +private: + + /** 已收集物品 ID 集合 */ + UPROPERTY() + TSet CollectedItemIDs; + + /** 门开关状态映射(true=打开,false=关闭) */ + UPROPERTY() + TMap DoorStates; +}; diff --git a/docs/technical/收集系统-技术方案.md b/docs/technical/收集系统-技术方案.md index addf516..6d7d532 100644 --- a/docs/technical/收集系统-技术方案.md +++ b/docs/technical/收集系统-技术方案.md @@ -1,45 +1,110 @@ -# 收集系统 技术设计文档(v0.1) +# 收集系统 技术设计文档(v1.0) -> 任务ID: P-009 +> 任务ID: P-015 > 对应执行案: `docs/design/收集系统-执行案.md` (v0.1.1) -> 关联系统: 物品系统(上游单向接收)、交互系统(间接,通过物品系统委托)、GameInstance 持久化层 -> 日期: 2026-06-07(2026-06-11 更新:加入多场景持久化架构) +> 关联系统: 交互系统(上游事件源,通过 ASystemDriver 回调)、UPersistenceSubsystem(持久化层) +> 日期: 2026-06-11 + +--- + +## 迭代记录 + +| 版本 | 日期 | 变更内容 | +|------|------|----------| +| v0.1 | 2026-06-07 | 初版技术方案:UCollectionManager (UActorComponent) 混合架构,逻辑+表现+持久化一体 | +| v1.0 | 2026-06-11 | 架构解耦修订:参考交互系统 v1.1 Effect/Action 模式,三层分离(FCollectionManager 普通 C++ / UPersistenceSubsystem 持久层 / UCollectionHUDComponent 表现层) | --- ## 概述 -收集系统追踪玩家的收集进度,在达成收集里程碑时给予反馈。v0.1 原型实现单一品类「杂物」的 10 个物品追踪:接收物品系统的获得通知 → 判定新收集 → 更新进度 → 弹出收集通知。包含收集状态管理、收集通知 UI、收集进度 UI 和全部完成表现四个子模块。 +收集系统追踪玩家的收集进度,在达成收集里程碑时给予反馈。v0.1 原型实现单一品类「杂物」的 10 个物品追踪:接收交互系统的获得通知 → 判定新收集 → 更新进度 → 弹出收集通知。 + +v1.0 核心架构变更——参考交互系统 v1.1 的数据层/表现层分离模式: + +| 维度 | v0.1 | v1.0 | +|------|------|------| +| Manager 类型 | `UCollectionManager` (UActorComponent) | `FCollectionManager`(普通 C++) | +| Manager 位置 | Character 上 | `ASystemDriver` 持有 | +| 持久化 | `UChildHoodGameInstance` 直接读写 | `UPersistenceSubsystem` (GameInstanceSubsystem) | +| UI 控制 | Manager 直接创建/管理 Widget | `UCollectionHUDComponent`(独立表现层) | +| 事件源 | 绑定 `UInteractionManager::OnItemPickedUp` 委托 | 通过 `ASystemDriver` 回调链,`FEffectManager` AddToBackpack Action 触发 | +| Tick 依赖 | Manager 无 Tick,用定时器 | Manager 无 Tick,纯事件驱动 | +| 数据流 | Manager 混合逻辑+表现 | 逻辑→缓存→表现(只读缓存) | **实现复杂度**:低-中 **预估工期**:2-3 天 -**新增/修改代码量**:约 250-350 行 C++ + 3 个 UMG 蓝图 +**新增/修改代码量**:约 300-400 行 C++ + 3 个 UMG 蓝图 -### 跨场景持久化架构 +--- -> 收集进度是玩家核心持久数据。当前设计中 `UCollectionManager` 将「行为控制」(UI、通知、动画)和「持久数据」(CollectedIDs、Progress)混在同一组件,随 Character 销毁而丢失。需要通过数据层分离解决: +## 架构分层 -- **持久数据层**:收集状态(CollectedIDs、Progress、bIsComplete)存储在 `UChildHoodGameInstance` 中(跨关卡存活) -- **行为层**:`UCollectionManager` 仅负责运行时 UI 控制和委托监听,BeginPlay 时从 GameInstance 恢复状态 -- **交互物恢复**:场景中的已收集物品由交互系统通过 `InteractableComponent::RestorePersistentState()` 处理(见交互系统技术方案第 12 节) +``` +┌──────────────────────────────────────────────────────────────────┐ +│ GameInstance 层(UPersistenceSubsystem,跨 World 持久) │ +│ │ +│ UPersistenceSubsystem │ +│ ├─ CollectedItemIDs / DoorStates(交互系统用) │ +│ └─ CollectionCollectedIDs / CollectionProgress / bCollectionComplete │ +│ │ +├──────────────────────────────────────────────────────────────────┤ +│ World 层(per-world,随 World 生灭) │ +│ │ +│ ASystemDriver (AInfo, 全局唯一) │ +│ ├─ FInteractionManager(交互逻辑,纯事件驱动) │ +│ ├─ FEffectManager(Effect 执行,Driver 提供 Tick) │ +│ └─ FCollectionManager(收集逻辑,纯事件驱动,无 Tick) │ +│ ├─ 品类检查 → 去重 → 记录 → 完成判定 │ +│ ├─ 缓存 Presentation 数据 (FCollectionCachedInfo) │ +│ └─ 通过 Driver 中回调连线接收物品事件 │ +│ │ +│ 协调:Driver::BeginPlay 中回调连线 │ +│ EffectManager AddToBackpack Action → Driver → CollectionManager │ +│ │ +├──────────────────────────────────────────────────────────────────┤ +│ Presentation 层(Character 上) │ +│ │ +│ UCollectionHUDComponent │ +│ ├─ 自有 TickComponent,只读 Manager 缓存 → 驱动 UI │ +│ ├─ 管理 WBP_CollectionNotification / Progress / Complete │ +│ └─ 不碰 Component、Subsystem,通过 Driver 获取 Manager │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` -v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前功能。详见 [第 12 节:跨场景持久化设计](#12-跨场景持久化设计)。 +### 数据流规则 + +``` +┌──────────────┐ 只读缓存 ┌──────────────┐ 物品事件 ┌──────────────┐ +│ Presentation │ ─────────→ │ Logic │ ←────────── │ Interaction │ +│ (HUDComponent│ │ (Collection │ │ (EffectMgr │ +│ 每帧 Poll) │ │ Manager) │ │ AddToBackpack) +└──────────────┘ └──────┬───────┘ └──────────────┘ + │ + │ 写入持久层 + ↓ + ┌──────────────┐ + │ Persistence │ + │ Subsystem │ + └──────────────┘ +``` + +- **表现层只读 Manager 缓存** — 不碰 Component,不碰 Subsystem +- **Manager 在物品事件时更新缓存并写入持久层** — Presentation 通过 Tick 检测变化 +- **逻辑事件写入 PersistenceSubsystem** — 跨场景持久 --- ## 1. 需求功能点罗列 -将执行文档中所有行为规则逐一列出,按实现方式分类。 - ### UE 内置功能 -以下功能点直接使用 UE5 引擎内置能力,无需或仅需极少量自定义代码: - | 功能点 | 使用的 UE 内置能力 | 对应行为规则 | 备注 | |--------|-------------------|-------------|------| | 收集通知淡入淡出动画 | UMG Widget Animation(FadeIn/FadeOut) | 收集通知 UI 显示动画 | UMG 内建动画时间轴 | -| 收集进度 UI 常驻显示 | UMG Widget AddToViewport | 收集进度 UI 常驻显示 | BeginPlay 时添加 | -| 通知延迟弹出 | FTimerHandle + SetTimer | 收集通知延迟 0.3s | 委托回调后启动定时器 | +| 收集进度 UI 常驻显示 | UMG Widget AddToViewport | 收集进度 UI 常驻显示 | HUDComponent 初始化时添加 | +| 通知延迟弹出 | FTimerHandle + SetTimer | 收集通知延迟 0.3s | HUDComponent 内定时器 | | 通知自动移除 | FTimerHandle + SetTimer | 收集通知淡出后移除 | 停留+淡出时间后 RemoveFromParent | | 重复判定数据结构 | TSet\ | 步骤 2 查重复 | O(1) 查找 | | 进度 UI 屏幕定位 | UMG Anchors(Top Left) | 收集进度 UI 位置 | Anchors + Offset 配置 | @@ -47,17 +112,16 @@ v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前 ### 自定义实现 -以下功能点需要编写自定义 C++ / 蓝图代码: - | 功能点 | 实现方式概述 | 对应行为规则 | 预估复杂度 | |--------|-------------|-------------|-----------| -| 收集状态管理 | UActorComponent 维护 progress / collectedIDs / isComplete | 状态定义/状态转换 | 低 | -| 物品通知接收与处理 | 绑定 InteractionManager 委托,执行查品类→查重复→记录→通知→完成判定 | 收到获得物品通知后的流程(步骤 1-5) | 低-中 | -| 收集通知 UI 控制器 | 管理通知 Widget 创建、位置叠放、最大数量限制 | 收集通知 UI + 叠放规则 | 中 | +| 收集状态管理 | FCollectionManager(普通 C++)维护 progress / collectedIDs / isComplete | 状态定义/状态转换 | 低 | +| 物品通知接收与处理 | ASystemDriver 回调连线 → CollectionManager,执行查品类→查重复→记录→完成判定 | 收到获得物品通知后的流程(步骤 1-5) | 低-中 | +| 表现层缓存驱动 | UCollectionHUDComponent Tick 读取 Manager 缓存,检测变化驱动 UI | 所有 UI 更新 | 低 | +| 收集通知 UI 控制器 | HUDComponent 管理通知 Widget 创建、位置叠放、最大数量限制 | 收集通知 UI + 叠放规则 | 中 | | 进度 UI 数值动画 | UMG Widget Scale 动画(弹性缩放) | 进度数字更新动画 + 完成金色缩放 | 低 | | 全部完成表现 | 完成判定 → 进度变金色 → 延迟弹出中心文字 → 淡出 | 全部收集完成表现 | 低 | -> **分类原则**:收集系统核心逻辑为简单的状态管理 + UI 控制,大部分功能基于 UE 定时器和 UMG 动画。自定义部分集中在通知 UI 的时序控制和叠放管理。 +> **分类原则**:收集系统核心逻辑为简单的状态管理 + UI 控制。架构变更集中在类的拆分和层次分离,行为规则本身不变。 --- @@ -66,41 +130,41 @@ v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前 ```mermaid flowchart TD subgraph 上游系统 - ItemSys[物品系统
InteractionManager 委托] + EffectMgr["FEffectManager
AddToBackpack Action"] end - subgraph 收集核心 - NotifHandler[通知处理器
品类检查 + 去重 + 记录] - StateTracker[状态追踪器
progress / collectedIDs / isComplete] - CompleteHandler[完成处理器
完成判定 + 完成表现] + subgraph World层逻辑 + CollMgr["FCollectionManager
(普通C++, ASystemDriver持有)
品类检查 + 去重 + 记录 + 完成判定"] + Persist["UPersistenceSubsystem
(GameInstanceSubsystem)
CollectionCollectedIDs / Progress / Complete"] end - subgraph 表现层 - NotifUI[收集通知 UI
WBP_CollectionNotification] - ProgressUI[收集进度 UI
WBP_CollectionProgress] - CompleteUI[完成提示 UI
WBP_CollectionComplete] + subgraph Presentation层 + NotifUI["WBP_CollectionNotification
收集通知"] + ProgressUI["WBP_CollectionProgress
收集进度 HUD"] + CompleteUI["WBP_CollectionComplete
完成提示"] + HUDComp["UCollectionHUDComponent
(Character上, 只读缓存)"] end - ItemSys -.->|OnItemPickedUp / OnItemFound| NotifHandler - NotifHandler -->|新增收集| StateTracker - NotifHandler -->|延迟 0.3s 后显示| NotifUI - StateTracker -->|progress 达到 total| CompleteHandler - StateTracker -->|progress 更新| ProgressUI - CompleteHandler -->|进度变金色| ProgressUI - CompleteHandler -->|延迟后弹出| CompleteUI + EffectMgr -.->|"Driver回调链: ItemId, DisplayName, Category"| CollMgr + CollMgr -->|"写入收集状态"| Persist + CollMgr -->|"缓存更新"| HUDComp + HUDComp -->|"创建/移除"| NotifUI + HUDComp -->|"更新进度"| ProgressUI + HUDComp -->|"完成时显示"| CompleteUI + Persist -.->|"BeginPlay 恢复"| CollMgr ``` ### 模块职责说明 | 模块 | 职责 | 关键输入 | 关键输出 | |------|------|---------|---------| -| 物品系统(上游) | 广播物品获得事件 | InteractionManager 在拾取/翻找动画事件点触发 | OnItemPickedUp/OnItemFound 委托广播 | -| 通知处理器 | 接收物品通知,执行品类检查和去重判定 | 委托回调(ItemId, DisplayName, Category) | 新增收集信号 → 状态追踪器;显示通知信号 → 通知 UI | -| 状态追踪器 | 维护收集进度数据 | 新增收集信号 | progress/collectedIDs/isComplete 状态;UI 更新触发 | -| 完成处理器 | 判断并执行全部收集完成表现 | progress == total 信号 | 进度变金色 + 延迟弹出完成提示 | -| 收集通知 UI | 显示「发现了新的杂物,进度 X/Y」通知 | 创建/移除指令 | 屏幕右上角淡入淡出通知 | -| 收集进度 UI | 常驻显示收集进度文本 | progress 更新 | 屏幕左上角进度文本 + 弹性动画 | -| 完成提示 UI | 屏幕中央完成文字 | 显示指令 | 「杂物全部收集完成!」白色大字 | +| FEffectManager(上游) | 执行 AddToBackpack Action 时通过 Driver 广播物品事件 | Effect Action 配置 | 回调: (ItemId, DisplayName, Category) | +| FCollectionManager | 收集逻辑核心:品类检查、去重判定、记录持久化、完成判定、缓存 Presentation 数据 | Driver 回调的物品事件 | 缓存 FCollectionCachedInfo;写入 PersistenceSubsystem | +| UPersistenceSubsystem | 跨场景持久数据存储 | CollectionManager 写入 | CollectionManager 读取恢复 | +| UCollectionHUDComponent | 表现层:管理通知/进度/完成 UI Widget 生命周期 | 只读 CollectionManager 缓存 | UI Widget 创建/更新/销毁 | +| WBP_CollectionNotification | 收集通知 UI 单条实例 | HUDComponent 创建指令 | 淡入→停留→淡出 | +| WBP_CollectionProgress | 收集进度常驻 HUD | HUDComponent 更新指令 | 进度文本 + 弹性动画 | +| WBP_CollectionComplete | 全部收集完成中心提示 | HUDComponent 显示指令 | 淡入→停留→淡出 | --- @@ -108,52 +172,69 @@ flowchart TD ```mermaid classDiagram - class AChildHoodCharacter { - -UCollectionManager* CollectionManager - +GetCollectionManager() UCollectionManager* + class ASystemDriver { + -FInteractionManager InteractionManager + -FEffectManager EffectManager + -FCollectionManager CollectionManager + +GetCollectionManager() FCollectionManager& + +BeginPlay() void + +Tick(float DeltaTime) void + +static Get(UWorld*) ASystemDriver* + Note: AInfo全局唯一 + Note: BeginPlay中回调连线 + Note: EffectManager AddToBackpack → CollectionManager } - class UCollectionManager { + class FCollectionManager { -FCollectionConfig Config -int32 Progress -TSet~FName~ CollectedIDs -bool bIsComplete + -FCollectionCachedInfo CachedInfo + +Initialize() void + +OnItemObtained(FName, FString, FName) void + +GetCachedInfo() const FCollectionCachedInfo& + +RestoreFromPersistence(UPersistenceSubsystem*) void + -ProcessNewCollection(FName) void + -RefreshCachedInfo(FString DisplayName) void + Note: 普通C++类,非UObject + Note: 纯事件驱动,无Tick + } + + class UPersistenceSubsystem { + -TSet~FName~ CollectedItemIDs + -TMap~FName_bool~ DoorStates + -TSet~FName~ CollectionCollectedIDs + -int32 CollectionProgress + -bool bCollectionComplete + +IsItemCollected(FName) bool + +MarkItemCollected(FName) void + +GetCollectionState(...) void + +SetCollectionState(...) void + +ResetAll() void + } + + class UCollectionHUDComponent { + -TWeakObjectPtr~ASystemDriver~ Driver + -int32 LastSeenProgress + -bool bCompletionShown -TArray~UCollectionNotifWidget*~ ActiveNotifications -UCollectionProgressWidget* ProgressWidget -TSubclassOf~UCollectionNotifWidget~ NotifWidgetClass -TSubclassOf~UCollectionProgressWidget~ ProgressWidgetClass -TSubclassOf~UCollectionCompleteWidget~ CompleteWidgetClass - -FTimerHandle NotifDelayTimerHandle - -FTimerHandle CompleteTimerHandle - +BeginPlay() void - -OnItemObtained(FName, FString, FName) void - -ProcessNewCollection(FName) void - -ShowCollectionNotification() void - -OnNotificationRemoved(UCollectionNotifWidget*) void - -CheckCompletion() void - -ShowCompletionPrompt() void - -UpdateProgressUI() void - -LoadStateFromGameInstance() void - -SaveStateToGameInstance() void - } - - class UChildHoodGameInstance { - -TSet~FName~ CollectedItemIDs - -int32 CollectionProgress - -bool bCollectionComplete - +GetCollectionState(TSet_FName_&, int32&, bool&) void - +SetCollectionState(TSet_FName_&, int32, bool) void - +ResetAll() void + +Initialize(ASystemDriver*) void + #TickComponent(float) void + -ShowNotification(FString) void + -UpdateProgress(int32, int32) void + -TriggerCompletionSequence() void + Note: 只读Manager缓存,不碰Component/Subsystem } class UCollectionNotifWidget { -FString DisplayText -FLinearColor TextColor - -float FadeInDuration - -float StayDuration - -float FadeOutDuration -FTimerHandle StayTimerHandle - -FTimerHandle FadeOutTimerHandle +InitializeNotif(FString, FLinearColor, float, float, float) void -OnFadeOutComplete() void } @@ -170,11 +251,6 @@ classDiagram +ShowComplete() void } - class UInteractionManager { - +OnItemPickedUp FMLocalItemEvent - +OnItemFound FMLocalItemEvent - } - class FCollectionConfig { +FName TrackedCategory +int32 TotalCount @@ -184,48 +260,55 @@ classDiagram +float NotifFadeOutDuration +FLinearColor NotifTextColor +int32 MaxSimultaneousNotifs - +float ProgressMarginX - +float ProgressMarginY - +float BounceScale - +float BounceDuration - +float CompleteScale - +float CompleteScaleDuration +float CompletePromptDelay +float CompletePromptDuration } - AChildHoodCharacter *-- UCollectionManager : 组合 - UCollectionManager --> UInteractionManager : 关联(绑定委托) - UCollectionManager ..> UChildHoodGameInstance : 读取/写入持久状态 - UCollectionManager o-- UCollectionNotifWidget : 聚合(管理生命周期) - UCollectionManager o-- UCollectionProgressWidget : 聚合(管理生命周期) - UCollectionManager ..> UCollectionCompleteWidget : 按需创建 - UCollectionManager *-- FCollectionConfig : 组合(内含) + class FCollectionCachedInfo { + +int32 Progress + +int32 TotalCount + +bool bIsComplete + +bool bJustCompleted + +FString PendingNotificationText + +FName LastCollectedItemId + } + + ASystemDriver *-- FCollectionManager : 持有(值) + ASystemDriver *-- FInteractionManager : 持有(值) + ASystemDriver *-- FEffectManager : 持有(值) + FCollectionManager ..> UPersistenceSubsystem : 读写持久数据 + FCollectionManager *-- FCollectionConfig : 内含 + FCollectionManager *-- FCollectionCachedInfo : 内含 + AChildHoodCharacter *-- UCollectionHUDComponent : 组合 + UCollectionHUDComponent ..> ASystemDriver : 只读Manager缓存 + UCollectionHUDComponent o-- UCollectionNotifWidget : 聚合(管理生命周期) + UCollectionHUDComponent o-- UCollectionProgressWidget : 聚合(管理生命周期) + UCollectionHUDComponent ..> UCollectionCompleteWidget : 按需创建 ``` -### 关系类型说明 - -| 关系类型 | 含义 | 本项目中的体现 | -|---------|------|--------------| -| 组合 `*--` | 生命周期绑定 | Character 持有 CollectionManager;CollectionManager 内含 Config | -| 聚合 `o--` | 引用但非拥有 | CollectionManager 管理通知/进度 Widget 的创建和销毁 | -| 关联 `-->` | 持久引用 | CollectionManager 绑定 InteractionManager 的委托 | -| 依赖 `..>` | 临时使用 | CollectionManager 从 GameInstance 读取/写入持久状态;按需创建完成提示 Widget | - ### 新增类 | 类名 | 类型 | 职责 | 位置 | |------|------|------|------| -| `UCollectionManager` | UActorComponent | 收集系统核心:状态管理、委托监听、通知处理、UI 控制 | `Source/ChildHood/Collection/` | -| `UCollectionNotifWidget` | UUserWidget | 收集通知 UI 单条实例:淡入→停留→淡出生命周期 | `Source/ChildHood/Collection/` | +| `FCollectionManager` | 普通 C++ | 收集逻辑核心:品类检查、去重、记录、完成判定、缓存 Presentation 数据 | `Source/ChildHood/Collection/` | +| `UCollectionHUDComponent` | UActorComponent | 表现层:管理收集通知/进度/完成 UI Widget,只读 Manager 缓存 | `Source/ChildHood/Collection/` | +| `UCollectionNotifWidget` | UUserWidget | 收集通知 UI 单条实例:淡入→停留→淡出 | `Source/ChildHood/Collection/` | | `UCollectionProgressWidget` | UUserWidget | 收集进度常驻 UI:进度文本 + 弹性动画 + 完成变色 | `Source/ChildHood/Collection/` | -| `UCollectionCompleteWidget` | UUserWidget | 全部收集完成中心提示 UI:淡入→停留→淡出 | `Source/ChildHood/Collection/` | +| `UCollectionCompleteWidget` | UUserWidget | 全部收集完成中心提示:淡入→停留→淡出 | `Source/ChildHood/Collection/` | ### 修改类 | 类名 | 修改内容 | |------|---------| -| `AChildHoodCharacter` | 新增 `UCollectionManager*` 成员和 `GetCollectionManager()` 访问器;`BeginPlay()` 中确保收集系统初始化 | +| `ASystemDriver` | 新增 `FCollectionManager` 成员和访问器;`BeginPlay()` 中添加回调连线(EffectManager → CollectionManager);恢复时从 PersistenceSubsystem 读取 | +| `UPersistenceSubsystem` | 新增收集状态数据成员(CollectionCollectedIDs / CollectionProgress / bCollectionComplete)和读写接口 | +| `AChildHoodCharacter` | 新增 `UCollectionHUDComponent*` 成员和访问器;`BeginPlay()` 中初始化 HUD 组件 | + +### 删除类(v0.1 → v1.0) + +| 类名 | 原因 | +|------|------| +| 旧 `UCollectionManager` (UActorComponent) | 逻辑迁入 `FCollectionManager`,表现迁入 `UCollectionHUDComponent` | ### 新增蓝图 @@ -239,88 +322,90 @@ classDiagram ## 4. 时序图 -> ⚠️ 收集系统涉及物品系统(委托源)→ 收集系统 → UI 三层协调,且有时序要求(通知延迟、完成延迟),满足时序图绘制条件。 - ### 4.1 新物品收集完整流程 +> 涉及 3 层(Interaction → Collection Logic → Presentation)协调,有时序要求(延迟、叠放),满足时序图绘制条件。 + ```mermaid sequenceDiagram autonumber - participant IM as InteractionManager - participant CM as CollectionManager - participant State as 内部状态 + participant EFM as FEffectManager
(ASystemDriver持有) + participant Driver as ASystemDriver
(回调连线) + participant CM as FCollectionManager
(纯逻辑,无Tick) + participant Persist as UPersistenceSubsystem + participant HUD as UCollectionHUDComponent
(Character上) participant NotifUI as WBP_CollectionNotification participant ProgressUI as WBP_CollectionProgress - IM->>CM: OnItemPickedUp/OnItemFound(ItemId, DisplayName, "杂物") - activate CM + Note over EFM,ProgressUI: === Phase 0: Effect 执行 AddToBackpack Action === + EFM->>Driver: 回调: OnItemAdded(ItemId, DisplayName, Category) + Driver->>CM: OnItemObtained(ItemId, DisplayName, Category) Note over CM: 步骤 1: 查品类 - CM->>CM: Category == TrackedCategory("杂物")? + CM->>CM: Category == Config.TrackedCategory("杂物")? Note over CM: 步骤 2: 查重复 - CM->>State: CollectedIDs.Contains(ItemId)? + CM->>CM: CollectedIDs.Contains(ItemId)? - Note over CM,State: 判定为新收集 + Note over CM,HUD: 判定为新收集 Note over CM: 步骤 3: 记录 - CM->>State: CollectedIDs.Add(ItemId), Progress = Num() + CM->>CM: CollectedIDs.Add(ItemId), Progress = CollectedIDs.Num() + CM->>Persist: SetCollectionState(CollectedIDs, Progress, bIsComplete) - Note over CM: 步骤 4: 弹收集通知(延迟) - CM->>CM: SetTimer(NotifDelay = 0.3s) + Note over CM: 步骤 4: 更新缓存(供HUD读取) + CM->>CM: RefreshCachedInfo(DisplayName) + Note right of CM: CachedInfo.Progress = 新值
CachedInfo.bJustCompleted = false
CachedInfo.PendingNotificationText = "发现了新的杂物,进度 X/Y" - Note over CM: 同步更新进度 UI - CM->>ProgressUI: UpdateProgress(Progress, TotalCount) - activate ProgressUI + Note over CM: 步骤 5: 完成判定 + CM->>CM: Progress >= Config.TotalCount? + + alt 是(全部完成) + CM->>CM: bIsComplete = true, bJustCompleted = true + CM->>Persist: SetCollectionState(完成状态) + end + + Note over EFM,ProgressUI: === Phase 1: HUD Tick 检测缓存变化 === + HUD->>CM: Tick: GetCachedInfo().Progress != LastSeenProgress? + HUD->>HUD: 检测到变化 → UpdateProgress() + HUD->>ProgressUI: UpdateProgress(Progress, TotalCount) ProgressUI->>ProgressUI: PlayBounceAnimation (1.2x, 0.2s) - deactivate ProgressUI - Note over CM: 等待 0.3 秒... + Note over HUD: 步骤 4: 弹收集通知(延迟 0.3s) + HUD->>HUD: SetTimer(NotifDelay = 0.3s) - CM->>NotifUI: CreateWidget + AddToViewport + Note over HUD: 等待 0.3 秒... + + HUD->>NotifUI: CreateWidget + AddToViewport activate NotifUI NotifUI->>NotifUI: PlayFadeIn (0.2s) NotifUI->>NotifUI: StayTimer (2.0s) - Note over CM: 步骤 5: 完成判定 - CM->>State: Progress >= TotalCount? - - alt 是(全部完成) - CM->>State: bIsComplete = true - CM->>ProgressUI: SetCompleteStyle() — 文字变金色 - CM->>ProgressUI: PlayCompleteAnimation (1.4x, 0.5s) - - Note over NotifUI: 通知继续显示/淡出 - - NotifUI->>NotifUI: PlayFadeOut (0.3s) - NotifUI->>NotifUI: RemoveFromParent() - deactivate NotifUI - - Note over CM: 等待通知消失 + 0.5s 延迟 - - CM->>CM: ShowCompletionPrompt() - Note over CM: 屏幕中央弹出 "杂物全部收集完成!" - else 未完成 - NotifUI->>NotifUI: PlayFadeOut (0.3s) - NotifUI->>NotifUI: RemoveFromParent() - deactivate NotifUI + alt 全部完成 + HUD->>HUD: 检测 CachedInfo.bJustCompleted == true + HUD->>ProgressUI: SetCompleteStyle() — 文字变金色 + HUD->>ProgressUI: PlayCompleteAnimation (1.4x, 0.5s) end - deactivate CM + NotifUI->>NotifUI: PlayFadeOut (0.3s) + NotifUI->>NotifUI: RemoveFromParent() + deactivate NotifUI + + alt 全部完成 + Note over HUD: 等待通知消失 + CompletePromptDelay + HUD->>HUD: ShowCompletionPrompt() + Note over HUD: 屏幕中央弹出 "杂物全部收集完成!" + end ``` --- ## 5. 核心数据结构 -### 枚举 - -> 收集系统 v0.1 不引入新的枚举类型。所有状态通过内置类型管理(int32 progress, bool bIsComplete, TSet\ collectedIDs)。 - ### 配置数据结构 ```cpp -// 收集系统配置(v0.1 在 CollectionManager 构造函数中设置默认值) +// 收集系统配置(FCollectionManager 使用) USTRUCT(BlueprintType) struct FCollectionConfig { @@ -393,20 +478,116 @@ struct FCollectionConfig }; ``` -### UCollectionManager 关键数据定义 +### Presentation 层缓存数据 + +> Manager 在物品事件时写入,HUDComponent 每帧 Tick 只读。与交互系统的 `FSelectedInfo` 模式一致。 + +```cpp +/** + * FCollectionCachedInfo — 收集系统 Presentation 层缓存 + * + * FCollectionManager 在每次 OnItemObtained 处理完后刷新此结构体。 + * UCollectionHUDComponent 的 TickComponent 每帧读取,通过比较 LastSeenProgress + * 检测变化,驱动 UI 更新。 + */ +struct FCollectionCachedInfo +{ + /** 当前进度 */ + int32 Progress = 0; + + /** 总数 */ + int32 TotalCount = 10; + + /** 是否已全部完成 */ + bool bIsComplete = false; + + /** 是否刚刚完成(本次 OnItemObtained 触发了完成) */ + bool bJustCompleted = false; + + /** 待显示的通知文本(如 "发现了新的杂物,进度 3/10") */ + FString PendingNotificationText; + + /** 本次收集的物品 ID(用于通知回调定位) */ + FName LastCollectedItemId; + + void Reset() + { + bJustCompleted = false; + PendingNotificationText.Empty(); + LastCollectedItemId = FName(); + } +}; +``` + +**写入时机**:`FCollectionManager::RefreshCachedInfo()` — 在 `OnItemObtained` 中成功记录新收集后调用。 + +**读取方**:`UCollectionHUDComponent::TickComponent` 每帧比较 `CachedInfo.Progress` 与自身 `LastSeenProgress`,检测变化后驱动 UI。 + +### FCollectionManager 关键数据定义 + +```cpp +/** + * FCollectionManager — 收集系统逻辑核心(普通 C++,非 UObject) + * + * 持有收集状态、配置、Presentation 缓存。通过 ASystemDriver 回调链接收物品事件。 + * 不持有任何 UObject 强引用,不直接操作 UI。 + */ +class FCollectionManager +{ +public: + // === 初始化 === + void Initialize(); // 从 UPersistenceSubsystem 恢复状态 + + // === 物品事件入口(由 ASystemDriver 回调触发)=== + void OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category); + + // === Presentation 层访问 === + const FCollectionCachedInfo& GetCachedInfo() const { return CachedInfo; } + const FCollectionConfig& GetConfig() const { return Config; } + + // === 持久化恢复 === + void RestoreFromPersistence(class UPersistenceSubsystem* Subsystem); + +private: + // === 配置 === + FCollectionConfig Config; // 收集系统参数 + + // === 运行时状态 === + int32 Progress = 0; // 当前已收集数 + TSet CollectedIDs; // 已收集物品 ID 集合(仅追踪品类) + bool bIsComplete = false; // 是否全部收集完成 + + // === Presentation 缓存 === + FCollectionCachedInfo CachedInfo; // 供 HUDComponent 只读 + + // === 内部方法 === + void ProcessNewCollection(FName ItemId, const FString& DisplayName); + void RefreshCachedInfo(const FString& DisplayName); +}; +``` + +### UCollectionHUDComponent 关键数据定义 ```cpp UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) -class UCollectionManager : public UActorComponent +class UCollectionHUDComponent : public UActorComponent { GENERATED_BODY() public: - UCollectionManager(); + UCollectionHUDComponent(); - // === 配置参数 === - UPROPERTY(EditAnywhere, Category="Collection") - FCollectionConfig Config; + // === 初始化 === + void Initialize(ASystemDriver* InDriver); + +protected: + virtual void BeginPlay() override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) override; + +private: + // === Driver 引用(弱引用,不持有)=== + TWeakObjectPtr Driver; // === UI 类引用(蓝图设置) === UPROPERTY(EditDefaultsOnly, Category="Collection|UI") @@ -418,46 +599,28 @@ public: UPROPERTY(EditDefaultsOnly, Category="Collection|UI") TSubclassOf CompleteWidgetClass; -protected: - virtual void BeginPlay() override; + // === 变化检测(Presentation 层状态)=== + int32 LastSeenProgress = -1; // 上次看到的进度,-1 = 未初始化 + bool bCompletionShown = false; // 完成表现是否已显示 - // === 运行时状态 === - int32 Progress = 0; // 当前已收集数 - TSet CollectedIDs; // 已收集物品 ID 集合 - bool bIsComplete = false; // 是否全部收集完成 + // === UI 实例 === + UPROPERTY() + TArray ActiveNotifications; - // === UI 引用 === - TArray ActiveNotifications; // 当前活跃的通知 Widget - UCollectionProgressWidget* ProgressWidget = nullptr; // 进度 UI + UPROPERTY() + UCollectionProgressWidget* ProgressWidget = nullptr; // === 定时器 === - FTimerHandle NotifDelayTimerHandle; // 通知延迟定时器 - FTimerHandle CompleteTimerHandle; // 完成提示定时器 + FTimerHandle NotifDelayTimerHandle; // 通知延迟定时器 + FTimerHandle CompleteTimerHandle; // 完成提示定时器 // === 关键接口 === - - // 物品获得回调(绑定到 InteractionManager 委托) - UFUNCTION() - void OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category); - - // 处理新收集(查品类 + 查重复通过后调用) - void ProcessNewCollection(FName ItemId); - - // 显示收集通知(延迟定时器回调) - void ShowCollectionNotification(); - - // 通知 Widget 移除回调 - UFUNCTION() - void OnNotificationRemoved(UCollectionNotifWidget* Widget); - - // 检查完成状态并触发完成表现 - void CheckCompletion(); - - // 显示完成提示 UI + void CreateProgressWidget(); + void ShowNotification(const FString& Text); + void UpdateProgress(int32 Current, int32 Total); + void TriggerCompletionSequence(); void ShowCompletionPrompt(); - - // 更新进度 UI 文本和动画 - void UpdateProgressUI(); + void OnNotificationRemoved(UCollectionNotifWidget* Widget); }; ``` @@ -465,21 +628,19 @@ protected: ## 6. 行为规则 → 实现方案映射 -对执行文档中每条行为规则,逐一说明实现方案。 - ### 一、收到获得物品通知后的流程(步骤 1-5) #### 步骤 1: 查品类 — 检查物品品类是否为追踪品类 -**实现**:在委托回调中检查 Category 是否匹配 TrackedCategory +**实现**:`FCollectionManager::OnItemObtained` 中检查 Category 是否匹配 ```cpp -void UCollectionManager::OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category) +void FCollectionManager::OnItemObtained(FName ItemId, const FString& DisplayName, const FName& Category) { // 步骤 1: 查品类 if (Category != Config.TrackedCategory) return; // 非追踪品类,忽略 // → 继续查重复 - ProcessNewCollection(ItemId); + ProcessNewCollection(ItemId, DisplayName); } ``` @@ -492,15 +653,13 @@ void UCollectionManager::OnItemObtained(FName ItemId, const FString& DisplayName **实现**:TSet\ 的 Contains 查找 ```cpp -void UCollectionManager::ProcessNewCollection(FName ItemId) +void FCollectionManager::ProcessNewCollection(FName ItemId, const FString& DisplayName) { // 步骤 2: 查重复 if (CollectedIDs.Contains(ItemId)) return; // 非新收集,流程结束 // → 新收集,继续记录 ``` -> TSet::Contains 为 O(1) 操作,v0.1 最多 10 个元素,性能无忧。 - --- #### 步骤 3: 记录 — 将编号加入 collectedIDs,progress +1 @@ -510,41 +669,72 @@ void UCollectionManager::ProcessNewCollection(FName ItemId) ```cpp // 步骤 3: 记录 CollectedIDs.Add(ItemId); - Progress = CollectedIDs.Num(); // 用 Set 大小作为权威进度值,避免状态不一致 - // 持久化到 GameInstance - SaveStateToGameInstance(); + Progress = CollectedIDs.Num(); // 用 Set 大小作为权威进度值 + + // 持久化到 UPersistenceSubsystem + SaveStateToPersistence(); ``` --- #### 步骤 4: 弹收集通知(延迟 0.3s) -**实现**:启动定时器,延迟后创建通知 Widget +**实现**:`FCollectionManager` 更新缓存(不直接操作 UI),`UCollectionHUDComponent` 通过 Tick 检测到进度变化后,在内部启动 0.3s 定时器再创建通知 Widget。 + +Manager 侧(更新缓存): ```cpp - // 步骤 4: 弹收集通知(延迟) - GetWorld()->GetTimerManager().SetTimer( - NotifDelayTimerHandle, - this, &UCollectionManager::ShowCollectionNotification, - Config.NotifDelay, false); + // 步骤 4: 更新 Presentation 缓存 + RefreshCachedInfo(DisplayName); + // HUDComponent 的 Tick 会检测到 Progress 变化 +``` - // 同步更新进度 UI - UpdateProgressUI(); +HUDComponent 侧(Tick 检测变化 → 延迟通知): + +```cpp +void UCollectionHUDComponent::TickComponent(float DeltaTime, ...) +{ + auto* Driver = ASystemDriver::Get(GetWorld()); + if (!Driver) return; + + const auto& Cache = Driver->GetCollectionManager().GetCachedInfo(); + + // 检测进度变化 + if (Cache.Progress != LastSeenProgress && LastSeenProgress >= 0) + { + // 更新进度 UI(同步) + UpdateProgress(Cache.Progress, Cache.TotalCount); + + // 弹收集通知(延迟 NotifDelay 后创建 Widget) + GetWorld()->GetTimerManager().SetTimer( + NotifDelayTimerHandle, + [this, Text = Cache.PendingNotificationText]() { ShowNotification(Text); }, + Driver->GetCollectionManager().GetConfig().NotifDelay, false); + } + LastSeenProgress = Cache.Progress; + + // 检测完成 + if (Cache.bJustCompleted && !bCompletionShown) + { + TriggerCompletionSequence(); + bCompletionShown = true; + } +} ``` --- #### 步骤 5: 完成判定 — progress 达到 10 则 isComplete = true -**实现**:ProcessNewCollection 末尾检查 +**实现**:`ProcessNewCollection` 末尾检查 ```cpp // 步骤 5: 完成判定 if (Progress >= Config.TotalCount && !bIsComplete) { bIsComplete = true; - SaveStateToGameInstance(); // 持久化完成状态 - CheckCompletion(); // 触发完成表现 + CachedInfo.bJustCompleted = true; + SaveStateToPersistence(); // 持久化完成状态 } } ``` @@ -555,11 +745,16 @@ void UCollectionManager::ProcessNewCollection(FName ItemId) #### 显示规则 -**实现**:ShowCollectionNotification() 创建 WBP_CollectionNotification 并添加到视口 +**实现**:`UCollectionHUDComponent::ShowNotification()` 创建 WBP_CollectionNotification 并添加到视口 ```cpp -void UCollectionManager::ShowCollectionNotification() +void UCollectionHUDComponent::ShowNotification(const FString& Text) { + auto* Driver = ASystemDriver::Get(GetWorld()); + if (!Driver) return; + + const auto& Config = Driver->GetCollectionManager().GetConfig(); + // 检查同屏最大数量 if (ActiveNotifications.Num() >= Config.MaxSimultaneousNotifs) return; @@ -567,80 +762,31 @@ void UCollectionManager::ShowCollectionNotification() GetWorld(), NotifWidgetClass); if (!NotifWidget) return; - // 设置内容 - FString Content = FString::Printf(TEXT("发现了新的杂物,进度 %d/%d"), - Progress, Config.TotalCount); - - NotifWidget->InitializeNotif(Content, Config.NotifTextColor, + NotifWidget->InitializeNotif(Text, Config.NotifTextColor, Config.NotifFadeInDuration, Config.NotifStayDuration, Config.NotifFadeOutDuration); - - // 设置位置:屏幕右上角,上一条通知下方 - NotifWidget->AddToViewport(10); // Z-order + NotifWidget->AddToViewport(10); ActiveNotifications.Add(NotifWidget); } ``` -**可调参数**:NotifDelay (0.3s, 0.1-0.5s), NotifTextColor (#F0D060), MaxSimultaneousNotifs (2, 1-3) +**可调参数**:NotifDelay (0.3s), NotifTextColor (#F0D060), MaxSimultaneousNotifs (2) --- #### 显示动画(淡入 → 停留 → 淡出) -**实现**:UCollectionNotifWidget 内部管理三阶段生命周期 +**实现**:`UCollectionNotifWidget` 内部管理三阶段生命周期(与 v0.1 相同,此处不涉及架构变更) - **淡入**:UMG Animation 从 Opacity 0 → 1,时长 0.2s - **停留**:SetTimer 延迟 2.0s 后触发淡出 -- **淡出**:UMG Animation 从 Opacity 1 → 0,时长 0.3s;完成后调用 RemoveFromParent() - -```cpp -void UCollectionNotifWidget::InitializeNotif(const FString& Text, const FLinearColor& Color, - float FadeIn, float Stay, float FadeOut) -{ - // 更新 TextBlock 内容和颜色 - // 保存动画参数 - FadeInDuration = FadeIn; - StayDuration = Stay; - FadeOutDuration = FadeOut; -} - -void UCollectionNotifWidget::NativeOnAddedToViewport() -{ - // 播放淡入动画 - PlayAnimation(FadeInAnim); - // 启动停留定时器 - GetWorld()->GetTimerManager().SetTimer(StayTimerHandle, this, - &UCollectionNotifWidget::StartFadeOut, StayDuration, false); -} - -void UCollectionNotifWidget::StartFadeOut() -{ - PlayAnimation(FadeOutAnim); - GetWorld()->GetTimerManager().SetTimer(FadeOutTimerHandle, this, - &UCollectionNotifWidget::OnFadeOutComplete, FadeOutDuration, false); -} - -void UCollectionNotifWidget::OnFadeOutComplete() -{ - RemoveFromParent(); - // 通知 CollectionManager 从 ActiveNotifications 中移除引用 -} -``` - -**可调参数**:NotifFadeInDuration (0.2s, 0.1-0.3s), NotifStayDuration (2.0s, 1.5-3.0s), NotifFadeOutDuration (0.3s, 0.2-0.5s) +- **淡出**:UMG Animation 从 Opacity 1 → 0,时长 0.3s;完成后 RemoveFromParent() --- #### 与拾取通知的叠放 -**实现**:通知位置基于 Anchor 和垂直偏移量 - -- **Anchor**:屏幕右上角(HorizontalAlignment: Right, VerticalAlignment: Top) -- **与拾取通知关系**:收集通知始终在拾取通知下方。v0.1 简化为固定偏移位置(拾取通知默认位置下方约 60px,可调) -- **收集通知之间**:新通知从上一条下方弹出,间距约 40px(可调) -- **最大 2 条**:ActiveNotifications.Num() >= MaxSimultaneousNotifs 时不再创建 - -> v0.1 原型简化:收集通知使用固定 Anchor + 垂直偏移。不动态追踪拾取通知的具体像素位置。若拾取通知 UI 不存在,收集通知在其默认位置显示。 +**实现**:与 v0.1 相同。通知 Anchor 固定在右上角,固定垂直偏移。v0.1 不动态追踪拾取通知位置。 --- @@ -648,143 +794,72 @@ void UCollectionNotifWidget::OnFadeOutComplete() #### 显示规则 -**实现**:WBP_CollectionProgress 在 CollectionManager::BeginPlay() 时添加到视口 - -- **Anchor**:Top Left(Anchors: X=0, Y=0) -- **Offset**:左边距 3% 屏幕宽度,上边距 3% 屏幕高度(在 NativeOnAddedToViewport 中根据屏幕尺寸计算) -- **格式**:`📦 杂物 3/10`(品类图标 + 品类名 + 当前/总数) -- **图标**:白盒阶段用 emoji 📦 或纯色方块,尺寸约 20×20px -- **文字**:白色,正常字体大小 -- **常驻显示**:游戏开始后始终可见,不随交互状态隐藏 +**实现**:WBP_CollectionProgress 在 `UCollectionHUDComponent::BeginPlay()` 时创建并添加到视口。 ```cpp -void UCollectionManager::BeginPlay() +void UCollectionHUDComponent::CreateProgressWidget() { - Super::BeginPlay(); + auto* Driver = ASystemDriver::Get(GetWorld()); + if (!Driver) return; - // 从 GameInstance 恢复持久状态(v0.1 单场景等效于空数据初始化) - LoadStateFromGameInstance(); + APlayerController* PC = GetWorld()->GetFirstPlayerController(); + if (!PC || !ProgressWidgetClass) return; - // 创建进度 UI - if (ProgressWidgetClass) + ProgressWidget = CreateWidget(PC, ProgressWidgetClass); + if (!ProgressWidget) return; + + ProgressWidget->AddToViewport(5); + + const auto& Cache = Driver->GetCollectionManager().GetCachedInfo(); + ProgressWidget->UpdateProgress(Cache.Progress, Cache.TotalCount); + + if (Cache.bIsComplete) { - APlayerController* PC = GetWorld()->GetFirstPlayerController(); - if (PC) - { - ProgressWidget = CreateWidget(PC, ProgressWidgetClass); - if (ProgressWidget) - { - ProgressWidget->AddToViewport(5); - ProgressWidget->UpdateProgress(Progress, Config.TotalCount); - // 已完成时恢复完成样式 - if (bIsComplete) - { - ProgressWidget->SetCompleteStyle(); - } - } - } + ProgressWidget->SetCompleteStyle(); } - // 绑定 InteractionManager 委托 - AChildHoodCharacter* Char = Cast(GetOwner()); - if (Char && Char->GetInteractionManager()) - { - UInteractionManager* IM = Char->GetInteractionManager(); - IM->OnItemPickedUp.AddDynamic(this, &UCollectionManager::OnItemObtained); - IM->OnItemFound.AddDynamic(this, &UCollectionManager::OnItemObtained); - } + LastSeenProgress = Cache.Progress; } ``` -**可调参数**:ProgressMarginX (3%, 2%-5%), ProgressMarginY (3%, 2%-5%) - --- #### 数值更新动画 -**实现**:UpdateProgress() 触发 UMG Scale 动画 +**实现**:`UCollectionProgressWidget::UpdateProgress()` 触发 UMG Scale 动画(与 v0.1 相同) -```cpp -void UCollectionProgressWidget::UpdateProgress(int32 Current, int32 Total) -{ - // 更新文本 - FString Text = FString::Printf(TEXT("📦 杂物 %d/%d"), Current, Total); - // 设置到 TextBlock - - // 播放弹性缩放动画 - PlayAnimation(BounceAnim); -} -``` - -- **弹性效果**:数字部分 RenderScale 从 1.0 → 1.2 → 1.0,时长 0.2s -- **使用 UMG Animation 时间轴**:FloatCurve 在 0→0.2→0 映射 Scale X/Y - -**可调参数**:BounceScale (1.2, 1.1-1.5), BounceDuration (0.2s, 0.1-0.4s) +- 弹性效果:数字部分 RenderScale 从 1.0 → 1.2 → 1.0,时长 0.2s --- ### 四、全部收集完成表现 -**触发条件**:ProcessNewCollection() 中 Progress 达到 TotalCount 时调用 CheckCompletion() +**触发条件**:`UCollectionHUDComponent::TickComponent` 检测到 `CachedInfo.bJustCompleted == true` -**实现步骤**: - -**1. 进度 UI 变金色 + 弹性缩放**: +**实现**: ```cpp -void UCollectionManager::CheckCompletion() +void UCollectionHUDComponent::TriggerCompletionSequence() { - if (!ProgressWidget) return; + auto* Driver = ASystemDriver::Get(GetWorld()); + if (!Driver || !ProgressWidget) return; - // 进度数字变金色 + const auto& Config = Driver->GetCollectionManager().GetConfig(); + + // 1. 进度 UI 变金色 + 弹性缩放 ProgressWidget->SetCompleteStyle(); - - // 完成缩放动画(1.4x, 0.5s) ProgressWidget->PlayCompleteAnimation(); + + // 2. 延迟弹出完成提示 + float TotalDelay = Config.NotifDelay + Config.NotifStayDuration + + Config.NotifFadeOutDuration + Config.CompletePromptDelay; + GetWorld()->GetTimerManager().SetTimer(CompleteTimerHandle, + this, &UCollectionHUDComponent::ShowCompletionPrompt, + TotalDelay, false); } ``` -**2. 延迟弹出完成提示**: - -完成提示需要在最后一条收集通知消失后 0.5s 弹出。从 ProcessNewCollection 调用时刻算起: - -``` -总延迟 = NotifDelay + NotifStayDuration + NotifFadeOutDuration + CompletePromptDelay - = 0.3 + 2.0 + 0.3 + 0.5 - = 3.1s -``` - -```cpp -// 在 ProcessNewCollection 中(完成判定分支内) -float TotalDelay = Config.NotifDelay + Config.NotifStayDuration - + Config.NotifFadeOutDuration + Config.CompletePromptDelay; -GetWorld()->GetTimerManager().SetTimer(CompleteTimerHandle, - this, &UCollectionManager::ShowCompletionPrompt, - TotalDelay, false); -``` - -**3. 完成提示 UI(WBP_CollectionComplete)**: - -```cpp -void UCollectionManager::ShowCompletionPrompt() -{ - if (!CompleteWidgetClass) return; - - UCollectionCompleteWidget* CompleteWidget = CreateWidget( - GetWorld(), CompleteWidgetClass); - if (!CompleteWidget) return; - - CompleteWidget->AddToViewport(20); // 最高 Z-order - CompleteWidget->ShowComplete(); // 开始淡入 + 定时淡出 -} -``` - -- **位置**:屏幕正中央(Anchors: Center) -- **内容**:「杂物全部收集完成!」白色大字 -- **动画**:淡入 → 停留 2.0s → 淡出 → 自动移除 -- **无其他效果**:过场动画、NPC 反应等留给后续版本 - -**可调参数**:CompleteScale (1.4, 1.2-1.8), CompleteScaleDuration (0.5s, 0.3-0.8s), CompletePromptDelay (0.5s, 0.3-1.0s), CompletePromptDuration (2.0s, 1.5-3.0s) +完成提示 UI(WBP_CollectionComplete)与 v0.1 相同:屏幕正中央、「杂物全部收集完成!」白色大字、淡入→停留 2.0s→淡出。 --- @@ -792,47 +867,37 @@ void UCollectionManager::ShowCompletionPrompt() #### 重复获得同一物品 -**实现**:CollectedIDs.Contains(ItemId) 在步骤 2 拦截。v0.1 交互物均为 oneShot,理论上不会重复触发同一 ItemId 的通知。即使发生,查重机制保证 progress 不超过 TotalCount,不会弹出重复的收集通知。 +**实现**:`CollectedIDs.Contains(ItemId)` 在步骤 2 拦截。v0.1 交互物均为 oneShot,理论上不会重复。 #### 全部收集完成后继续收到通知 -**实现**:v0.1 不会发生(oneShot 机制 + 最多 10 个物品)。即使发生,CollectedIDs 查重拦截,无副作用。bIsComplete 仅触发一次完成表现。 +**实现**:CollectedIDs 查重拦截,无副作用。bIsComplete 仅触发一次完成表现。 #### 重启状态处理 -**实现**:收集状态通过 `UChildHoodGameInstance` 跨场景持久化。CollectionManager::BeginPlay() 中从 GameInstance 恢复: +**实现**:`FCollectionManager::RestoreFromPersistence()` 在 `ASystemDriver::BeginPlay()` 中调用,从 `UPersistenceSubsystem` 恢复: ```cpp -void UCollectionManager::LoadStateFromGameInstance() +void FCollectionManager::RestoreFromPersistence(UPersistenceSubsystem* Subsystem) { - if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance()) - { - GI->GetCollectionState(CollectedIDs, Progress, bIsComplete); - } - else - { - // 回退:无 GameInstance 时初始化为空(编辑器预览等场景) - Progress = 0; - CollectedIDs.Empty(); - bIsComplete = false; - } -} + if (!Subsystem) return; -void UCollectionManager::SaveStateToGameInstance() -{ - if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance()) - { - GI->SetCollectionState(CollectedIDs, Progress, bIsComplete); - } + TSet SavedIDs; + int32 SavedProgress = 0; + bool SavedComplete = false; + Subsystem->GetCollectionState(SavedIDs, SavedProgress, SavedComplete); + + CollectedIDs = MoveTemp(SavedIDs); + Progress = SavedProgress; + bIsComplete = SavedComplete; + + // 初始化缓存 + CachedInfo.Progress = Progress; + CachedInfo.TotalCount = Config.TotalCount; + CachedInfo.bIsComplete = bIsComplete; } ``` -v0.1 单场景下:GameInstance 以空数据初始化,等效于 `📦 杂物 0/10`。场景内正常收集后状态写入 GameInstance,若通过 Seamless Travel 切换关卡则自动恢复。非无缝跳转(OpenLevel)时 GameInstance 仍存活,新场景的 CollectionManager 可读取之前的数据。 - -#### 收集通知与拾取通知时序 - -**实现**:收集通知通过 NotifDelay (0.3s) 定时器延迟弹出,保证与拾取通知有时间差。若拾取通知已消失(停留时间已过),收集通知在其原位置显示。两者不会完全重叠。 - --- ## 7. 美术资源需求 @@ -844,7 +909,7 @@ v0.1 单场景下:GameInstance 以空数据初始化,等效于 `📦 杂物 | UI Widget | WBP_CollectionComplete | 1 | UMG Widget,白色大字,含淡入/淡出动画 | 全部收集完成时弹出 | | 2D 图标 | 品类图标(杂物占位) | 1 | 约 20×20px,白盒阶段用 emoji 📦 或纯色方块 | 进度 UI 中的品类图标 | -> v0.1 原型阶段:UI Widget 使用简单 UMG 实现(TextBlock + Border),图标用 emoji 或纯色方块占位。字体使用引擎默认字体。后续版本替换为正式美术资源和自定义字体。 +> v0.1 原型阶段:UI Widget 使用简单 UMG 实现(TextBlock + Border),图标用 emoji 或纯色方块占位。 --- @@ -852,52 +917,48 @@ v0.1 单场景下:GameInstance 以空数据初始化,等效于 `📦 杂物 | 风险 | 等级 | 说明 | 缓解措施 | |------|------|------|---------| -| 通知叠放位置与拾取通知冲突 | 🟡中 | 收集通知需要与拾取通知协调屏幕位置,但两个系统独立管理各自的 UI | v0.1 简化处理:收集通知 Anchor 固定在右上角,不动态追踪拾取通知;后续版本引入统一 HUD 管理器协调位置 | -| 完成提示时机精度 | 🟢低 | 完成提示需要在最后一条收集通知消失后 0.5s 弹出,延迟计算依赖动画时间参数 | 使用精确的定时器计算总延迟,参数集中管理在 FCollectionConfig 中 | -| 通知 Widget 内存管理 | 🟢低 | 频繁创建/销毁通知 Widget 理论上有性能开销 | v0.1 最多 10 个物品、最多同屏 2 条通知,性能无忧;后续可改为对象池 | -| 委托绑定时机 | 🟢低 | CollectionManager 需在 InteractionManager 初始化后绑定委托 | Character::BeginPlay() 按序初始化两个组件;CollectionManager 中绑定前做非空检查 | -| 多通知定时器覆盖 | 🟢低 | 使用单个 NotifDelayTimerHandle,连续拾取可能覆盖前一个定时器 | v0.1 物品均为 oneShot 且拾取间隔远大于 0.3s,不会发生;后续版本改为队列机制 | +| 回调连线时序 | 🟡中 | CollectionManager 依赖 EffectManager 的 AddToBackpack Action 回调,需在 Driver::BeginPlay 中确保 EffectManager 先初始化 | Driver::BeginPlay 显式控制初始化顺序:EffectManager → CollectionManager 回调注册 | +| 通知叠放位置与拾取通知冲突 | 🟡中 | 收集通知与拾取通知由不同系统管理 | v0.1 简化处理:收集通知 Anchor 固定在右上角 | +| 完成提示时机精度 | 🟢低 | 延迟计算依赖动画时间参数 | 使用精确的定时器计算总延迟,参数集中管理 | +| HUDComponent Tick 性能 | 🟢低 | 每 Tick 读取 Manager 缓存(int32 比较) | 开销极低(几个 int32 比较),可忽略 | ### 注意事项 -1. **委托绑定时机**:CollectionManager 在 BeginPlay() 中通过 `GetOwner()->GetComponentByClass()` 获取引用并绑定。需确保 InteractionManager 已初始化。若为 nullptr 则在 Tick 中延迟绑定(最多重试 N 帧后报错)。 -2. **持久化读写时机**:`SaveStateToGameInstance()` 在每次 ProcessNewCollection 记录后和完成判定后调用,保证 GameInstance 中的数据始终最新。`LoadStateFromGameInstance()` 仅在 BeginPlay 中调用一次。 -2. **v0.1 无拾取通知 UI**:执行文档中拾取通知由物品系统负责,但 v0.1 物品系统仅为委托层(InteractionManager 上的委托)。收集通知的 0.3s 延迟仍按设计实现(为拾取通知预留时间窗口),即使拾取通知 UI 暂未实现。 -3. **通知移除回调**:UCollectionNotifWidget 淡出完成后需通知 CollectionManager 从 ActiveNotifications 中移除自身引用。使用委托或弱引用回调实现,避免循环引用。 -4. **FName 作为 ID**:收集系统使用 FName(ItemId)作为去重键,与 InteractionManager 委托广播的 ItemId 类型一致。 -5. **TSet 线程安全**:v0.1 单线程游戏,TSet 无需加锁。 +1. **FCollectionManager 是普通 C++**:不是 UObject,不参与 GC。不持有任何 UObject 强引用。与交互系统的 `FInteractionManager` / `FEffectManager` 模式一致。 +2. **回调连线在 Driver::BeginPlay**:EffectManager 的 AddToBackpack Action 执行时,通过 Driver 注册的回调通知 CollectionManager。连线逻辑集中在 Driver,不散落在各 Manager 内部。 +3. **HUDComponent 不碰 Subsystem**:所有持久化操作由 FCollectionManager 在 World 层完成,Presentation 层只读 Manager 缓存。 +4. **持久化读写时机**:`SaveStateToPersistence()` 在每次 ProcessNewCollection 记录后和完成判定后调用。`RestoreFromPersistence()` 在 Driver::BeginPlay 中调用一次。 +5. **v0.1 无拾取通知 UI**:执行文档中拾取通知由物品系统负责。收集通知的 0.3s 延迟仍按设计实现。 --- ## 9. 数据流 -### 配置参数流 +### 事件源流 ``` -FCollectionConfig(C++ 构造函数默认值) - → CollectionManager EditAnywhere 属性(蓝图/Details 面板可覆盖) - → 运行时读取 Config 中的参数驱动 UI 和定时器 -``` - -v0.1 原型阶段所有参数在 C++ 中设置默认值,蓝图可覆盖。批量生产阶段迁移到 DataAsset 配置。 - -### 运行时状态流 - -``` -InteractionManager (ExecuteAnimEvent) - → OnItemPickedUp / OnItemFound.Broadcast(ItemId, DisplayName, Category) - → UCollectionManager::OnItemObtained() (委托回调) +FEffectManager (AddToBackpack Action 执行) + → ASystemDriver 回调: OnItemAdded(ItemId, DisplayName, Category) + → FCollectionManager::OnItemObtained() ├─ 查品类 (Category == TrackedCategory?) → 不匹配则忽略 ├─ 查重复 (CollectedIDs.Contains?) → 已存在则忽略 ├─ 记录 (CollectedIDs.Add + Progress = Num()) - ├─ SaveStateToGameInstance() → UChildHoodGameInstance (持久化) - ├─ UpdateProgressUI() → WBP_CollectionProgress (文本 + 弹性动画) - ├─ SetTimer(NotifDelay) → ShowCollectionNotification() → WBP_CollectionNotification + ├─ SaveStateToPersistence() → UPersistenceSubsystem + ├─ RefreshCachedInfo() → FCollectionCachedInfo 更新 └─ 完成判定 (Progress >= TotalCount) - → bIsComplete = true - → SaveStateToGameInstance() (持久化完成状态) - → SetCompleteStyle() → WBP_CollectionProgress (金色) - → SetTimer(TotalDelay) → ShowCompletionPrompt() → WBP_CollectionComplete + → bIsComplete = true, bJustCompleted = true + → SaveStateToPersistence() +``` + +### Presentation 数据流(独立于逻辑,只读 Manager 缓存) + +``` +UCollectionHUDComponent::TickComponent + → Driver->GetCollectionManager().GetCachedInfo() + → CachedInfo.Progress != LastSeenProgress? + → 是 → UpdateProgress() + SetTimer(NotifDelay) → ShowNotification() + → CachedInfo.bJustCompleted && !bCompletionShown? + → 是 → TriggerCompletionSequence() ``` ### 跨场景恢复流 @@ -905,16 +966,17 @@ InteractionManager (ExecuteAnimEvent) ``` 场景切换 (OpenLevel / Seamless Travel) → 新场景加载 + → ASystemDriver::BeginPlay() + → FCollectionManager::RestoreFromPersistence(Subsystem) + → Subsystem->GetCollectionState(OutIDs, OutProgress, OutComplete) + → 恢复 CollectedIDs / Progress / bIsComplete → AChildHoodCharacter::BeginPlay() - → UCollectionManager::BeginPlay() - → LoadStateFromGameInstance() - → UChildHoodGameInstance::GetCollectionState(OutIDs, OutProgress, OutComplete) - → 恢复 CollectedIDs / Progress / bIsComplete - → 创建进度 UI(显示已恢复的进度值) + → UCollectionHUDComponent::BeginPlay() + → CreateProgressWidget()(显示已恢复的进度值) → 若 bIsComplete → 恢复完成样式 - → 场景中 AInteractableActor::BeginPlay() - → UInteractableComponent::RestorePersistentState(GI) - → GI->IsItemCollected(ItemId) → 已收集则隐藏 + 禁用 + → 场景中 AInteractableActor::BeginPlay() + → UInteractableComponent::RestorePersistentState(Subsystem) + → Subsystem->IsItemCollected(ItemId) → 已收集则隐藏 + 禁用 ``` --- @@ -923,22 +985,22 @@ InteractionManager (ExecuteAnimEvent) | 参数 | C++ 属性 | 默认值 | 位置 | |------|---------|--------|------| -| 追踪品类 | `Config.TrackedCategory` | "杂物" | CollectionManager 构造函数 | -| 品类总数 | `Config.TotalCount` | 10 | CollectionManager 构造函数 | -| 通知延迟 | `Config.NotifDelay` | 0.3s | CollectionManager 构造函数 | -| 通知淡入时长 | `Config.NotifFadeInDuration` | 0.2s | CollectionManager 构造函数 | -| 通知停留时长 | `Config.NotifStayDuration` | 2.0s | CollectionManager 构造函数 | -| 通知淡出时长 | `Config.NotifFadeOutDuration` | 0.3s | CollectionManager 构造函数 | -| 通知文字颜色 | `Config.NotifTextColor` | #F0D060 | CollectionManager 构造函数 | -| 最大同屏通知数 | `Config.MaxSimultaneousNotifs` | 2 | CollectionManager 构造函数 | -| 进度 UI 左边距 | `Config.ProgressMarginX` | 3% | CollectionManager 构造函数 | -| 进度 UI 上边距 | `Config.ProgressMarginY` | 3% | CollectionManager 构造函数 | -| 进度数字弹性缩放 | `Config.BounceScale` | 1.2 | CollectionManager 构造函数 | -| 进度数字弹性时长 | `Config.BounceDuration` | 0.2s | CollectionManager 构造函数 | -| 完成金色缩放 | `Config.CompleteScale` | 1.4 | CollectionManager 构造函数 | -| 完成金色时长 | `Config.CompleteScaleDuration` | 0.5s | CollectionManager 构造函数 | -| 完成提示延迟 | `Config.CompletePromptDelay` | 0.5s | CollectionManager 构造函数 | -| 完成提示显示时长 | `Config.CompletePromptDuration` | 2.0s | CollectionManager 构造函数 | +| 追踪品类 | `Config.TrackedCategory` | "杂物" | FCollectionConfig | +| 品类总数 | `Config.TotalCount` | 10 | FCollectionConfig | +| 通知延迟 | `Config.NotifDelay` | 0.3s | FCollectionConfig | +| 通知淡入时长 | `Config.NotifFadeInDuration` | 0.2s | FCollectionConfig | +| 通知停留时长 | `Config.NotifStayDuration` | 2.0s | FCollectionConfig | +| 通知淡出时长 | `Config.NotifFadeOutDuration` | 0.3s | FCollectionConfig | +| 通知文字颜色 | `Config.NotifTextColor` | #F0D060 | FCollectionConfig | +| 最大同屏通知数 | `Config.MaxSimultaneousNotifs` | 2 | FCollectionConfig | +| 进度 UI 左边距 | `Config.ProgressMarginX` | 3% | FCollectionConfig | +| 进度 UI 上边距 | `Config.ProgressMarginY` | 3% | FCollectionConfig | +| 进度数字弹性缩放 | `Config.BounceScale` | 1.2 | FCollectionConfig | +| 进度数字弹性时长 | `Config.BounceDuration` | 0.2s | FCollectionConfig | +| 完成金色缩放 | `Config.CompleteScale` | 1.4 | FCollectionConfig | +| 完成金色时长 | `Config.CompleteScaleDuration` | 0.5s | FCollectionConfig | +| 完成提示延迟 | `Config.CompletePromptDelay` | 0.5s | FCollectionConfig | +| 完成提示显示时长 | `Config.CompletePromptDuration` | 2.0s | FCollectionConfig | --- @@ -947,129 +1009,162 @@ InteractionManager (ExecuteAnimEvent) ``` Source/ChildHood/ ├── Collection/ -│ ├── CollectionTypes.h # FCollectionConfig 结构体定义 -│ ├── CollectionManager.h/.cpp # 收集系统核心组件(含 GameInstance 读写) -│ ├── CollectionNotifWidget.h/.cpp # 收集通知 UI 基类 -│ ├── CollectionProgressWidget.h/.cpp # 收集进度 UI 基类 -│ └── CollectionCompleteWidget.h/.cpp # 完成提示 UI 基类 -├── ChildHoodGameInstance.h/.cpp # 新增:跨关卡持久数据持有者(收集状态在此持久化) -├── ChildHoodCharacter.h/.cpp # 修改:添加 CollectionManager 成员和访问器 +│ ├── CollectionTypes.h # FCollectionConfig + FCollectionCachedInfo +│ ├── CollectionManager.h/.cpp # FCollectionManager(普通C++,纯逻辑) +│ ├── CollectionHUDComponent.h/.cpp # UCollectionHUDComponent(表现层) +│ ├── CollectionNotifWidget.h/.cpp # UCollectionNotifWidget(通知 UI 基类) +│ ├── CollectionProgressWidget.h/.cpp # UCollectionProgressWidget(进度 UI 基类) +│ └── CollectionCompleteWidget.h/.cpp # UCollectionCompleteWidget(完成 UI 基类) +├── SystemDriver.h/.cpp # 修改:新增 FCollectionManager + 回调连线 +├── Interaction/ +│ └── PersistenceSubsystem.h/.cpp # 修改:新增收集状态数据 + 读写接口 +├── ChildHoodCharacter.h/.cpp # 修改:新增 UCollectionHUDComponent 成员 +└── ChildHoodGameInstance.h/.cpp # 清理:移除收集相关数据(迁入 PersistenceSubsystem) Content/ ├── UI/ -│ ├── WBP_CollectionNotification.uasset # 收集通知 UMG Widget -│ ├── WBP_CollectionProgress.uasset # 收集进度 UMG Widget -│ └── WBP_CollectionComplete.uasset # 完成提示 UMG Widget +│ ├── WBP_CollectionNotification.uasset # 收集通知 UMG Widget +│ ├── WBP_CollectionProgress.uasset # 收集进度 UMG Widget +│ └── WBP_CollectionComplete.uasset # 完成提示 UMG Widget ``` --- -## 工期预估与实现拆分 +## 12. 跨场景持久化设计 + +### 架构原则 + +与交互系统 v1.1 一致:持久数据存储在 `UPersistenceSubsystem` (GameInstanceSubsystem),逻辑层随 World 生灭。 + +``` +┌──────────────────────────────────────────────────┐ +│ UPersistenceSubsystem (GameInstanceSubsystem) │ ← 跨关卡存活 +│ ├── CollectedItemIDs: TSet │ (交互系统用) +│ ├── DoorStates: TMap │ (交互系统用) +│ ├── CollectionCollectedIDs: TSet │ (收集系统用) +│ ├── CollectionProgress: int32 │ +│ └── bCollectionComplete: bool │ +└──────────────────────────────────────────────────┘ + ↕ RestoreFromPersistence / SaveStateToPersistence +┌──────────────────────────────────────────────────┐ +│ FCollectionManager (每个场景重建) │ ← World 层逻辑 +│ ├── 从 Subsystem 读取恢复进度 │ +│ ├── 收集时写入 Subsystem │ +│ └── 不持有持久数据所有权 │ +└──────────────────────────────────────────────────┘ +``` + +### UPersistenceSubsystem 新增接口 + +```cpp +UCLASS() +class UPersistenceSubsystem : public UGameInstanceSubsystem +{ + // ... 交互系统已有接口 ... + + // === 收集系统接口 === + + /** 获取收集状态(恢复用) */ + void GetCollectionState(TSet& OutIDs, int32& OutProgress, bool& OutComplete) const; + + /** 设置收集状态(收集时写入) */ + void SetCollectionState(const TSet& IDs, int32 Progress, bool bComplete); + +private: + // === 收集系统数据 === + UPROPERTY() + TSet CollectionCollectedIDs; // 追踪品类的已收集物品 ID + + UPROPERTY() + int32 CollectionProgress = 0; + + UPROPERTY() + bool bCollectionComplete = false; +}; +``` + +### 读写时机 + +| 时机 | 方法 | 说明 | +|------|------|------| +| ASystemDriver::BeginPlay | `CollectionManager.RestoreFromPersistence()` | 场景加载时恢复进度 | +| ProcessNewCollection 记录后 | `SaveStateToPersistence()` | 每次新收集时同步写入 | +| 完成判定后 | `SaveStateToPersistence()` | 完成状态立即持久化 | +| InteractableComponent::Consume | `Subsystem->MarkItemCollected()` | 交互物消耗状态写入(交互系统调用) | + +### v0.1 行为 + +- v0.1 为单场景原型,不存在跨场景切换 +- `UPersistenceSubsystem` 在 v0.1 中以空数据初始化 +- `RestoreFromPersistence()` 等效于初始化为 0 +- 后续多场景版本无需修改收集系统代码 + +--- + +## 13. ASystemDriver 回调连线 + +> 收集系统通过 Driver 的回调链接收物品事件,这是 v1.0 解耦的关键机制。 + +### Driver::BeginPlay 连线示意 + +```cpp +void ASystemDriver::BeginPlay() +{ + Super::BeginPlay(); + + // 1. 初始化 EffectManager + EffectManager.Initialize(/* ... */); + + // 2. 初始化 InteractionManager + InteractionManager.Initialize(/* ... */); + + // 3. 初始化 CollectionManager + CollectionManager.Initialize(); + CollectionManager.RestoreFromPersistence( + GetGameInstance()->GetSubsystem()); + + // 4. 回调连线:EffectManager AddToBackpack → CollectionManager + EffectManager.SetOnItemAddedCallback( + [this](FName ItemId, const FString& DisplayName, const FName& Category) + { + CollectionManager.OnItemObtained(ItemId, DisplayName, Category); + } + ); +} +``` + +### 设计要点 + +- **EffectManager 不直接知道 CollectionManager** — 通过 Driver 的 std::function 回调解耦 +- **CollectionManager 不直接知道 EffectManager** — 只接收 (ItemId, DisplayName, Category) 三元组 +- **连线逻辑集中在 Driver** — 一个地方看清所有系统间关系 +- **后续扩展**:新增需要物品事件的系统,只需在 Driver 中多注册一个回调 + +--- + +## 14. 工期预估与实现拆分 ### 工作量估算 | 维度 | 估算 | |------|------| | 总工期 | 2-3 天 | -| 新增 C++ 代码 | 250-350 行 | +| 新增 C++ 代码 | 300-400 行 | | 新增蓝图 | 3 个 UMG Widget | -| 修改现有代码 | 约 10 行(Character 添加成员) | +| 修改现有代码 | 约 50 行(Driver + Character + PersistenceSubsystem) | | 实现复杂度 | 低-中 | ### 实现拆分建议 -供步骤⑥(程序实现)参考的推荐拆分方式,每个子任务可独立验证: - | 子任务 | 内容 | 预估时间 | 依赖 | 验证方式 | |--------|------|---------|------|---------| -| 6b-1 | 数据类型定义 | CollectionTypes.h FCollectionConfig + CollectionManager 基础框架(状态管理 + BeginPlay) | 0.5 天 | 交互系统 6a | 编译通过 | -| 6b-2 | 委托绑定 + OnItemObtained 完整流程(查品类→查重复→记录→完成判定) | 0.5 天 | 6b-1, 交互系统 | PIE 控制台输出收集日志(新收集/重复/忽略) | -| 6b-3 | WBP_CollectionProgress UI + UpdateProgress + 弹性动画 | 0.5 天 | 6b-1 | PIE 中拾取物品后进度文本更新、数字弹跳 | -| 6b-4 | WBP_CollectionNotification UI + 延迟弹出 + 淡入淡出 + 叠放 | 1 天 | 6b-2 | PIE 中拾取新物品后 0.3s 看到金色通知弹出、自动消失 | -| 6b-5 | WBP_CollectionComplete UI + 完成表现(进度变金色 + 中心文字) | 0.5 天 | 6b-3, 6b-4 | 拾取全部 10 个物品后看到完成提示 | -| 6b-6 | 集成测试 + 参数微调 | 0.5 天 | 6b-5 | 按探索路线完整拾取全部物品验证 | +| 14a | CollectionTypes 重写(FCollectionConfig + FCollectionCachedInfo) | 0.5d | 无 | 编译通过 | +| 14b | FCollectionManager 核心逻辑(品类检查+去重+记录+完成判定+缓存) | 0.5d | 14a | 单元验证:构造测试用例验证逻辑 | +| 14c | UPersistenceSubsystem 收集接口 + ASystemDriver 回调连线 | 0.5d | 14b | 编译通过 | +| 14d | UCollectionHUDComponent + WBP_CollectionProgress + 进度动画 | 0.5d | 14b | PIE 进度文本更新、数字弹跳 | +| 14e | WBP_CollectionNotification + 延迟弹出 + 淡入淡出 + 叠放 | 0.5d | 14d | PIE 通知弹出、自动消失 | +| 14f | WBP_CollectionComplete + 完成表现 | 0.5d | 14d, 14e | PIE 完成提示弹出 | +| 14g | 集成测试(与交互系统联调) | 0.5d | 14c, 14f | 完整拾取流程验证 | -> **关键路径**:6b-1 → 6b-2 → 6b-4 → 6b-5 → 6b-6。6b-3(进度 UI)可在 6b-1 完成后并行。总关键路径约 3 天。 -> **前置依赖**:交互系统的 InteractionManager 组件需已完成(特别是 OnItemPickedUp/OnItemFound 委托),方可进行 6b-2 的委托绑定测试。 - ---- - -## 12. 跨场景持久化设计 - -> 收集进度是玩家的核心持久数据,必须在场景切换时保留。本节定义收集系统如何通过 `UChildHoodGameInstance` 实现跨场景数据持久化。 - -### 架构原则 - -``` -┌──────────────────────────────────────────────────┐ -│ UChildHoodGameInstance │ ← 跨关卡存活 -│ ├── CollectedItemIDs: TSet │ 所有场景共享 -│ ├── CollectionProgress: int32 │ -│ └── bCollectionComplete: bool │ -└──────────────────────────────────────────────────┘ - ↕ BeginPlay 加载 / 收集时回写 -┌──────────────────────────────────────────────────┐ -│ UCollectionManager (每个场景重建) │ ← 行为层 -│ ├── 从 GameInstance 读取恢复进度 │ 不持有持久数据所有权 -│ ├── 收集时写入 GameInstance │ -│ └── 控制 UI / 通知 / 动画 │ -└──────────────────────────────────────────────────┘ - ↕ 场景恢复 -┌──────────────────────────────────────────────────┐ -│ 场景中的 AInteractableActor │ ← 由交互系统负责恢复 -│ └── RestorePersistentState() │ (见交互系统技术方案第 12 节) -└──────────────────────────────────────────────────┘ -``` - -### UChildHoodGameInstance 收集相关接口 - -```cpp -UCLASS() -class UChildHoodGameInstance : public UGameInstance -{ - GENERATED_BODY() - -public: - // === 收集状态 === - void GetCollectionState(TSet& OutIDs, int32& OutProgress, bool& OutComplete) const; - void SetCollectionState(const TSet& IDs, int32 Progress, bool bComplete); - - // === 物品消耗状态(交互系统也使用)=== - bool IsItemCollected(FName ItemId) const; - void MarkItemCollected(FName ItemId); - - // === 门状态(交互系统也使用)=== - bool IsDoorOpen(FName DoorId) const; - void SetDoorOpen(FName DoorId, bool bIsOpen); - - // === 重置 === - void ResetAll(); - -private: - // 收集系统 - TSet CollectedItemIDs; - int32 CollectionProgress = 0; - bool bCollectionComplete = false; - // 交互系统 - TMap DoorStates; -}; -``` - -> **注意**:`CollectedItemIDs` 同时被 `IsItemCollected()`(交互物恢复)和 `GetCollectionState()`(收集进度恢复)使用,确保两系统数据一致性。 - -### 读写时机 - -| 时机 | 方法 | 说明 | -|------|------|------| -| CollectionManager::BeginPlay | `LoadStateFromGameInstance()` | 场景加载时恢复进度 | -| ProcessNewCollection 记录后 | `SaveStateToGameInstance()` | 每次新收集时同步写入 | -| 完成判定后 | `SaveStateToGameInstance()` | 完成状态立即持久化 | -| InteractableComponent::Consume | `GI->MarkItemCollected()` | 交互物消耗状态写入(由交互系统调用) | - -### v0.1 行为 - -- v0.1 为单场景原型,不存在跨场景切换 -- `UChildHoodGameInstance` 在 v0.1 中以空数据初始化,所有查询返回默认值 -- `LoadStateFromGameInstance()` 等效于初始化为 0 -- 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为 `UChildHoodGameInstance`,收集系统代码无需修改 -- 后续版本可通过 USaveGame 将 GameInstance 数据序列化到磁盘,实现跨会话持久化 +> **关键路径**:14a → 14b → 14c → 14g。14d/14e/14f(UI 层)可在 14b 完成后并行。总关键路径约 2.5 天。 +> **前置依赖**:交互系统 v1.1 的 FEffectManager 需已实现 AddToBackpack Action(P-012),方可进行 14g 集成测试。14a-14f 可独立开发。