docs(technical): 收集系统技术方案 v1.0:Effect/Action 解耦架构修订

This commit is contained in:
2026-06-11 23:08:27 +08:00
parent 8a75e682c5
commit 690cfeb29d
12 changed files with 1463 additions and 790 deletions
@@ -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)
@@ -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:** 后续涉及交互系统技术方案变更时参考此记录。
+275
View File
@@ -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:
<types>
<type>
<name>user</name>
<description>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.</description>
<when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>
<how_to_use>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.</how_to_use>
<examples>
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]
</examples>
</type>
<type>
<name>feedback</name>
<description>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.</description>
<when_to_save>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.</when_to_save>
<how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>
<body_structure>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.</body_structure>
<examples>
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]
</examples>
</type>
<type>
<name>project</name>
<description>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.</description>
<when_to_save>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.</when_to_save>
<how_to_use>Use these memories to more fully understand the details and nuance behind the user's request and make better informed suggestions.</how_to_use>
<body_structure>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.</body_structure>
<examples>
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]
</examples>
</type>
<type>
<name>reference</name>
<description>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.</description>
<when_to_save>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_to_save>
<how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>
<examples>
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]
</examples>
</type>
</types>
## 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.
@@ -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;
};
@@ -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)
@@ -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<UStaticMeshComponent> MeshComp;
/** 交互物的交互组件(数据 + 检测 + 状态) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UInteractableComponent* InteractableComp;
TObjectPtr<UInteractableComponent> InteractableComp;
};
@@ -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<USphereComponent>(this, USphereComponent::StaticClass(), TEXT("DetectionSphere"));
if (DetectionSphere)
{
@@ -58,14 +61,14 @@ void UInteractableComponent::BeginPlay()
}
// --- 恢复跨场景持久状态 ---
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
if (UPersistenceSubsystem* Persistence = GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
{
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<UChildHoodGameInstance>())
if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
{
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<AInteractableActor>(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<AInteractableActor>(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<UInteractionManager>())
{
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<UInteractionManager>())
{
IM->UnregisterInteractable(this);
}
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this)
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
// TODO(Phase 2): 替换为 ASystemDriver 路径
}
@@ -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<USphereComponent> 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,
+243 -120
View File
@@ -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<UAnimMontage> Montage;
// --- PlayMontageOnTarget ---
/** 交互物蒙太奇动画 */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type == EInteractActionType::PlayMontageOnTarget", EditConditionHides))
TObjectPtr<UAnimMontage> 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<FString> 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<FInteractAction> 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<FString> NarrativeTexts;
// ============================================================================
// 运行时上下文
// ============================================================================
// === 摄像机推镜 ===
/** Effect 运行时上下文 — EffectManager 执行时传入,持有弱引用 */
USTRUCT()
struct FEffectContext
{
GENERATED_BODY()
/** 是否使用摄像机推镜 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera")
bool bUseCameraPush = false;
/** 触发交互的角色 */
TWeakObjectPtr<ACharacter> Instigator;
/** 推镜距离(cm),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "10.0", ClampMax = "300.0"))
float CameraPushDistance = 80.0f;
/** 交互物组件(配置数据来源) */
TWeakObjectPtr<UInteractableComponent> TargetComp;
/** 推镜过渡时长(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
float CameraPushDuration = 0.5f;
/** 交互物 Actor(视觉操作目标) */
TWeakObjectPtr<AActor> 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<UAnimMontage> 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);
@@ -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: 所有持久数据已重置"));
}
@@ -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<FName> CollectedItemIDs;
/** 门开关状态映射(true=打开,false=关闭) */
UPROPERTY()
TMap<FName, bool> DoorStates;
};
File diff suppressed because it is too large Load Diff