diff --git a/docs/api/XCEngine/Components/AudioListenerComponent/AudioListenerComponent.md b/docs/api/XCEngine/Components/AudioListenerComponent/AudioListenerComponent.md index b3711436..96cfccd1 100644 --- a/docs/api/XCEngine/Components/AudioListenerComponent/AudioListenerComponent.md +++ b/docs/api/XCEngine/Components/AudioListenerComponent/AudioListenerComponent.md @@ -6,43 +6,116 @@ **头文件**: `XCEngine/Components/AudioListenerComponent.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `AudioListenerComponent` public API。 +**描述**: 表示场景中的监听器对象,负责把宿主对象的变换同步到音频系统,并保存若干全局监听参数。 -## 概述 +## 角色概述 -`AudioListenerComponent.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`AudioListenerComponent` 在设计意图上对应 Unity 风格的“场景监听器”,也就是从哪个位置和朝向去听声音。 -## 声明概览 +但按当前实现,它的真实职责要分成两层看: -| 声明 | 类型 | 说明 | -|------|------|------| -| `AudioListenerComponent` | `class` | 继承自 `Component` 的公开声明。 | +- 已接入的部分: 同步监听器位置/旋转,控制 master volume 和 mute +- 尚未完整接入的部分: 多数高级监听参数与频谱分析数据 -## 公共方法 +## 当前实现行为 -| 方法 | 描述 | -|------|------| -| [AudioListenerComponent()](Constructor.md) | 构造对象。 | -| [~AudioListenerComponent()](Destructor.md) | 销毁对象并释放相关资源。 | -| [GetEnergy](GetEnergy.md) | 获取相关状态或对象。 | -| [GetFrequencyData](GetFrequencyData.md) | 获取相关状态或对象。 | -| [GetFrequencyDataSize](GetFrequencyDataSize.md) | 获取相关状态或对象。 | -| [SetMasterVolume](SetMasterVolume.md) | 设置相关状态或配置。 | -| [GetMasterVolume](GetMasterVolume.md) | 获取相关状态或对象。 | -| [SetMute](SetMute.md) | 设置相关状态或配置。 | -| [IsMute](IsMute.md) | 查询当前状态。 | -| [SetDopplerLevel](SetDopplerLevel.md) | 设置相关状态或配置。 | -| [GetDopplerLevel](GetDopplerLevel.md) | 获取相关状态或对象。 | -| [SetSpeedOfSound](SetSpeedOfSound.md) | 设置相关状态或配置。 | -| [GetSpeedOfSound](GetSpeedOfSound.md) | 获取相关状态或对象。 | -| [SetReverbLevel](SetReverbLevel.md) | 设置相关状态或配置。 | -| [GetReverbLevel](GetReverbLevel.md) | 获取相关状态或对象。 | -| [SetReverb](SetReverb.md) | 设置相关状态或配置。 | -| [GetReverb](GetReverb.md) | 获取相关状态或对象。 | -| [Update](Update.md) | 更新运行时状态。 | -| [GetName](GetName.md) | 获取相关状态或对象。 | +### 1. `Update()` 会把 Transform 同步到 AudioSystem + +当前 `Update(float deltaTime)` 实际会做的事情非常明确: + +- 如果没有宿主对象,直接返回 +- 读取 `transform().GetPosition()` 和 `transform().GetRotation()` +- 调用 `AudioSystem::SetListenerTransform(position, rotation)` + +因此,这个组件的核心已接线能力就是“用场景对象表示监听器”。 + +### 2. `SetMasterVolume()` 和 `SetMute()` 会立即作用于 AudioSystem + +这两个 setter 不是单纯存值: + +- `SetMasterVolume()` 会 clamp 到 `0.0f ~ 1.0f`,然后立刻调用 `AudioSystem::SetMasterVolume()` +- `SetMute()` 会立刻调用 `AudioSystem::SetMuted()` + +这意味着它们属于当前已真实生效的运行时控制项。 + +### 3. 其他监听参数当前主要是本地存值 + +按当前取证到的 `AudioListenerComponent.cpp` 和 `AudioSystem.*`: + +- `SetDopplerLevel()` +- `SetSpeedOfSound()` +- `SetReverbLevel()` +- `SetReverb()` + +这些 API 目前主要更新组件自身字段,没有看到额外代码把它们接入实际音频处理后端。 + +也就是说,API 形状已经预留了更完整的监听器能力,但当前版本真正落地的还比较有限。 + +## 分析与监视接口的现实状态 + +组件公开了: + +- `GetEnergy()` +- `GetFrequencyData()` +- `GetFrequencyDataSize()` + +但在当前取证范围内,没有看到代码去持续更新 `m_energy` 或填充 `m_frequencyData`。因此这些接口目前更像预留位,不应被当成成熟的实时音频分析能力。 + +## 序列化语义 + +`AudioListenerComponent` 当前也 **没有覆写** `Serialize()` / `Deserialize()`。 + +这意味着: + +- 场景保存/加载不会持久化 master volume、mute、doppler、reverb 等监听器配置 +- 场景反序列化即使能重建组件类型,也只会得到默认值 + +## 线程语义 + +- 当前实现没有内部加锁。 +- `Update()` 默认应视为主线程场景更新路径。 +- 和 `AudioSystem` 的交互不意味着该组件本身是线程安全对象。 + +## 当前实现限制 + +- 除 `master volume`、`mute` 和监听器 transform 外,多数公开参数当前没有完整接入实际音频后端。 +- `deltaTime` 形参在 `Update()` 里当前未使用。 +- `GetEnergy()` / `GetFrequencyData()` 暂时不能当成可靠的分析结果来源。 +- 没有在当前测试目录中看到该组件的直接单元测试覆盖。 + +## 推荐理解方式 + +现阶段最准确的理解是: + +- 它已经是“可用的监听器挂点” +- 但还不是“完整的音频分析与环境声学中心” + +如果你只是需要让音频系统跟随相机或玩家位置,这个组件已经足够;如果你期待更复杂的环境音频控制,目前还需要继续实现。 + +## 相关方法 + +- [Constructor](Constructor.md) +- [Update](Update.md) +- [SetMasterVolume](SetMasterVolume.md) +- [GetMasterVolume](GetMasterVolume.md) +- [SetMute](SetMute.md) +- [IsMute](IsMute.md) +- [SetDopplerLevel](SetDopplerLevel.md) +- [GetDopplerLevel](GetDopplerLevel.md) +- [SetSpeedOfSound](SetSpeedOfSound.md) +- [GetSpeedOfSound](GetSpeedOfSound.md) +- [SetReverbLevel](SetReverbLevel.md) +- [GetReverbLevel](GetReverbLevel.md) +- [SetReverb](SetReverb.md) +- [GetReverb](GetReverb.md) +- [GetEnergy](GetEnergy.md) +- [GetFrequencyData](GetFrequencyData.md) +- [GetFrequencyDataSize](GetFrequencyDataSize.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [AudioSourceComponent](../AudioSourceComponent/AudioSourceComponent.md) +- [AudioSystem](../../Audio/AudioSystem/AudioSystem.md) +- [GameObject](../GameObject/GameObject.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/AudioSourceComponent/AudioSourceComponent.md b/docs/api/XCEngine/Components/AudioSourceComponent/AudioSourceComponent.md index 5ba58dee..a2616df6 100644 --- a/docs/api/XCEngine/Components/AudioSourceComponent/AudioSourceComponent.md +++ b/docs/api/XCEngine/Components/AudioSourceComponent/AudioSourceComponent.md @@ -6,66 +6,174 @@ **头文件**: `XCEngine/Components/AudioSourceComponent.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `AudioSourceComponent` public API。 +**描述**: 表示一个可挂载到 `GameObject` 上的音频源,负责音频片段引用、基础播放状态、样本解码和简化混音输出。 -## 概述 +## 角色概述 -`AudioSourceComponent.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`AudioSourceComponent` 名字看起来像传统商业引擎里的完整 3D 音频源,但按当前实现,它更准确的定位是: -## 声明概览 +- 一个挂在对象上的播放状态容器 +- 一个对 `AudioClip` 做基础解码和样本拷贝的运行时节点 +- 一个向 [AudioSystem](../../Audio/AudioSystem/AudioSystem.md) 注册/反注册自身的音频源 -| 声明 | 类型 | 说明 | -|------|------|------| -| `AudioSourceComponent` | `class` | 继承自 `Component` 的公开声明。 | +它已经不只是纯数据组件,因为确实参与了解码和混音;但它距离完整的 mixer routing、3D spatialization、reverb、doppler 和高质量变调系统仍有明显距离。 -## 公共方法 +## 当前实现行为 -| 方法 | 描述 | -|------|------| -| [AudioSourceComponent()](Constructor.md) | 构造对象。 | -| [~AudioSourceComponent()](Destructor.md) | 销毁对象并释放相关资源。 | -| [Play](Play.md) | 公开方法,详见头文件声明。 | -| [Pause](Pause.md) | 公开方法,详见头文件声明。 | -| [Stop](Stop.md) | 公开方法,详见头文件声明。 | -| [IsPlaying](IsPlaying.md) | 查询当前状态。 | -| [IsPaused](IsPaused.md) | 查询当前状态。 | -| [SetClip](SetClip.md) | 设置相关状态或配置。 | -| [GetClip](GetClip.md) | 获取相关状态或对象。 | -| [SetVolume](SetVolume.md) | 设置相关状态或配置。 | -| [GetVolume](GetVolume.md) | 获取相关状态或对象。 | -| [SetPitch](SetPitch.md) | 设置相关状态或配置。 | -| [GetPitch](GetPitch.md) | 获取相关状态或对象。 | -| [SetPan](SetPan.md) | 设置相关状态或配置。 | -| [GetPan](GetPan.md) | 获取相关状态或对象。 | -| [SetLooping](SetLooping.md) | 设置相关状态或配置。 | -| [IsLooping](IsLooping.md) | 查询当前状态。 | -| [SetSpatialize](SetSpatialize.md) | 设置相关状态或配置。 | -| [IsSpatialize](IsSpatialize.md) | 查询当前状态。 | -| [Set3DParams](Set3DParams.md) | 设置相关状态或配置。 | -| [Get3DParams](Get3DParams.md) | 获取相关状态或对象。 | -| [SetDopplerLevel](SetDopplerLevel.md) | 设置相关状态或配置。 | -| [GetDopplerLevel](GetDopplerLevel.md) | 获取相关状态或对象。 | -| [SetSpread](SetSpread.md) | 设置相关状态或配置。 | -| [GetSpread](GetSpread.md) | 获取相关状态或对象。 | -| [SetReverbZoneMix](SetReverbZoneMix.md) | 设置相关状态或配置。 | -| [GetReverbZoneMix](GetReverbZoneMix.md) | 获取相关状态或对象。 | -| [SetOutputMixer](SetOutputMixer.md) | 设置相关状态或配置。 | -| [GetOutputMixer](GetOutputMixer.md) | 获取相关状态或对象。 | -| [SetTime](SetTime.md) | 设置相关状态或配置。 | -| [GetTime](GetTime.md) | 获取相关状态或对象。 | -| [GetDuration](GetDuration.md) | 获取相关状态或对象。 | -| [GetEnergy](GetEnergy.md) | 获取相关状态或对象。 | -| [StartEnergyDetect](StartEnergyDetect.md) | 公开方法,详见头文件声明。 | -| [StopEnergyDetect](StopEnergyDetect.md) | 公开方法,详见头文件声明。 | -| [IsEnergyDetecting](IsEnergyDetecting.md) | 查询当前状态。 | -| [Update](Update.md) | 更新运行时状态。 | -| [OnEnable](OnEnable.md) | 公开方法,详见头文件声明。 | -| [OnDisable](OnDisable.md) | 公开方法,详见头文件声明。 | -| [OnDestroy](OnDestroy.md) | 公开方法,详见头文件声明。 | -| [ProcessAudio](ProcessAudio.md) | 公开方法,详见头文件声明。 | -| [GetName](GetName.md) | 获取相关状态或对象。 | +### 1. `Play()` 的前提是 clip 有效 + +`Play()` 会先检查: + +- `m_clip != nullptr` +- `m_clip->IsValid() == true` + +不满足时直接返回,不报错、不抛异常。 + +### 2. 设置音频片段时会立即尝试解码 + +`SetClip()` 会: + +- 保存 `m_clip` +- 把 `m_isDecoded` 置为 false +- 如果 clip 有效,立即执行 `DecodeAudioData()` + +所以当前实现更接近“设置资源时做一次预解码缓存”,而不是延迟到首帧播放时才解码。 + +### 3. 组件真的参与样本混音 + +`ProcessAudio()` 当前会: + +- 检查播放状态、clip 和已解码缓存 +- 按输出采样块遍历样本 +- 从 `m_decodedData` 中拷贝/映射到输出 buffer +- 乘以音量 +- 可选执行简化距离衰减 +- 在结尾推进 `m_samplePosition` + +这说明它不是“只存参数,实际交给别处”的假组件;它确实承担了当前版本的播放核心路径。 + +## 和 AudioSystem 的关系 + +按当前实现: + +- `Play()` 会把源注册到 `AudioSystem` +- `Pause()` 和 `Stop()` 会从 `AudioSystem` 反注册 +- `OnEnable()` / `OnDisable()` 也会在播放态下做注册或反注册 +- `OnDestroy()` 会调用 `Stop()` + +这套设计的好处是对象激活状态和音频系统中的活跃源列表能保持基本一致。 + +## 当前参数里哪些真的生效 + +这是当前文档必须最明确说明的一部分。 + +### 已确认有直接效果的参数 + +- `clip` +- `volume` +- `pitch` + 说明: 当前只体现在 `Update()` 中对时间和样本位置推进的影响,不是高质量重采样。 +- `looping` +- `spatialize` + 说明: 决定是否执行当前的简化距离衰减。 +- `Audio3DParams::maxDistance` + 说明: 这是当前 `Apply3DAttenuation()` 里真正使用到的 3D 参数之一。 + +### 当前主要是存值或接线不完整的参数 + +- `pan` + 说明: 在当前 `ProcessAudio()` 路径中没有看到实际声像处理。 +- `outputMixer` + 说明: 当前没有看到混音路由消费它。 +- `dopplerLevel` +- `spread` +- `reverbZoneMix` +- `Audio3DParams::speedOfSound` +- `Audio3DParams::minDistance` +- `Audio3DParams::panLevel` + +这些字段的 API 形状已经存在,但根据当前取证到的代码,它们还没有完整接入实际播放路径。 + +## 序列化语义 + +`AudioSourceComponent` 当前 **没有覆写** `Serialize()` / `Deserialize()`。 + +这意味着: + +- 场景保存/加载时,不会持久化 clip、音量、循环、3D 参数等音频源状态 +- 即使 `ComponentFactoryRegistry` 能按类型名重新创建 `AudioSourceComponent`,它恢复出来的也只是默认构造状态 + +这是当前模块最重要的限制之一。 + +## 线程与更新路径 + +需要特别注意,当前实现的时间推进有两条路径: + +- `Update(float deltaTime)` 会推进 `m_lastingTime` 和 `m_samplePosition` +- `ProcessAudio()` 在混音结束后也会推进 `m_samplePosition` + +这意味着当前样本位置推进逻辑比较粗糙,不能把它理解成已经完成了严格的音频时钟统一。 + +## 当前实现限制 + +- `Stop(Audio::StopMode mode)` 当前忽略 `mode`,无论传什么都立即停止。 +- `ProcessAudio(..., listenerRotation)` 形参当前未使用。 +- `Apply3DAttenuation()` 会直接修改成员 `m_volume`,导致距离衰减会污染后续音量状态,而不是只作用于当次混音。 +- `Pause()` 会反注册音频源,但 `Play()` 在“从暂停恢复”路径下只改状态,不重新注册到 `AudioSystem`。 +- 如果对象已经在播放中再次调用 `Play()`,当前实现会再次 `RegisterSource(this)`,没有去重保护。 +- `pan`、`outputMixer`、多数 3D 参数目前没有完整接入实际音频处理。 +- 没有在当前测试目录中看到该组件的直接单元测试覆盖。 + +这些限制都来自当前源码,而不是推测。 + +## 推荐理解方式 + +现阶段更适合把 `AudioSourceComponent` 理解成“已经能跑起来的基础播放节点”,而不是完整商业级音频系统的最终形态。它已经足够支撑基础功能验证和引擎接线,但在参数生效范围和播放状态一致性上还需要继续打磨。 + +## 相关方法 + +- [Constructor](Constructor.md) +- [Play](Play.md) +- [Pause](Pause.md) +- [Stop](Stop.md) +- [SetClip](SetClip.md) +- [GetClip](GetClip.md) +- [SetVolume](SetVolume.md) +- [GetVolume](GetVolume.md) +- [SetPitch](SetPitch.md) +- [GetPitch](GetPitch.md) +- [SetPan](SetPan.md) +- [GetPan](GetPan.md) +- [SetLooping](SetLooping.md) +- [IsLooping](IsLooping.md) +- [SetSpatialize](SetSpatialize.md) +- [IsSpatialize](IsSpatialize.md) +- [Set3DParams](Set3DParams.md) +- [Get3DParams](Get3DParams.md) +- [SetDopplerLevel](SetDopplerLevel.md) +- [GetDopplerLevel](GetDopplerLevel.md) +- [SetSpread](SetSpread.md) +- [GetSpread](GetSpread.md) +- [SetReverbZoneMix](SetReverbZoneMix.md) +- [GetReverbZoneMix](GetReverbZoneMix.md) +- [SetOutputMixer](SetOutputMixer.md) +- [GetOutputMixer](GetOutputMixer.md) +- [SetTime](SetTime.md) +- [GetTime](GetTime.md) +- [GetDuration](GetDuration.md) +- [StartEnergyDetect](StartEnergyDetect.md) +- [StopEnergyDetect](StopEnergyDetect.md) +- [GetEnergy](GetEnergy.md) +- [Update](Update.md) +- [OnEnable](OnEnable.md) +- [OnDisable](OnDisable.md) +- [OnDestroy](OnDestroy.md) +- [ProcessAudio](ProcessAudio.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [AudioListenerComponent](../AudioListenerComponent/AudioListenerComponent.md) +- [AudioSystem](../../Audio/AudioSystem/AudioSystem.md) +- [GameObject](../GameObject/GameObject.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/CameraComponent/CameraComponent.md b/docs/api/XCEngine/Components/CameraComponent/CameraComponent.md index 1cfbb821..980c970f 100644 --- a/docs/api/XCEngine/Components/CameraComponent/CameraComponent.md +++ b/docs/api/XCEngine/Components/CameraComponent/CameraComponent.md @@ -6,44 +6,115 @@ **头文件**: `XCEngine/Components/CameraComponent.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `CameraComponent` public API。 +**描述**: 保存相机投影、裁剪面、深度和清屏颜色等参数,供渲染场景提取阶段构建真正的相机数据。 -## 概述 +## 角色概述 -`CameraComponent.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`CameraComponent` 是当前渲染管线里的相机配置组件。它本身不持有 view/projection 矩阵,也不直接执行渲染;真正的消费方是 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md)。 -## 声明概览 +从职责上看,它更像 Unity 里“挂在对象上的 Camera 配置数据”,而不是一个自带完整渲染流程的重型对象。 -| 声明 | 类型 | 说明 | -|------|------|------| -| `CameraProjectionType` | `enum class` | 头文件中的公开声明。 | -| `CameraComponent` | `class` | 继承自 `Component` 的公开声明。 | +## 当前实现行为 -## 公共方法 +### 1. 这是一个纯数据组件 -| 方法 | 描述 | -|------|------| -| [GetName](GetName.md) | 获取相关状态或对象。 | -| [GetProjectionType](GetProjectionType.md) | 获取相关状态或对象。 | -| [SetProjectionType](SetProjectionType.md) | 设置相关状态或配置。 | -| [GetFieldOfView](GetFieldOfView.md) | 获取相关状态或对象。 | -| [SetFieldOfView](SetFieldOfView.md) | 设置相关状态或配置。 | -| [GetOrthographicSize](GetOrthographicSize.md) | 获取相关状态或对象。 | -| [SetOrthographicSize](SetOrthographicSize.md) | 设置相关状态或配置。 | -| [GetNearClipPlane](GetNearClipPlane.md) | 获取相关状态或对象。 | -| [SetNearClipPlane](SetNearClipPlane.md) | 设置相关状态或配置。 | -| [GetFarClipPlane](GetFarClipPlane.md) | 获取相关状态或对象。 | -| [SetFarClipPlane](SetFarClipPlane.md) | 设置相关状态或配置。 | -| [GetDepth](GetDepth.md) | 获取相关状态或对象。 | -| [SetDepth](SetDepth.md) | 设置相关状态或配置。 | -| [IsPrimary](IsPrimary.md) | 查询当前状态。 | -| [SetPrimary](SetPrimary.md) | 设置相关状态或配置。 | -| [GetClearColor](GetClearColor.md) | 获取相关状态或对象。 | -| [SetClearColor](SetClearColor.md) | 设置相关状态或配置。 | -| [Serialize](Serialize.md) | 公开方法,详见头文件声明。 | -| [Deserialize](Deserialize.md) | 公开方法,详见头文件声明。 | +当前 public API 和 `.cpp` 实现只做三件事: + +- 保存投影类型、FOV、正交尺寸、裁剪面、深度、主相机标记和清屏颜色 +- 对关键数值做基础 clamp +- 提供 `Serialize()` / `Deserialize()` 支持 + +组件自身没有 `Update()`,也没有直接向 GPU 或渲染线程提交命令的逻辑。 + +### 2. 数值约束是强制执行的 + +按 `engine/src/Components/CameraComponent.cpp` 当前实现: + +- `SetFieldOfView()` 会把值限制到 `1.0f ~ 179.0f` +- `SetOrthographicSize()` 会限制到 `>= 0.001f` +- `SetNearClipPlane()` 会限制到 `>= 0.001f` +- `SetFarClipPlane()` 会限制到 `>= near + 0.001f` + +这些行为已被 `tests/Components/test_camera_light_component.cpp` 覆盖。 + +### 3. 相机选择规则由渲染场景提取器决定 + +当前 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 的选择顺序是: + +1. 先用可用的 override camera +2. 否则在所有可用主相机里挑 `Depth` 最大的那个 +3. 如果没有主相机,则退回到第一个可用相机 + +“可用”的定义还要求: + +- 组件存在 +- 组件已启用 +- 宿主 `GameObject` 存在 +- 宿主对象 `IsActiveInHierarchy() == true` + +因此,`IsPrimary` 和 `Depth` 的价值并不是抽象概念,而是直接决定当前默认渲染相机选择。 + +## 与 Transform 的关系 + +`CameraComponent` 不保存自己的位置和朝向。渲染时使用的是宿主对象的 `TransformComponent`: + +- 世界位置来自 `camera.transform().GetPosition()` +- 视图矩阵来自 `camera.transform().GetWorldToLocalMatrix()` + +这也是为什么把相机做成 `GameObject` 上的一个组件,比单独创建一个完全独立的相机对象更符合当前引擎设计。 + +## 序列化语义 + +当前会序列化以下字段: + +- `projection` +- `fov` +- `orthoSize` +- `near` +- `far` +- `depth` +- `primary` +- `clearColor` + +因此,`CameraComponent` 已经属于当前场景系统中“可完整保存和恢复的基础组件”。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 应默认在主线程场景配置路径上读写。 +- 文档和代码都没有提供渲染线程并发修改 `CameraComponent` 的安全保证。 + +## 当前实现限制 + +- 组件只描述相机参数,不负责 viewport、后处理、剔除掩码或多相机栈管理。 +- `Depth` 和 `IsPrimary` 的消费点目前主要是 `RenderSceneExtractor`,能力范围仍然比较基础。 +- 没有看到额外的测试覆盖相机切换策略;这部分行为主要来自源码调用点分析。 + +## 相关方法 + +- [GetProjectionType](GetProjectionType.md) +- [SetProjectionType](SetProjectionType.md) +- [GetFieldOfView](GetFieldOfView.md) +- [SetFieldOfView](SetFieldOfView.md) +- [GetOrthographicSize](GetOrthographicSize.md) +- [SetOrthographicSize](SetOrthographicSize.md) +- [GetNearClipPlane](GetNearClipPlane.md) +- [SetNearClipPlane](SetNearClipPlane.md) +- [GetFarClipPlane](GetFarClipPlane.md) +- [SetFarClipPlane](SetFarClipPlane.md) +- [GetDepth](GetDepth.md) +- [SetDepth](SetDepth.md) +- [IsPrimary](IsPrimary.md) +- [SetPrimary](SetPrimary.md) +- [GetClearColor](GetClearColor.md) +- [SetClearColor](SetClearColor.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [GameObject](../GameObject/GameObject.md) +- [TransformComponent](../TransformComponent/TransformComponent.md) +- [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/Component/Component.md b/docs/api/XCEngine/Components/Component/Component.md index fa9b3046..3ce42c20 100644 --- a/docs/api/XCEngine/Components/Component/Component.md +++ b/docs/api/XCEngine/Components/Component/Component.md @@ -2,45 +2,101 @@ **命名空间**: `XCEngine::Components` -**类型**: `class (abstract)` +**类型**: `class (abstract base)` **头文件**: `XCEngine/Components/Component.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `Component` public API。 +**描述**: 所有挂载式组件的抽象基类,定义统一的生命周期钩子、归属对象访问和启用状态控制。 -## 概述 +## 角色概述 -`Component.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`Component` 是 `GameObject` 组合模型里的最小行为单元。它本身不拥有场景对象,也不负责更新循环;真正的驱动者是 `GameObject` 和 `Scene`。`Component` 主要提供三类能力: -## 声明概览 +- 生命周期钩子:`Awake`、`Start`、`Update`、`FixedUpdate`、`LateUpdate`、`OnEnable`、`OnDisable`、`OnDestroy`。 +- 归属关系访问:`GetGameObject()`、`transform()`、`GetScene()`。 +- 运行期开关:`IsEnabled()`、`SetEnabled()`。 -| 声明 | 类型 | 说明 | -|------|------|------| -| `Component` | `class` | 头文件中的公开声明。 | +这和 Unity 的 `Component`/`Behaviour` 设计思路相近,但当前实现更轻:基类默认几乎全部是空实现,没有调度器、执行顺序系统或线程隔离层。 -## 公共方法 +## 生命周期 -| 方法 | 描述 | -|------|------| -| [~Component()](Destructor.md) | 销毁对象并释放相关资源。 | -| [Awake](Awake.md) | 公开方法,详见头文件声明。 | -| [Start](Start.md) | 公开方法,详见头文件声明。 | -| [Update](Update.md) | 更新运行时状态。 | -| [FixedUpdate](FixedUpdate.md) | 公开方法,详见头文件声明。 | -| [LateUpdate](LateUpdate.md) | 公开方法,详见头文件声明。 | -| [OnEnable](OnEnable.md) | 公开方法,详见头文件声明。 | -| [OnDisable](OnDisable.md) | 公开方法,详见头文件声明。 | -| [OnDestroy](OnDestroy.md) | 公开方法,详见头文件声明。 | -| [GetName](GetName.md) | 获取相关状态或对象。 | -| [Serialize](Serialize.md) | 公开方法,详见头文件声明。 | -| [Deserialize](Deserialize.md) | 公开方法,详见头文件声明。 | -| [GetGameObject](GetGameObject.md) | 获取相关状态或对象。 | -| [transform](transform.md) | 公开方法,详见头文件声明。 | -| [GetScene](GetScene.md) | 获取相关状态或对象。 | -| [IsEnabled](IsEnabled.md) | 查询当前状态。 | -| [SetEnabled](SetEnabled.md) | 设置相关状态或配置。 | +`Component` 的生命周期由宿主 `GameObject` 驱动,而不是由组件自身驱动。 + +- `Awake()`、`Start()`、`Update()`、`FixedUpdate()`、`LateUpdate()` 只有在 `GameObject`/`Scene` 主动调用时才会发生。 +- `SetEnabled()` 会根据“组件启用状态”和“对象是否在层级中激活”共同决定是否触发 `OnEnable()` 或 `OnDisable()`。 +- `OnDestroy()` 由 `GameObject::Destroy()` 或 `Scene::DestroyGameObject()` 路径触发;基类析构函数本身不补发销毁回调。 + +按当前实现,直接对一个已经运行中的 `GameObject` 调用 `AddComponent()`,不会自动调用新组件的 `Awake()`、`Start()` 或 `OnEnable()`。如果你的运行时逻辑依赖这些阶段,需要明确安排调用时机。 + +## 所有权与归属 + +- `Component` 不直接暴露 public 构造 ownership;实例通常由 `GameObject` 持有在 `std::unique_ptr` 容器中。 +- `m_gameObject` 由 `GameObject::AddComponent()` 在挂接时回填。 +- `GetGameObject()` 仅返回裸指针,不转移所有权。 +- `GetScene()` 只是经由宿主对象转发访问场景。 + +`transform()` 的前提条件比 `GetScene()` 更严格。当前实现直接解引用 `m_gameObject->GetTransform()`,没有空指针保护,因此它只适用于“已经成功挂到某个 `GameObject` 上”的组件。 + +## 启用状态语义 + +`SetEnabled(bool)` 的有效状态不是单纯的 `m_enabled`,而是: + +`component enabled && gameObject active in hierarchy` + +这意味着: + +- 对挂在非激活对象上的组件执行 `SetEnabled(true)`,不会立刻触发 `OnEnable()`。 +- 当对象重新进入激活层级时,`GameObject` 会补发 `OnEnable()`。 +- 如果启用状态前后“有效可用性”没有变化,`SetEnabled()` 会直接返回,不重复触发回调。 + +这套行为已被 `tests/Components/test_component.cpp` 覆盖验证。 + +## 序列化语义 + +- 基类 `Serialize(std::ostream&)` 默认是 no-op。 +- 基类 `Deserialize(std::istream&)` 默认是 no-op。 + +因此,组件是否能被场景保存/加载,完全取决于具体子类是否覆写这两个函数。文档阅读时不要默认所有组件都可持久化。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 文档和源码都没有提供跨线程读写同一组件实例的安全保证。 +- 按当前调用关系,生命周期和大多数组件访问应默认视为主线程/场景线程语义。 + +对音频类组件来说,虽然会与 `AudioSystem` 交互,但这不代表 `Component` 基类本身具备线程同步能力。 + +## 当前实现限制 + +- `transform()` 对未附着组件是未定义用法,因为实现里没有 null check。 +- 基类不区分“已构造”“已挂接”“已进入场景”“已启动”等更细粒度状态。 +- 没有统一的反射、属性枚举或编辑器元数据系统;文档页只能按实际 public API 解释。 + +## 相关方法 + +- [Awake](Awake.md) +- [Start](Start.md) +- [Update](Update.md) +- [FixedUpdate](FixedUpdate.md) +- [LateUpdate](LateUpdate.md) +- [OnEnable](OnEnable.md) +- [OnDisable](OnDisable.md) +- [OnDestroy](OnDestroy.md) +- [GetGameObject](GetGameObject.md) +- [transform](transform.md) +- [GetScene](GetScene.md) +- [IsEnabled](IsEnabled.md) +- [SetEnabled](SetEnabled.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) + +## 相关指南 + +- [GameObject-Component Lifecycle And Serialization](../../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [GameObject](../GameObject/GameObject.md) +- [TransformComponent](../TransformComponent/TransformComponent.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md b/docs/api/XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md index cfb0c361..bbfb8b5d 100644 --- a/docs/api/XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md +++ b/docs/api/XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md @@ -2,33 +2,119 @@ **命名空间**: `XCEngine::Components` -**类型**: `class (singleton)` +**类型**: `class` **头文件**: `XCEngine/Components/ComponentFactoryRegistry.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `ComponentFactoryRegistry` public API。 +**描述**: 维护“组件类型名 -> 创建函数”的全局注册表,供场景反序列化按类型名恢复组件实例。 -## 概述 +## 角色概述 -`ComponentFactoryRegistry.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`ComponentFactoryRegistry` 是 `Components` 模块和场景文本反序列化之间的桥梁。 -## 声明概览 +当 [Scene::DeserializeFromString](../../Scene/Scene/DeserializeFromString.md) 读到类似下面的记录时: -| 声明 | 类型 | 说明 | -|------|------|------| -| `ComponentFactoryRegistry` | `class` | 头文件中的公开声明。 | +```text +component=Camera;projection=0;fov=60;... +``` -## 公共方法 +它需要先回答一个问题: `"Camera"` 应该创建成什么 C++ 类型? -| 方法 | 描述 | -|------|------| -| [Get](Get.md) | 获取相关状态或对象。 | -| [RegisterFactory](RegisterFactory.md) | 注册对象、回调或映射。 | -| [CreateComponent](CreateComponent.md) | 创建新对象或资源。 | -| [IsRegistered](IsRegistered.md) | 查询当前状态。 | -| [GetRegisteredTypes](GetRegisteredTypes.md) | 获取相关状态或对象。 | +当前答案就是交给 `ComponentFactoryRegistry`。它保存组件类型名与工厂函数的映射,并按需调用 `GameObject::AddComponent()` 创建实例。 + +## 当前实现行为 + +### 1. 这是一个单例式全局注册表 + +- 使用 [Get](Get.md) 获取全局唯一实例 +- 构造函数里完成内建组件的预注册 +- 存储结构是 `unordered_map` 加一个注册顺序数组 + +### 2. 当前预注册组件 + +按 `engine/src/Components/ComponentFactoryRegistry.cpp` 当前实现,默认注册了: + +- `Camera` +- `Light` +- `AudioSource` +- `AudioListener` +- `MeshFilter` +- `MeshRenderer` +- `ScriptComponent` + +`Transform` 没有注册,这不是遗漏,而是当前设计决定:`TransformComponent` 由 `GameObject` 构造时内建创建,不通过工厂补建。 + +### 3. 工厂真正做的事情 + +内建工厂函数最终调用的是: + +`gameObject->AddComponent()` + +所以它继承了 `GameObject::AddComponent()` 的所有现实语义,包括: + +- 只负责创建并挂接组件 +- 不会自动补发 `Awake()`、`Start()` 或 `OnEnable()` + +这一点对理解场景加载后的生命周期非常关键。 + +## 注册顺序语义 + +`RegisterFactory()` 当前使用 `insert_or_assign`: + +- 新类型会加入 `m_registrationOrder` +- 已存在类型会被替换工厂函数 +- 替换已有类型时不会重复追加到顺序数组 + +因此 [GetRegisteredTypes](GetRegisteredTypes.md) 返回的是“首次注册顺序”,不是 map key 的排序结果。 + +## 与 Scene 反序列化的关系 + +当前 `Scene::DeserializeFromString()` 的流程大致是: + +1. 创建 `GameObject` +2. 读出 `transform` payload,恢复内建 `Transform` +3. 对每条 `component=;payload` 记录调用 `ComponentFactoryRegistry::CreateComponent()` +4. 让新组件执行各自的 `Deserialize()` +5. 最后再恢复父子关系 + +这条链路解释了两个常见现象: + +- 只有已注册组件类型才能从场景文本成功还原 +- 反序列化路径并不会自动把组件送入完整运行时生命周期 + +## 线程语义 + +- 当前实现没有加锁。 +- 注册和查询都应视为非线程安全操作。 +- 典型使用方式是在初始化阶段完成注册,在场景加载阶段只读查询。 + +## 当前实现限制 + +- 只支持基于字符串类型名的简单工厂映射,没有版本迁移、字段升级或 schema 管理层。 +- 反序列化后不会自动补发 `Awake()`/`Start()`。 +- 没有发现针对自定义组件热注册冲突、卸载或线程安全的额外保护。 + +## 推荐使用方式 + +1. 内建组件依赖默认注册即可。 +2. 自定义组件如果要参与场景反序列化,必须在加载前完成注册。 +3. 不要把它当成完整反射系统;它只是“名字到构造函数”的桥接层。 + +## 相关方法 + +- [Get](Get.md) +- [RegisterFactory](RegisterFactory.md) +- [CreateComponent](CreateComponent.md) +- [IsRegistered](IsRegistered.md) +- [GetRegisteredTypes](GetRegisteredTypes.md) + +## 相关指南 + +- [GameObject-Component Lifecycle And Serialization](../../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [GameObject](../GameObject/GameObject.md) +- [Scene](../../Scene/Scene/Scene.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/Components.md b/docs/api/XCEngine/Components/Components.md index a1ca6d33..950c2716 100644 --- a/docs/api/XCEngine/Components/Components.md +++ b/docs/api/XCEngine/Components/Components.md @@ -4,26 +4,61 @@ **类型**: `module` -**描述**: 组件系统与游戏对象 API。 +**描述**: 提供 `GameObject`/`Component` 组合式对象模型,以及相机、灯光、网格渲染、音频等场景组件的 public API。 ## 概览 -该目录与 `XCEngine/Components` 对应的 public headers 保持平行,用于承载唯一的 canonical API 文档入口。 +`XCEngine::Components` 是当前引擎运行时对象模型的核心入口。它不是 ECS 风格的数据块系统,而是更接近 Unity 的 `GameObject + Component` 组织方式: + +- `GameObject` 负责对象身份、父子层级、激活状态和生命周期分发。 +- `TransformComponent` 是每个对象自带的内建组件,负责局部/世界变换与层级矩阵计算。 +- 其他组件按职责挂接到 `GameObject` 上,例如相机、灯光、网格与音频。 + +这一设计的好处是上手门槛低,场景树、组件组合和序列化边界都比较直观,适合编辑器、脚本层和运行时调试共同使用。代价是对象关系、生命周期和序列化逻辑会集中在 `GameObject`/`Scene` 这条主线上,文档必须把这些边界讲清楚,否则很容易误用。 + +## 设计要点 + +- 当前实现以场景树为中心,而不是以查询驱动的 ECS 为中心。 +- `TransformComponent` 不是普通可选组件,而是 `GameObject` 构造时就创建的内建组件。 +- 生命周期由 `GameObject` 和 `Scene` 驱动,运行时直接 `AddComponent()` 并不会自动补发 `Awake`、`Start` 或 `OnEnable`。 +- 序列化职责分层:`GameObject::Serialize()` 只写对象基础字段和 `Transform`,完整场景保存由 `Scene::SerializeToString()` 负责组件和父子关系。 +- `ComponentFactoryRegistry` 只负责把序列化文本里的组件类型名还原为内建组件实例,`Transform` 不走这条路径。 + +## 适用场景 + +- 构建编辑器可见的场景对象树。 +- 以组合方式挂接渲染、音频或脚本行为。 +- 让脚本层或工具层以 Unity 类似的心智模型操作对象。 +- 做轻量级场景保存/加载和运行时对象遍历。 + +## 当前实现边界 + +- 静态查找接口 `GameObject::Find()`、`FindObjectsOfType()`、`FindGameObjectsWithTag()` 只会看到已注册到全局表的对象;普通 `GameObject` 直接构造后并不会自动注册。 +- `FindGameObjectsWithTag()` 当前按对象名称比较,不是真正的 tag 系统。 +- 某些组件已经具备基础运行时行为,但序列化覆盖并不完整,例如音频组件当前没有自定义 `Serialize()`/`Deserialize()`。 +- 一些 API 名义上接近 Unity 对应物,但当前实现能力更轻量,不能直接按“完整 Unity 行为”等同理解。 ## 头文件 -- [AudioListenerComponent](AudioListenerComponent/AudioListenerComponent.md) - `AudioListenerComponent.h` -- [AudioSourceComponent](AudioSourceComponent/AudioSourceComponent.md) - `AudioSourceComponent.h` -- [CameraComponent](CameraComponent/CameraComponent.md) - `CameraComponent.h` -- [Component](Component/Component.md) - `Component.h` -- [ComponentFactoryRegistry](ComponentFactoryRegistry/ComponentFactoryRegistry.md) - `ComponentFactoryRegistry.h` -- [GameObject](GameObject/GameObject.md) - `GameObject.h` -- [LightComponent](LightComponent/LightComponent.md) - `LightComponent.h` -- [MeshFilterComponent](MeshFilterComponent/MeshFilterComponent.md) - `MeshFilterComponent.h` -- [MeshRendererComponent](MeshRendererComponent/MeshRendererComponent.md) - `MeshRendererComponent.h` -- [TransformComponent](TransformComponent/TransformComponent.md) - `TransformComponent.h` +- [AudioListenerComponent](AudioListenerComponent/AudioListenerComponent.md) - `AudioListenerComponent.h`,监听器状态与全局音频监听参数。 +- [AudioSourceComponent](AudioSourceComponent/AudioSourceComponent.md) - `AudioSourceComponent.h`,音频源播放、解码与混音入口。 +- [CameraComponent](CameraComponent/CameraComponent.md) - `CameraComponent.h`,相机投影与清屏参数。 +- [Component](Component/Component.md) - `Component.h`,所有组件的抽象基类。 +- [ComponentFactoryRegistry](ComponentFactoryRegistry/ComponentFactoryRegistry.md) - `ComponentFactoryRegistry.h`,场景反序列化时的组件工厂注册表。 +- [GameObject](GameObject/GameObject.md) - `GameObject.h`,对象身份、层级和生命周期分发中心。 +- [LightComponent](LightComponent/LightComponent.md) - `LightComponent.h`,基础灯光参数。 +- [MeshFilterComponent](MeshFilterComponent/MeshFilterComponent.md) - `MeshFilterComponent.h`,网格资源引用。 +- [MeshRendererComponent](MeshRendererComponent/MeshRendererComponent.md) - `MeshRendererComponent.h`,材质槽和渲染附加参数。 +- [TransformComponent](TransformComponent/TransformComponent.md) - `TransformComponent.h`,局部/世界变换与层级矩阵。 + +## 推荐阅读顺序 + +1. 先读 [GameObject-Component Lifecycle And Serialization](../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md),建立整体心智模型。 +2. 再读 [GameObject](GameObject/GameObject.md) 和 [TransformComponent](TransformComponent/TransformComponent.md),理解对象树和变换树的关系。 +3. 最后按功能阅读具体组件页,例如 [CameraComponent](CameraComponent/CameraComponent.md)、[MeshRendererComponent](MeshRendererComponent/MeshRendererComponent.md)、[AudioSourceComponent](AudioSourceComponent/AudioSourceComponent.md)。 ## 相关文档 - [上级目录](../XCEngine.md) +- [Scene](../Scene/Scene.md) - 场景对象的创建、更新和完整序列化入口。 - [API 总索引](../../main.md) diff --git a/docs/api/XCEngine/Components/GameObject/GameObject.md b/docs/api/XCEngine/Components/GameObject/GameObject.md index bd9dc647..26c0b44c 100644 --- a/docs/api/XCEngine/Components/GameObject/GameObject.md +++ b/docs/api/XCEngine/Components/GameObject/GameObject.md @@ -6,61 +6,196 @@ **头文件**: `XCEngine/Components/GameObject.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `GameObject` public API。 +**描述**: 场景对象树的基础节点,负责对象身份、父子层级、激活状态、组件容器和生命周期分发。 -## 概述 +## 角色概述 -`GameObject.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`GameObject` 是当前引擎对象模型的中心类型。你可以把它理解成一个“可挂组件的层级节点”: -## 声明概览 +- 它自身保存 `name`、`id`、`uuid`、active 状态、父子关系和所属 `Scene`。 +- 它始终自带一个 `TransformComponent`。 +- 它通过模板 `AddComponent()`/`GetComponent()` 组织附加组件。 +- 它负责把生命周期回调分发给已挂接组件。 -| 声明 | 类型 | 说明 | -|------|------|------| -| `GameObject` | `class` | 头文件中的公开声明。 | +这个设计明显接近 Unity 的 `GameObject` 模式,优点是对象树和组件树可以直接映射到编辑器层级与脚本使用习惯;代价是很多行为依赖“对象是否由 `Scene` 托管”,而不是只看对象本身。 -## 公共方法 +## 内建 Transform 语义 -| 方法 | 描述 | -|------|------| -| [GameObject()](Constructor.md) | 构造对象。 | -| [~GameObject()](Destructor.md) | 销毁对象并释放相关资源。 | -| [GetID](GetID.md) | 获取相关状态或对象。 | -| [GetUUID](GetUUID.md) | 获取相关状态或对象。 | -| [GetName](GetName.md) | 获取相关状态或对象。 | -| [SetName](SetName.md) | 设置相关状态或配置。 | -| [DetachFromParent](DetachFromParent.md) | 公开方法,详见头文件声明。 | -| [GetScene](GetScene.md) | 获取相关状态或对象。 | -| [GetTransform](GetTransform.md) | 获取相关状态或对象。 | -| [AddComponent](AddComponent.md) | 添加元素或建立关联。 | -| [GetComponent](GetComponent.md) | 获取相关状态或对象。 | -| [GetComponents](GetComponents.md) | 获取相关状态或对象。 | -| [RemoveComponent](RemoveComponent.md) | 移除元素或解除关联。 | -| [GetComponentInChildren](GetComponentInChildren.md) | 获取相关状态或对象。 | -| [GetComponentInParent](GetComponentInParent.md) | 获取相关状态或对象。 | -| [GetComponentsInChildren](GetComponentsInChildren.md) | 获取相关状态或对象。 | -| [GetParent](GetParent.md) | 获取相关状态或对象。 | -| [SetParent](SetParent.md) | 设置相关状态或配置。 | -| [GetChildCount](GetChildCount.md) | 获取相关状态或对象。 | -| [GetChild](GetChild.md) | 获取相关状态或对象。 | -| [GetChildren](GetChildren.md) | 获取相关状态或对象。 | -| [DetachChildren](DetachChildren.md) | 公开方法,详见头文件声明。 | -| [IsActive](IsActive.md) | 查询当前状态。 | -| [SetActive](SetActive.md) | 设置相关状态或配置。 | -| [IsActiveInHierarchy](IsActiveInHierarchy.md) | 查询当前状态。 | -| [Find](Find.md) | 查找并返回匹配对象。 | -| [FindObjectsOfType](FindObjectsOfType.md) | 查找并返回匹配对象。 | -| [FindGameObjectsWithTag](FindGameObjectsWithTag.md) | 查找并返回匹配对象。 | -| [Awake](Awake.md) | 公开方法,详见头文件声明。 | -| [Start](Start.md) | 公开方法,详见头文件声明。 | -| [Update](Update.md) | 更新运行时状态。 | -| [FixedUpdate](FixedUpdate.md) | 公开方法,详见头文件声明。 | -| [LateUpdate](LateUpdate.md) | 公开方法,详见头文件声明。 | -| [OnDestroy](OnDestroy.md) | 公开方法,详见头文件声明。 | -| [Destroy](Destroy.md) | 公开方法,详见头文件声明。 | -| [Serialize](Serialize.md) | 公开方法,详见头文件声明。 | -| [Deserialize](Deserialize.md) | 公开方法,详见头文件声明。 | +`GameObject` 构造函数会直接 `new TransformComponent()`,并把该组件绑定为内建 `m_transform`。这带来几个重要结果: + +- 每个 `GameObject` 天生就有 `TransformComponent`,不需要也不应该自己创建。 +- `AddComponent()` 不会生成第二个 `Transform`,而是直接返回内建的那个实例。 +- `RemoveComponent()` 明确禁止移除 `Transform`。 + +如果你把这个模块当成 Unity 风格对象树来理解,这一行为是合理的;但它与“所有组件都等价可增删”的纯组合系统不同,文档中必须明确区分。 + +## 场景托管与独立对象 + +`GameObject` 有两种常见存在方式: + +### 1. 独立构造 + +直接调用 `GameObject go;` 或 `GameObject go("Name");` + +- 会创建对象和内建 `Transform`。 +- 不会自动注册到全局查找表。 +- `m_scene` 为空。 +- 不会自动调用 `Awake()`。 + +这类对象适合单元测试或临时对象操作,但不要把它和“已经加入场景”的对象等同。 + +### 2. 由 `Scene::CreateGameObject()` 创建 + +这是当前运行时的推荐路径。 + +- `Scene` 会把对象放进场景拥有的 `unique_ptr` 容器。 +- 对象会注册到全局 registry。 +- `m_scene` 被设置为所属场景。 +- 如果指定父对象,会接入场景层级。 +- 创建结束后会立即调用 `Awake()`。 + +因此,静态查找接口和完整生命周期语义,本质上都更偏向“场景托管对象”。 + +## 生命周期 + +`GameObject` 自己不派生自 `Component`,但它是组件生命周期的分发器。 + +- `Awake()` 会遍历普通组件并调用对应钩子。 +- `Start()` 只在对象处于 `active in hierarchy` 时执行;首次执行后会把 `m_started` 置为 true,之后不再重复调用。 +- `Update()`、`FixedUpdate()`、`LateUpdate()` 只在对象处于激活层级时向已启用组件分发,并递归处理子对象。 +- `OnDestroy()` 会把销毁消息发给普通组件。 + +注意两个当前实现特征: + +- `TransformComponent` 不在 `m_components` 容器中,因此这些遍历不会覆盖 `Transform`。 +- `AddComponent()` 只是创建并挂接组件,不会自动调用 `Awake()`、`Start()` 或 `OnEnable()`。 +- 如果 `GameObject` 已经进入过 `Start()` 阶段,后续再动态添加组件时,新的组件也不会在未来被自动补发 `Start()`,因为 `m_started` 已经锁定为 true。 + +这意味着运行时动态加组件时,当前行为比 Unity 更原始,使用者需要自己控制初始化时机。 + +## 激活状态与层级传播 + +`GameObject` 同时维护: + +- `m_activeSelf`:对象自身开关,对应 `IsActive()` +- 层级有效激活态:父链都激活时才为 true,对应 `IsActiveInHierarchy()` + +`SetActive()` 和 `SetParent()` 都会计算层级激活态是否发生变化;如果发生变化,会: + +- 给当前对象上已启用组件发送 `OnEnable()` 或 `OnDisable()` +- 递归向子对象传播 + +这一行为已经由 `tests/Components/test_game_object.cpp` 和 `tests/Components/test_component.cpp` 覆盖。 + +## 查找与全局注册表 + +静态接口: + +- `Find(const std::string&)` +- `FindObjectsOfType()` +- `FindGameObjectsWithTag(const std::string&)` + +它们都依赖 `GameObject::GetGlobalRegistry()`。按当前实现,这个 registry 主要在以下路径填充: + +- `Scene::CreateGameObject()` +- `Scene::DeserializeFromString()` + +普通构造出来但未加入场景的对象,不会自动出现在这些查找结果里。 + +另外,`FindGameObjectsWithTag()` 当前实际上比较的是对象名称,而不是独立的 tag 字段。这是一个重要的实现限制,文档和上层调用都不应假定已经存在真正的 tag 系统。 + +## 所有权与销毁 + +### 对象所有权 + +- 场景托管对象由 `Scene` 通过 `std::unique_ptr` 持有。 +- 普通组件由 `GameObject` 通过 `std::unique_ptr` 持有。 +- `TransformComponent` 单独由裸指针 `m_transform` 持有,并在析构中 `delete`。 + +### `Destroy()` 的当前行为 + +- 如果对象属于某个 `Scene`,`Destroy()` 会委托给 `Scene::DestroyGameObject(this)`。 +- 如果对象不属于场景,`Destroy()` 只会调用 `OnDestroy()`,不会 `delete this`。 + +这和很多用户的直觉并不完全一致。对独立对象来说,`Destroy()` 更像“发送销毁事件”,不是“释放对象内存”。 + +另外,析构函数本身不会主动调用 `OnDestroy()`;它只会删除内建 `Transform` 并清空组件容器。所以不要依赖析构语义去替代显式销毁回调。 + +## 序列化边界 + +`GameObject::Serialize()` 当前只写入: + +- `name` +- `active` +- `id` +- `uuid` +- `transform` + +它不会负责写出普通组件列表,也不会负责写出父子关系。完整场景序列化是在 `Scene::SerializeToString()` 中递归完成的,那里才会: + +- 输出 `parent` +- 遍历普通组件并写入 `component=;payload` +- 递归所有子对象 + +因此,单独调用 `GameObject::Serialize()` 更适合基础对象状态转储,而不是完整 prefab/scene 快照。 + +还要注意一个当前版本的生命周期差异:`Scene::DeserializeFromString()` 在重建对象和组件后,并不会自动调用 `Awake()`。也就是说,场景加载恢复出的对象和通过 `Scene::CreateGameObject()` 新建出来的对象,在初始化阶段语义上并不完全一致。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 父子层级、组件容器和全局 registry 都不是线程安全容器。 +- 默认应按主线程场景更新路径使用。 + +## 当前实现限制 + +- 没有真正的 tag 系统,`FindGameObjectsWithTag()` 只是按名称匹配。 +- `AddComponent()` 不会自动驱动新组件进入完整生命周期。 +- 已经 `Start()` 过的对象在运行时新增组件后,新组件不会被自动 `Start()`。 +- 静态查找接口只对已注册对象生效,独立构造对象默认不可见。 +- `Destroy()` 对独立对象不会释放自身内存。 +- `Transform` 不会被普通组件遍历逻辑自动包含在生命周期分发中。 +- 通过 `Scene::DeserializeFromString()` 恢复的对象当前不会自动补发 `Awake()`。 + +## 推荐使用方式 + +1. 运行时对象优先通过 `Scene::CreateGameObject()` 创建,而不是直接构造裸对象。 +2. 操作父子关系优先使用 `GameObject::SetParent()`,而不是只改 `TransformComponent::SetParent()`。 +3. 需要保存完整对象树时,优先走 `Scene::SerializeToString()` / `Scene::DeserializeFromString()`。 +4. 运行时动态挂组件时,不要假定引擎已经自动补发 `Awake`/`Start`。 +5. 场景反序列化后如果某些组件依赖 `Awake()` 完成初始化,需要显式检查当前加载路径是否已补齐这一步。 + +## 相关方法 + +- [Constructor](Constructor.md) +- [AddComponent](AddComponent.md) +- [GetComponent](GetComponent.md) +- [GetComponents](GetComponents.md) +- [GetComponentInChildren](GetComponentInChildren.md) +- [GetComponentInParent](GetComponentInParent.md) +- [GetComponentsInChildren](GetComponentsInChildren.md) +- [GetTransform](GetTransform.md) +- [SetParent](SetParent.md) +- [DetachFromParent](DetachFromParent.md) +- [DetachChildren](DetachChildren.md) +- [SetActive](SetActive.md) +- [IsActive](IsActive.md) +- [IsActiveInHierarchy](IsActiveInHierarchy.md) +- [Find](Find.md) +- [FindObjectsOfType](FindObjectsOfType.md) +- [FindGameObjectsWithTag](FindGameObjectsWithTag.md) +- [Destroy](Destroy.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) + +## 相关指南 + +- [GameObject-Component Lifecycle And Serialization](../../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [Component](../Component/Component.md) +- [TransformComponent](../TransformComponent/TransformComponent.md) +- [Scene](../../Scene/Scene/Scene.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/LightComponent/LightComponent.md b/docs/api/XCEngine/Components/LightComponent/LightComponent.md index 8bf26ccf..680e56a6 100644 --- a/docs/api/XCEngine/Components/LightComponent/LightComponent.md +++ b/docs/api/XCEngine/Components/LightComponent/LightComponent.md @@ -6,40 +6,107 @@ **头文件**: `XCEngine/Components/LightComponent.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `LightComponent` public API。 +**描述**: 保存基础灯光类型、颜色、强度、范围、聚光角和阴影标记等参数。 -## 概述 +## 角色概述 -`LightComponent.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`LightComponent` 是当前对象系统中的基础光源配置组件。和 [CameraComponent](../CameraComponent/CameraComponent.md) 类似,它本身是轻量数据容器: -## 声明概览 +- 它不拥有 GPU 资源 +- 它不负责生成阴影贴图 +- 它不直接执行光照计算 -| 声明 | 类型 | 说明 | -|------|------|------| -| `LightType` | `enum class` | 头文件中的公开声明。 | -| `LightComponent` | `class` | 继承自 `Component` 的公开声明。 | +它的价值在于把“这个对象应被解释成什么灯光”这件事挂在 `GameObject` 上,便于场景、编辑器和后续渲染管线统一消费。 -## 公共方法 +## 当前实现行为 -| 方法 | 描述 | -|------|------| -| [GetName](GetName.md) | 获取相关状态或对象。 | -| [GetLightType](GetLightType.md) | 获取相关状态或对象。 | -| [SetLightType](SetLightType.md) | 设置相关状态或配置。 | -| [GetColor](GetColor.md) | 获取相关状态或对象。 | -| [SetColor](SetColor.md) | 设置相关状态或配置。 | -| [GetIntensity](GetIntensity.md) | 获取相关状态或对象。 | -| [SetIntensity](SetIntensity.md) | 设置相关状态或配置。 | -| [GetRange](GetRange.md) | 获取相关状态或对象。 | -| [SetRange](SetRange.md) | 设置相关状态或配置。 | -| [GetSpotAngle](GetSpotAngle.md) | 获取相关状态或对象。 | -| [SetSpotAngle](SetSpotAngle.md) | 设置相关状态或配置。 | -| [GetCastsShadows](GetCastsShadows.md) | 获取相关状态或对象。 | -| [SetCastsShadows](SetCastsShadows.md) | 设置相关状态或配置。 | -| [Serialize](Serialize.md) | 公开方法,详见头文件声明。 | -| [Deserialize](Deserialize.md) | 公开方法,详见头文件声明。 | +### 1. 这是一个可序列化的数据组件 + +当前实现提供: + +- 灯光类型 `LightType` +- 颜色 `Color` +- 强度 `Intensity` +- 范围 `Range` +- 聚光角 `SpotAngle` +- 阴影标记 `CastsShadows` +- 文本序列化/反序列化 + +没有 `Update()`,也没有直接面向渲染后端的执行逻辑。 + +### 2. 关键数值会被 clamp + +按当前 `.cpp` 实现: + +- `SetIntensity()` 限制为 `>= 0.0f` +- `SetRange()` 限制为 `>= 0.001f` +- `SetSpotAngle()` 限制为 `1.0f ~ 179.0f` + +这些行为已被 `tests/Components/test_camera_light_component.cpp` 覆盖。 + +## 设计理解 + +把灯光做成组件,而不是做成一个脱离对象树的单独系统,有几个直接好处: + +- 可以直接复用 `TransformComponent` 决定光源位置和朝向 +- 编辑器层级里“哪个对象是灯”非常直观 +- 场景保存/加载时,灯光和对象树天然同生命周期 + +这也是多数商业引擎采用的思路。 + +## 与 Transform 的关系 + +`LightComponent` 自身不保存位置与朝向。当前 API 的空间语义来自宿主对象: + +- `Directional` 灯通常依赖对象旋转表示方向 +- `Point` / `Spot` 灯通常依赖对象位置 + +因此在上层使用时,应该把灯光参数和宿主 `Transform` 一起看待,而不是孤立看这个组件。 + +## 序列化语义 + +当前会写出: + +- `type` +- `color` +- `intensity` +- `range` +- `spotAngle` +- `shadows` + +这意味着 `LightComponent` 已属于当前场景系统中可以完整持久化的内建组件。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 默认应在主线程配置路径使用。 + +## 当前实现限制 + +- 这里只描述光源参数,不代表当前渲染管线已经完整消费了所有高级灯光特性。 +- 文档没有把它写成“完整商业级光照系统”,因为从当前代码看,它更接近基础参数容器。 +- 没有在当前取证范围里看到专门的阴影系统或复杂光照调度与它强绑定。 + +## 相关方法 + +- [GetLightType](GetLightType.md) +- [SetLightType](SetLightType.md) +- [GetColor](GetColor.md) +- [SetColor](SetColor.md) +- [GetIntensity](GetIntensity.md) +- [SetIntensity](SetIntensity.md) +- [GetRange](GetRange.md) +- [SetRange](SetRange.md) +- [GetSpotAngle](GetSpotAngle.md) +- [SetSpotAngle](SetSpotAngle.md) +- [GetCastsShadows](GetCastsShadows.md) +- [SetCastsShadows](SetCastsShadows.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [GameObject](../GameObject/GameObject.md) +- [TransformComponent](../TransformComponent/TransformComponent.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/MeshFilterComponent/MeshFilterComponent.md b/docs/api/XCEngine/Components/MeshFilterComponent/MeshFilterComponent.md index fbe558c8..85b295ce 100644 --- a/docs/api/XCEngine/Components/MeshFilterComponent/MeshFilterComponent.md +++ b/docs/api/XCEngine/Components/MeshFilterComponent/MeshFilterComponent.md @@ -6,42 +6,106 @@ **头文件**: `XCEngine/Components/MeshFilterComponent.h` -**描述**: 持有一个 `Mesh` 资源引用,负责告诉渲染系统“这个对象要画哪一个网格”。 +**描述**: 保存网格资源引用及其路径,让场景对象能够声明“我使用哪一个 Mesh”。 -## 概述 +## 角色概述 -`MeshFilterComponent` 和 [MeshRendererComponent](../MeshRendererComponent/MeshRendererComponent.md) 的拆分,很像 Unity 经典的 `MeshFilter + MeshRenderer` 设计: +`MeshFilterComponent` 负责“几何数据来自哪里”,而不是“怎么绘制”。在当前对象模型里: -- `MeshFilterComponent` 决定几何体是什么。 -- `MeshRendererComponent` 决定材质槽和渲染设置是什么。 +- `MeshFilterComponent` 提供网格 +- [MeshRendererComponent](../MeshRendererComponent/MeshRendererComponent.md) 提供材质和渲染附加配置 +- 两者一起被 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 消费 -这种拆分的好处是很明确的: +这和 Unity 里 `MeshFilter` / `MeshRenderer` 分工的思路是一致的,优点是几何与渲染配置解耦,便于编辑器或脚本单独替换。 -- 同一个 mesh 可以搭配不同材质策略。 -- 渲染系统在提取阶段可以清楚地区分“几何数据”和“绘制配置”。 +## 当前实现行为 -## 当前实现边界 +### 1. 同时保存资源句柄和资源路径 -- 内部同时保存 `ResourceHandle` 和一个字符串路径。 -- [SetMesh](SetMesh.md) 会同步更新 `m_meshPath`。 -- [ClearMesh](ClearMesh.md) 会同时清空 handle 和路径。 -- [Deserialize](Deserialize.md) 会尝试通过 `ResourceManager` 重新加载 mesh。 +内部状态有两份: -## 公开方法 +- `m_mesh`:`ResourceHandle` +- `m_meshPath`:`std::string` -| 方法 | 说明 | -|------|------| -| [GetName](GetName.md) | 返回组件名字。 | -| [GetMesh](GetMesh.md) | 获取当前 mesh 指针。 | -| [GetMeshHandle](GetMeshHandle.md) | 获取当前 mesh handle。 | -| [GetMeshPath](GetMeshPath.md) | 获取当前 mesh 路径。 | -| [SetMesh](SetMesh.md) | 设置 mesh 资源。 | -| [ClearMesh](ClearMesh.md) | 清空当前 mesh。 | -| [Serialize](Serialize.md) | 序列化 mesh 路径。 | -| [Deserialize](Deserialize.md) | 反序列化并尝试重新加载 mesh。 | +这样设计的直接收益是: + +- 运行时可以快速拿到已加载资源 +- 序列化时又能稳定写出路径 + +### 2. `SetMeshPath()` 会尝试加载,但即使加载失败也保留路径 + +按当前实现: + +- 传入空路径时会清空 handle 和 path +- 传入非空路径时会调用 `ResourceManager::Get().Load(...)` +- 无论资源是否成功加载,`m_meshPath` 都会先被更新 + +这是一条很重要的行为约定。它意味着: + +- 文档页或编辑器可以先持有“用户想要的路径” +- 即使资源此刻没加载出来,序列化信息也不会丢 + +这一点已经被 `tests/Components/test_mesh_render_components.cpp` 覆盖。 + +### 3. 直接设置 handle 时会反向推导路径 + +`SetMesh(const ResourceHandle&)` 会: + +- 保存 handle +- 如果 handle 非空,则从 `mesh->GetPath()` 反推出 `m_meshPath` +- 如果 handle 为空,则清空路径 + +这保证了路径和运行时资源引用不会长期脱节。 + +## 序列化语义 + +当前序列化只写一个字段: + +- `mesh=` + +反序列化时会先清空旧状态,再通过 `SetMeshPath()` 恢复路径并尝试加载资源。 + +这意味着: + +- 场景文件的真实持久化载体是路径 +- 运行时 handle 是可重建缓存,而不是主数据 + +## 与渲染提取的关系 + +`RenderSceneExtractor` 只有在以下条件都满足时才会把对象当成可见渲染项: + +- `GameObject` 处于激活层级 +- 同时存在 `MeshFilterComponent` 和 `MeshRendererComponent` +- 两个组件都已启用 +- `MeshFilterComponent::GetMesh()` 返回非空且 `mesh->IsValid()` + +因此,只有 `MeshFilterComponent` 还不够,它必须和 `MeshRendererComponent` 配对使用。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 资源加载调用走 `ResourceManager`,但本组件文档不提供额外线程安全保证。 + +## 当前实现限制 + +- 它只负责引用网格,不处理子网格裁剪、LOD 或网格 streaming 策略。 +- 场景保存后真正持久化的是路径,不是已经加载好的资源实例。 +- 当前目录下只有 [SetMesh](SetMesh.md) 方法页,`SetMeshPath()` 也是 public API,应优先参考本页的行为说明。 + +## 相关方法 + +- [GetMesh](GetMesh.md) +- [GetMeshHandle](GetMeshHandle.md) +- [GetMeshPath](GetMeshPath.md) +- [SetMesh](SetMesh.md) +- [SetMeshPath](SetMeshPath.md) +- [ClearMesh](ClearMesh.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) ## 相关文档 -- [Components](../Components.md) +- [当前模块](../Components.md) - [MeshRendererComponent](../MeshRendererComponent/MeshRendererComponent.md) - [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/MeshFilterComponent/SetMeshPath.md b/docs/api/XCEngine/Components/MeshFilterComponent/SetMeshPath.md new file mode 100644 index 00000000..c99697c1 --- /dev/null +++ b/docs/api/XCEngine/Components/MeshFilterComponent/SetMeshPath.md @@ -0,0 +1,30 @@ +# SetMeshPath + +**所属类型**: [MeshFilterComponent](MeshFilterComponent.md) + +## 签名 + +```cpp +void SetMeshPath(const std::string& meshPath); +``` + +## 作用 + +按资源路径声明当前组件应引用哪个 `Mesh`,并尝试通过 `ResourceManager` 立即加载该资源。 + +## 当前实现行为 + +- 先把 `m_meshPath` 更新为传入值。 +- 如果路径为空,清空当前 `m_mesh` handle 并返回。 +- 如果路径非空,调用 `ResourceManager::Get().Load(meshPath.c_str())`。 +- 即使加载失败,路径也会被保留下来。 + +## 使用建议 + +这是编辑器或序列化恢复路径更应该使用的接口,因为它保留了“目标资源路径”这一主数据。 + +## 相关文档 + +- [MeshFilterComponent](MeshFilterComponent.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) diff --git a/docs/api/XCEngine/Components/MeshRendererComponent/GetMaterialPath.md b/docs/api/XCEngine/Components/MeshRendererComponent/GetMaterialPath.md new file mode 100644 index 00000000..c09d8544 --- /dev/null +++ b/docs/api/XCEngine/Components/MeshRendererComponent/GetMaterialPath.md @@ -0,0 +1,28 @@ +# GetMaterialPath + +**所属类型**: [MeshRendererComponent](MeshRendererComponent.md) + +## 签名 + +```cpp +const std::string& GetMaterialPath(size_t index) const; +``` + +## 作用 + +返回指定材质槽当前记录的资源路径字符串。 + +## 当前实现行为 + +- 如果 `index` 在范围内,返回 `m_materialPaths[index]`。 +- 如果越界,返回一个静态空字符串引用,而不是抛异常。 + +## 使用建议 + +这个接口更适合做编辑器显示、序列化检查和路径级调试;真正的运行时材质对象访问仍应优先看 [GetMaterial](GetMaterial.md) 或 [GetMaterialHandle](GetMaterialHandle.md)。 + +## 相关文档 + +- [MeshRendererComponent](MeshRendererComponent.md) +- [GetMaterial](GetMaterial.md) +- [SetMaterialPath](SetMaterialPath.md) diff --git a/docs/api/XCEngine/Components/MeshRendererComponent/MeshRendererComponent.md b/docs/api/XCEngine/Components/MeshRendererComponent/MeshRendererComponent.md index 891569cd..903290dd 100644 --- a/docs/api/XCEngine/Components/MeshRendererComponent/MeshRendererComponent.md +++ b/docs/api/XCEngine/Components/MeshRendererComponent/MeshRendererComponent.md @@ -6,50 +6,125 @@ **头文件**: `XCEngine/Components/MeshRendererComponent.h` -**描述**: 保存材质槽、阴影标志和渲染层信息,负责告诉渲染系统“这个 mesh 应该如何被绘制”。 +**描述**: 保存材质槽、阴影开关和渲染层等绘制配置,声明“这个对象上的 Mesh 应该如何被渲染”。 -## 概述 +## 角色概述 -`MeshRendererComponent` 承担的是“绘制配置”这一半职责: +`MeshRendererComponent` 负责的是“绘制配置”,不是“几何来源”。 -- 材质槽数组 -- 阴影开关 -- 渲染层 +- [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md) 负责网格 +- `MeshRendererComponent` 负责材质槽和渲染附加参数 -和 [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md) 配合后,场景提取器就能把几何和材质信息一起整理成 [VisibleRenderObject](../../Rendering/VisibleRenderObject/VisibleRenderObject.md)。 +两者配合后,`RenderSceneExtractor` 才能把场景对象整理成可提交到渲染管线的可见项。 -## 当前实现边界 +## 当前实现行为 -- 内部同时维护 `m_materials` 和 `m_materialPaths` 两套数组。 -- [SetMaterial](SetMaterial.md) 会在需要时自动扩容材质槽。 -- [SetMaterials](SetMaterials.md) 会整体替换材质数组。 -- [Deserialize](Deserialize.md) 会尝试通过 `ResourceManager` 重新加载材质。 -- 当前 `castShadows`、`receiveShadows` 和 `renderLayer` 还没有真正接入 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 或 [BuiltinForwardPipeline](../../Rendering/Pipelines/BuiltinForwardPipeline/BuiltinForwardPipeline.md)。 +### 1. 同时维护材质 handle 和路径数组 -## 公开方法 +内部维护两套并行数组: -| 方法 | 说明 | -|------|------| -| [GetName](GetName.md) | 返回组件名字。 | -| [GetMaterialCount](GetMaterialCount.md) | 获取材质槽数量。 | -| [GetMaterial](GetMaterial.md) | 获取指定槽位材质。 | -| [GetMaterialHandle](GetMaterialHandle.md) | 获取指定槽位材质句柄。 | -| [GetMaterialPaths](GetMaterialPaths.md) | 获取序列化路径数组。 | -| [SetMaterial](SetMaterial.md) | 设置单个材质槽。 | -| [SetMaterials](SetMaterials.md) | 批量设置材质槽。 | -| [ClearMaterials](ClearMaterials.md) | 清空所有材质槽。 | -| [GetCastShadows](GetCastShadows.md) | 查询投射阴影标志。 | -| [SetCastShadows](SetCastShadows.md) | 设置投射阴影标志。 | -| [GetReceiveShadows](GetReceiveShadows.md) | 查询接收阴影标志。 | -| [SetReceiveShadows](SetReceiveShadows.md) | 设置接收阴影标志。 | -| [GetRenderLayer](GetRenderLayer.md) | 获取渲染层。 | -| [SetRenderLayer](SetRenderLayer.md) | 设置渲染层。 | -| [Serialize](Serialize.md) | 序列化材质路径与标志位。 | -| [Deserialize](Deserialize.md) | 反序列化材质路径与标志位。 | +- `m_materials` +- `m_materialPaths` + +这和 `MeshFilterComponent` 的思路一致,目的是同时满足: + +- 运行时快速拿资源 +- 序列化时稳定写路径 + +### 2. 材质槽会按需自动扩容 + +`SetMaterialPath()` 和 `SetMaterial()` 在写入指定槽位前,都会先调用内部 `EnsureMaterialSlot(index)`。 + +因此: + +- 可以直接写入一个较大的槽位索引 +- 中间缺失槽位会被自动补成空材质 + +`tests/Components/test_mesh_render_components.cpp` 已覆盖这一行为。 + +### 3. 越界读取是“安全空值”语义 + +按当前实现: + +- `GetMaterial(index)` 越界时返回 `nullptr` +- `GetMaterialHandle(index)` 越界时返回静态空 handle +- `GetMaterialPath(index)` 越界时返回静态空字符串 + +这让上层调用少了很多显式边界判断,但也意味着调用者不能把空返回值误读成“这个槽位一定存在但内容为空”。 + +### 4. 反序列化按路径重建槽位 + +`Deserialize()` 会: + +- 先清空旧材质与标记 +- 解析 `materials=` 字段 +- 用 `|` 分隔多材质路径 +- 对每个槽位调用 `SetMaterialPath()` 尝试重新加载 + +即使资源此刻没有加载成功,路径数组也会被保留下来。这一点同样有测试覆盖。 + +## 阴影和渲染层的现实状态 + +当前组件公开了: + +- `GetCastShadows()` / `SetCastShadows()` +- `GetReceiveShadows()` / `SetReceiveShadows()` +- `GetRenderLayer()` / `SetRenderLayer()` + +但按当前 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 实现,场景提取时还没有看到这些字段被真正消费。也就是说: + +- 这些字段当前可以保存 +- 也可以被序列化 +- 但它们还没有完整接入当前已取证的渲染提取路径 + +文档必须把这一点说清楚,避免用户把“字段存在”误解成“功能已完整生效”。 + +## 序列化语义 + +当前写出: + +- `materials`,多个路径用 `|` 分隔 +- `castShadows` +- `receiveShadows` +- `renderLayer` + +测试还覆盖了“末尾空材质槽”会被保留的情况,这对编辑器材质槽 UI 很重要。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- 资源路径解析和材质切换默认按主线程配置路径使用。 + +## 当前实现限制 + +- 当前文档目录原先缺少 `GetMaterialPath()` 和 `SetMaterialPath()` 独立方法页,本轮已补齐。 +- `castShadows`、`receiveShadows` 和 `renderLayer` 还没有完整接入当前已取证的场景提取逻辑。 +- 它只负责声明绘制配置,不等于完整 renderer feature 集合。 + +## 相关方法 + +- [GetMaterialCount](GetMaterialCount.md) +- [GetMaterial](GetMaterial.md) +- [GetMaterialHandle](GetMaterialHandle.md) +- [GetMaterialPath](GetMaterialPath.md) +- [GetMaterialPaths](GetMaterialPaths.md) +- [SetMaterial](SetMaterial.md) +- [SetMaterialPath](SetMaterialPath.md) +- [SetMaterials](SetMaterials.md) +- [ClearMaterials](ClearMaterials.md) +- [GetCastShadows](GetCastShadows.md) +- [SetCastShadows](SetCastShadows.md) +- [GetReceiveShadows](GetReceiveShadows.md) +- [SetReceiveShadows](SetReceiveShadows.md) +- [GetRenderLayer](GetRenderLayer.md) +- [SetRenderLayer](SetRenderLayer.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) ## 相关文档 -- [Components](../Components.md) +- [当前模块](../Components.md) - [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md) - [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) - [BuiltinForwardPipeline](../../Rendering/Pipelines/BuiltinForwardPipeline/BuiltinForwardPipeline.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/XCEngine/Components/MeshRendererComponent/SetMaterialPath.md b/docs/api/XCEngine/Components/MeshRendererComponent/SetMaterialPath.md new file mode 100644 index 00000000..3b225052 --- /dev/null +++ b/docs/api/XCEngine/Components/MeshRendererComponent/SetMaterialPath.md @@ -0,0 +1,32 @@ +# SetMaterialPath + +**所属类型**: [MeshRendererComponent](MeshRendererComponent.md) + +## 签名 + +```cpp +void SetMaterialPath(size_t index, const std::string& materialPath); +``` + +## 作用 + +为指定材质槽设置资源路径,并尝试通过 `ResourceManager` 加载对应材质。 + +## 当前实现行为 + +- 调用内部 `EnsureMaterialSlot(index)` 自动扩容槽位数组。 +- 先记录 `m_materialPaths[index] = materialPath`。 +- 若路径为空,则重置该槽位的材质 handle。 +- 若路径非空,则调用 `ResourceManager::Get().Load(...)` 尝试加载。 +- 即使加载失败,路径也会保留。 + +## 使用建议 + +这比直接设置 handle 更适合编辑器材质面板和序列化恢复路径,因为路径才是当前场景文件的主持久化数据。 + +## 相关文档 + +- [MeshRendererComponent](MeshRendererComponent.md) +- [GetMaterialPath](GetMaterialPath.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) diff --git a/docs/api/XCEngine/Components/TransformComponent/TransformComponent.md b/docs/api/XCEngine/Components/TransformComponent/TransformComponent.md index 304f0c93..aa67127e 100644 --- a/docs/api/XCEngine/Components/TransformComponent/TransformComponent.md +++ b/docs/api/XCEngine/Components/TransformComponent/TransformComponent.md @@ -6,69 +6,154 @@ **头文件**: `XCEngine/Components/TransformComponent.h` -**描述**: 定义 `XCEngine/Components` 子目录中的 `TransformComponent` public API。 +**描述**: `GameObject` 的内建变换组件,负责局部/世界空间变换、层级矩阵计算和常见的空间操作。 -## 概述 +## 角色概述 -`TransformComponent.h` 是 `XCEngine/Components` 子目录 下的 public header,当前页面作为平行目录中的 canonical 总览,用于汇总该头文件暴露的主要声明。 +`TransformComponent` 是当前对象树里最基础、也是最特殊的组件。它既继承自 `Component`,又不属于普通“可随意添加和删除”的组件集合: -## 声明概览 +- 每个 `GameObject` 构造时都会创建一个 `TransformComponent`。 +- 普通组件通过 `Component::transform()` 获取它。 +- 场景层级和空间层级的大部分语义都围绕它展开。 -| 声明 | 类型 | 说明 | -|------|------|------| -| `Space` | `enum class` | 头文件中的公开声明。 | -| `TransformComponent` | `class` | 继承自 `Component` 的公开声明。 | +如果把整个 `Components` 模块类比成 Unity 风格对象系统,那么 `TransformComponent` 对应的就是最接近 Unity `Transform` 的那一层基础设施。 -## 公共方法 +## 局部与世界变换 -| 方法 | 描述 | -|------|------| -| [TransformComponent()](Constructor.md) | 构造对象。 | -| [~TransformComponent()](Destructor.md) | 销毁对象并释放相关资源。 | -| [GetName](GetName.md) | 获取相关状态或对象。 | -| [Serialize](Serialize.md) | 公开方法,详见头文件声明。 | -| [Deserialize](Deserialize.md) | 公开方法,详见头文件声明。 | -| [GetLocalPosition](GetLocalPosition.md) | 获取相关状态或对象。 | -| [SetLocalPosition](SetLocalPosition.md) | 设置相关状态或配置。 | -| [GetLocalRotation](GetLocalRotation.md) | 获取相关状态或对象。 | -| [SetLocalRotation](SetLocalRotation.md) | 设置相关状态或配置。 | -| [GetLocalScale](GetLocalScale.md) | 获取相关状态或对象。 | -| [SetLocalScale](SetLocalScale.md) | 设置相关状态或配置。 | -| [GetLocalEulerAngles](GetLocalEulerAngles.md) | 获取相关状态或对象。 | -| [SetLocalEulerAngles](SetLocalEulerAngles.md) | 设置相关状态或配置。 | -| [GetPosition](GetPosition.md) | 获取相关状态或对象。 | -| [SetPosition](SetPosition.md) | 设置相关状态或配置。 | -| [GetRotation](GetRotation.md) | 获取相关状态或对象。 | -| [SetRotation](SetRotation.md) | 设置相关状态或配置。 | -| [GetScale](GetScale.md) | 获取相关状态或对象。 | -| [SetScale](SetScale.md) | 设置相关状态或配置。 | -| [GetForward](GetForward.md) | 获取相关状态或对象。 | -| [GetRight](GetRight.md) | 获取相关状态或对象。 | -| [GetUp](GetUp.md) | 获取相关状态或对象。 | -| [GetLocalToWorldMatrix](GetLocalToWorldMatrix.md) | 获取相关状态或对象。 | -| [GetWorldToLocalMatrix](GetWorldToLocalMatrix.md) | 获取相关状态或对象。 | -| [GetParent](GetParent.md) | 获取相关状态或对象。 | -| [SetParent](SetParent.md) | 设置相关状态或配置。 | -| [GetChildCount](GetChildCount.md) | 获取相关状态或对象。 | -| [GetChild](GetChild.md) | 获取相关状态或对象。 | -| [Find](Find.md) | 查找并返回匹配对象。 | -| [DetachChildren](DetachChildren.md) | 公开方法,详见头文件声明。 | -| [GetSiblingIndex](GetSiblingIndex.md) | 获取相关状态或对象。 | -| [SetSiblingIndex](SetSiblingIndex.md) | 设置相关状态或配置。 | -| [SetAsFirstSibling](SetAsFirstSibling.md) | 设置相关状态或配置。 | -| [SetAsLastSibling](SetAsLastSibling.md) | 设置相关状态或配置。 | -| [LookAt](LookAt.md) | 公开方法,详见头文件声明。 | -| [Rotate](Rotate.md) | 公开方法,详见头文件声明。 | -| [Translate](Translate.md) | 公开方法,详见头文件声明。 | -| [TransformPoint](TransformPoint.md) | 公开方法,详见头文件声明。 | -| [InverseTransformPoint](InverseTransformPoint.md) | 公开方法,详见头文件声明。 | -| [TransformDirection](TransformDirection.md) | 公开方法,详见头文件声明。 | -| [InverseTransformDirection](InverseTransformDirection.md) | 公开方法,详见头文件声明。 | -| [SetDirty](SetDirty.md) | 设置相关状态或配置。 | -| [UpdateWorldTransform](UpdateWorldTransform.md) | 更新运行时状态。 | -| [NotifyHierarchyChanged](NotifyHierarchyChanged.md) | 公开方法,详见头文件声明。 | +类型内部同时维护: + +- 局部数据:`m_localPosition`、`m_localRotation`、`m_localScale` +- 派生缓存:`m_localToWorldMatrix`、`m_worldPosition`、`m_worldRotation`、`m_worldScale` +- 脏标记:`m_dirty` + +当前实现使用懒更新模式: + +- 修改局部位置、旋转或缩放时,调用 `SetDirty()` +- 真正读取世界矩阵、世界位置、世界旋转或世界缩放时,才通过 `UpdateWorldTransform()` 重新计算 + +这是一种常见的商业引擎做法,优点是多个 setter 可以先累积,再在真正需要世界空间数据时统一计算;代价是层级脏标记传播一定要正确,否则很容易出现缓存过期问题。 + +## 层级关系 + +`TransformComponent` 自己维护一套父子变换树: + +- `GetParent()` +- `SetParent(TransformComponent*, bool worldPositionStays = true)` +- `GetChild()` +- `GetChildCount()` +- `DetachChildren()` + +但要注意,变换树和 `GameObject` 的对象树虽然通常同步,却不是完全同一个概念。 + +### 推荐做法 + +当你操作真实场景对象关系时,优先使用 `GameObject::SetParent()`。 + +原因是 `GameObject::SetParent()` 会同时处理: + +- `GameObject` 子列表 +- 场景根节点列表 +- 激活层级变化传播 +- 对应 `Transform` 的父子关系 + +而直接调用 `TransformComponent::SetParent()` 只会改变变换树,不会完整维护场景对象树的其他语义。 + +## 空间操作 + +`TransformComponent` 提供了当前最常用的一组空间 API: + +- 世界/局部位置、旋转、缩放读写 +- `GetForward()`、`GetRight()`、`GetUp()` +- `LookAt()` +- `Rotate()`、`Translate()` +- `TransformPoint()` / `InverseTransformPoint()` +- `TransformDirection()` / `InverseTransformDirection()` + +从实现看,这些方法都是直接基于 `Quaternion`、`Matrix4x4` 和当前缓存矩阵完成的轻量封装,没有额外的物理、动画或约束系统参与。 + +## 查找语义 + +`Find(const std::string& name)` 的行为容易被误解。按当前实现,它不是“从当前节点的子树开始找”,而是: + +1. 通过宿主 `GameObject` 拿到所属 `Scene` +2. 从场景所有 root game objects 开始递归搜索 +3. 找到第一个名称匹配的 `GameObject` 后返回其 `Transform` + +因此,它更像一个场景级按名称查找的快捷入口,而不是严格的局部子树查询 API。 + +## 序列化语义 + +`Serialize()` / `Deserialize()` 当前只处理局部变换: + +- `position` +- `rotation` +- `scale` + +不会直接写出父子关系,也不会直接写出世界矩阵缓存。世界空间数据会在反序列化后通过 `SetDirty()` 延迟重建。 + +这和商业引擎常见做法一致:持久化局部值,把世界值当运行时派生结果。 + +## 线程语义 + +- 当前实现没有内部加锁。 +- `m_dirty`、父子列表和缓存矩阵都按单线程访问模型设计。 +- 默认应在主线程或明确受控的场景更新线程上读写。 + +## 当前实现限制 + +- `SetDirty()` 会递归标记所有子节点,这是正确方向,但如果层级很深,批量修改会带来明显传播成本。 +- `SetSiblingIndex()`、`SetAsFirstSibling()`、`SetAsLastSibling()` 当前只修改 `m_siblingIndex` 数值,没有真正重排父节点 `m_children` 顺序。 +- `DetachChildren()` 只改 `TransformComponent` 自己的父子指针,不会同步处理 `GameObject` 层级与 `Scene` 根节点列表。 +- `Find()` 是场景级名称搜索,不是局部子树搜索。 + +这些限制非常值得在编辑器或脚本层显式规避。 + +## 推荐使用方式 + +1. 日常层级重组优先用 `GameObject::SetParent()`。 +2. 把 `TransformComponent` 视为内建基础设施,不要尝试把它当普通组件那样增删。 +3. 需要保存/加载时只关心局部值,不要手动持久化世界矩阵。 +4. 使用 `Find()` 时明确它是场景级名称匹配,不要把它当路径查询接口。 + +## 相关方法 + +- [Constructor](Constructor.md) +- [GetLocalPosition](GetLocalPosition.md) +- [SetLocalPosition](SetLocalPosition.md) +- [GetLocalRotation](GetLocalRotation.md) +- [SetLocalRotation](SetLocalRotation.md) +- [GetLocalScale](GetLocalScale.md) +- [SetLocalScale](SetLocalScale.md) +- [GetPosition](GetPosition.md) +- [SetPosition](SetPosition.md) +- [GetRotation](GetRotation.md) +- [SetRotation](SetRotation.md) +- [GetScale](GetScale.md) +- [SetScale](SetScale.md) +- [GetLocalToWorldMatrix](GetLocalToWorldMatrix.md) +- [GetWorldToLocalMatrix](GetWorldToLocalMatrix.md) +- [SetParent](SetParent.md) +- [Find](Find.md) +- [DetachChildren](DetachChildren.md) +- [SetDirty](SetDirty.md) +- [UpdateWorldTransform](UpdateWorldTransform.md) +- [LookAt](LookAt.md) +- [Rotate](Rotate.md) +- [Translate](Translate.md) +- [TransformPoint](TransformPoint.md) +- [InverseTransformPoint](InverseTransformPoint.md) +- [TransformDirection](TransformDirection.md) +- [InverseTransformDirection](InverseTransformDirection.md) +- [Serialize](Serialize.md) +- [Deserialize](Deserialize.md) + +## 相关指南 + +- [GameObject-Component Lifecycle And Serialization](../../../_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md) ## 相关文档 -- [当前目录](../Components.md) - 返回 `Components` 平行目录 -- [API 总索引](../../../main.md) - 返回顶层索引 +- [当前模块](../Components.md) +- [GameObject](../GameObject/GameObject.md) +- [Component](../Component/Component.md) +- [API 总索引](../../../main.md) diff --git a/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md b/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md new file mode 100644 index 00000000..9b02fb9f --- /dev/null +++ b/docs/api/_guides/Components/GameObject-Component-Lifecycle-And-Serialization.md @@ -0,0 +1,266 @@ +# GameObject-Component Lifecycle And Serialization + +## 这篇指南解决什么问题 + +只看 `Components` 模块下的单个 API 页,很容易知道“有哪些函数”,但不容易知道“应该怎么组织对象”“生命周期到底是谁驱动的”“为什么有些对象能被查到、有些不能”“为什么某些组件保存后会丢状态”。 + +这篇指南专门回答这些问题。它不是第二套 API 参考,而是帮助你建立当前 `XCEngine::Components` 模块的整体心智模型。 + +## 先建立正确心智模型 + +当前引擎采用的是 **GameObject + Component + Scene 树** 模型,而不是 ECS。 + +可以把它粗略类比成 Unity 的对象体系: + +- `Scene` 像场景容器,拥有对象并驱动更新与序列化。 +- `GameObject` 像层级树节点,负责身份、父子关系、激活状态和组件容器。 +- `TransformComponent` 像内建 `Transform`,每个对象必有一个。 +- 其他组件按职责挂接到对象上,例如 `CameraComponent`、`LightComponent`、`MeshRendererComponent`、`AudioSourceComponent`。 + +这种设计的优势是: + +- 对编辑器层级、脚本调用和运行时调试都比较直观。 +- 对多数游戏开发者来说学习成本低。 +- 场景保存/加载可以直接围绕对象树展开。 + +代价是: + +- 生命周期不再只是“组件自己的事”,而是依赖 `Scene -> GameObject -> Component` 这条链路。 +- 某些看起来像全局查询的接口,其实只对场景托管对象成立。 +- 运行时动态加组件时,如果没有自动补发初始化阶段,就会出现“对象在,但逻辑没进入完整生命周期”的情况。 + +## 为什么 `TransformComponent` 是内建组件 + +商业级游戏引擎里,`Transform` 往往不是普通业务组件,而是对象树的基础设施。当前实现也是这个思路: + +- `GameObject` 构造时立即创建 `TransformComponent` +- `AddComponent()` 返回已有实例 +- `RemoveComponent()` 不能移除它 + +这样设计的好处是: + +- 所有对象都天然具有空间位置和层级关系 +- 相机、灯光、网格、音频这些组件都可以稳定依赖 `transform()` +- 编辑器和场景序列化不需要处理“这个对象没有变换”的特殊分支 + +代价是 `TransformComponent` 不再是完全对称的组件类型。你在上层逻辑里必须把它当成“基础设施组件”,不是“普通可选功能块”。 + +## 场景托管对象 vs 独立对象 + +这是当前模块最容易被忽略的一条边界。 + +### 独立对象 + +```cpp +XCEngine::Components::GameObject player("Player"); +``` + +这会得到一个可用对象,但它: + +- 只有内建 `Transform` +- 默认不在任何 `Scene` 中 +- 不会自动注册到全局查找表 +- 不会自动触发 `Awake` + +它适合测试、临时对象或手工控制生命周期的场景。 + +### 场景托管对象 + +```cpp +auto* player = scene.CreateGameObject("Player"); +``` + +按当前实现,这条路径会额外完成: + +- 设置 `m_scene` +- 注册到全局 registry +- 接入场景根节点或父节点 +- 调用 `Awake()` + +这才是更接近“真实运行时对象”的创建方式。 + +### 为什么这很重要 + +因为下列能力都和“是否已被场景托管”直接相关: + +- `GameObject::Find()` +- `GameObject::FindObjectsOfType()` +- `GameObject::FindGameObjectsWithTag()` +- `Component::GetScene()` +- 场景级保存/加载 + +如果对象只是裸构造出来的,你不能期待这些能力都成立。 + +## 当前真实生命周期 + +下面这张表描述的是 **当前代码已经实现的行为**,不是理想化设计图。 + +| 阶段 | 谁触发 | 当前行为 | +|------|------|------| +| 构造 `GameObject` | 用户或 `Scene` | 创建对象、分配 ID/UUID、创建内建 `Transform` | +| `Scene::CreateGameObject()` | `Scene` | 注册全局表、设置场景、挂到父子层级、调用 `Awake()` | +| `Scene::DeserializeFromString()` | `Scene` | 重建对象、Transform、普通组件和父子关系,但当前不会自动补发 `Awake()` | +| `GameObject::Awake()` | `Scene` 或用户 | 遍历普通组件并调用 `Awake()` | +| `Scene::Update()` | `Scene` | 对激活 root object 调用 `Start()`,之后 `Update()` | +| `GameObject::Start()` | `Scene` 或用户 | 只调用一次;仅对激活层级中的已启用普通组件生效 | +| `SetEnabled()` / `SetActive()` / `SetParent()` | 用户或内部逻辑 | 根据层级有效激活态触发 `OnEnable()` / `OnDisable()` | +| `Destroy()` | 用户 | 场景托管对象走 `Scene::DestroyGameObject()`;独立对象只发 `OnDestroy()`,不自动 delete | + +## 运行时 `AddComponent()` 的现实语义 + +很多用户会下意识把它当成 Unity 那种“加上就自动进入生命周期”的接口,但当前实现不是。 + +```cpp +auto* go = scene.CreateGameObject("Emitter"); +auto* source = go->AddComponent(); +``` + +这里会发生的事情只有: + +- 创建组件实例 +- 回填 `component->m_gameObject = go` +- 放进 `m_components` + +这里 **不会自动发生**: + +- `Awake()` +- `Start()` +- `OnEnable()` + +为什么这很关键?因为很多组件初始化逻辑都习惯写在这些阶段里。如果你在运行中动态加组件,就需要自己明确何时让它进入可用状态。 + +还有一个更容易踩坑的点:如果这个 `GameObject` 之前已经进入过 `Start()`,后续再添加的新组件也不会在未来被自动 `Start()`,因为当前实现只用 `m_started` 记录“这个对象是否已经启动过一次”,而不是“每个组件是否已经启动过”。 + +## 激活状态为什么要区分两层 + +和 Unity 一样,这里也区分: + +- `activeSelf` +- `activeInHierarchy` + +原因很现实: + +- 一个对象自己打开,不代表它真的在运行;只要父对象被禁用,它依然应该整体失活。 +- 组件的 `OnEnable()` / `OnDisable()` 不能只看自己的 `enabled`,还要看宿主对象是否真的在活跃层级里。 + +当前实现里,`Component::SetEnabled()` 就按下面的“有效启用态”处理: + +`component enabled && gameObject active in hierarchy` + +这能避免父节点关闭时组件仍然误以为自己处于活动状态。 + +## 为什么序列化要分层 + +初看时可能会觉得:既然 `GameObject` 有组件容器,为什么不让 `GameObject::Serialize()` 一次把所有内容都写完? + +当前实现没有这么做,而是分成两层: + +### `GameObject::Serialize()` + +只写对象基础状态: + +- `name` +- `active` +- `id` +- `uuid` +- `transform` + +### `Scene::SerializeToString()` + +负责完整场景结构: + +- 所有 root object 递归展开 +- `parent` 关系 +- 每个普通组件的类型名和 payload +- 反序列化时通过 `ComponentFactoryRegistry` 重建组件 + +这种分层的好处是职责清晰: + +- `GameObject` 负责“我是谁,我的基础状态是什么” +- `Scene` 负责“我如何把整棵对象树持久化” + +这和商业引擎里把对象局部状态和场景容器职责拆开的思路是一致的。 + +但当前加载路径还有一个必须知道的现实差异:`Scene::DeserializeFromString()` 在恢复对象和组件后,没有自动重放 `Awake()`。所以“新建场景对象”和“从场景文本加载出来的对象”在初始化阶段并不完全对称。 + +## `ComponentFactoryRegistry` 在这里的作用 + +反序列化场景时,文本里只有组件类型名,例如: + +```text +component=Camera;projection=0;fov=60;... +``` + +引擎需要把 `"Camera"` 重新变成具体组件实例。当前实现就是通过 `ComponentFactoryRegistry` 完成这件事。 + +它当前预注册了: + +- `Camera` +- `Light` +- `AudioSource` +- `AudioListener` +- `MeshFilter` +- `MeshRenderer` +- `ScriptComponent` + +注意 `Transform` 不在里面,因为 `TransformComponent` 不是靠工厂反序列化创建的,它是 `GameObject` 的内建成员。 + +## 你现在必须知道的限制 + +### 1. `FindGameObjectsWithTag()` 不是 tag 系统 + +当前实现按对象名称匹配,不是独立 tag 字段。 + +### 2. `TransformComponent::Find()` 不是局部子树查找 + +它会从整个场景的 root objects 开始搜索第一个名字匹配项。 + +### 3. `TransformComponent` 的 sibling API 还不完整 + +`SetSiblingIndex()`、`SetAsFirstSibling()`、`SetAsLastSibling()` 目前只修改索引字段,没有真正重排父节点子列表。 + +### 4. 音频组件当前不参与完整序列化 + +`AudioSourceComponent` 和 `AudioListenerComponent` 没有自定义 `Serialize()` / `Deserialize()`,所以场景保存/加载不会持久化它们的运行时参数。 + +### 5. 独立对象的 `Destroy()` 不会删除对象本身 + +它只调用 `OnDestroy()`。如果对象不是 `Scene` 持有的,内存释放仍然要由真正的所有者负责。 + +### 6. 反序列化路径当前不会自动重放 `Awake()` + +如果某些组件把关键初始化逻辑写在 `Awake()`,那么从场景文本恢复后的状态可能和新建对象不同。 + +## 推荐工作流 + +### 场景内对象 + +1. 用 `Scene::CreateGameObject()` 创建对象。 +2. 用 `GameObject::SetParent()` 组织层级。 +3. 用 `AddComponent()` 挂功能组件。 +4. 用 `Scene::Update()` / `FixedUpdate()` / `LateUpdate()` 驱动生命周期。 +5. 用 `Scene::SerializeToString()` / `DeserializeFromString()` 保存和恢复。 + +### 非场景测试对象 + +1. 直接构造 `GameObject`。 +2. 只依赖本地组件访问和 `Transform` 操作。 +3. 不要假定静态查找、场景序列化或自动生命周期已经可用。 + +## 和 Unity 的关系应该怎么理解 + +最合理的理解方式是: + +- 它在对象模型上明显借鉴了 Unity。 +- 它提供了足够熟悉的 `GameObject + Transform + Component` 心智模型。 +- 但当前实现仍然比 Unity 轻得多,特别是在 tag、运行时加组件生命周期、完整组件序列化和层级顺序控制上。 + +把它理解成“Unity 风格、但当前版本能力更收敛的对象系统”是比较准确的。 + +## 从这里继续读什么 + +- [Components](../../XCEngine/Components/Components.md) +- [GameObject](../../XCEngine/Components/GameObject/GameObject.md) +- [Component](../../XCEngine/Components/Component/Component.md) +- [TransformComponent](../../XCEngine/Components/TransformComponent/TransformComponent.md) +- [ComponentFactoryRegistry](../../XCEngine/Components/ComponentFactoryRegistry/ComponentFactoryRegistry.md) diff --git a/docs/api/_meta/rebuild-status.md b/docs/api/_meta/rebuild-status.md index 51cf51f3..c6116d41 100644 --- a/docs/api/_meta/rebuild-status.md +++ b/docs/api/_meta/rebuild-status.md @@ -1,14 +1,14 @@ # API 文档重构状态 -**生成时间**: `2026-03-26 21:30:11` +**生成时间**: `2026-03-27 16:00:15` **来源**: `docs/api/_tools/audit_api_docs.py` ## 摘要 -- Markdown 页面数(全部): `2535` -- Markdown 页面数(canonical): `2524` -- Public headers 数: `204` +- Markdown 页面数(全部): `2625` +- Markdown 页面数(canonical): `2612` +- Public headers 数: `221` - 有效头文件引用数(全部): `197` - 有效头文件引用数(canonical): `197` - 无效头文件引用数: `0` @@ -20,9 +20,9 @@ ## 平行目录 - Canonical 根目录: `XCEngine` -- 源码目录节点数: `27` +- 源码目录节点数: `29` - 已生成目录总览页节点数: `26` -- 缺失目录总览页节点数: `1` +- 缺失目录总览页节点数: `3` - 支撑目录: `_meta, _tools` ## 模块覆盖 @@ -36,30 +36,49 @@ | `Input` | `5` | `5` | `0` | | `Memory` | `5` | `5` | `0` | | `Platform` | `11` | `11` | `0` | -| `RHI` | `68` | `68` | `0` | -| `Rendering` | `10` | `10` | `0` | +| `RHI` | `83` | `68` | `15` | +| `Rendering` | `11` | `10` | `1` | | `Resources` | `13` | `13` | `0` | | `Scene` | `3` | `2` | `1` | -| `Scripting` | `6` | `0` | `6` | +| `Scripting` | `7` | `0` | `7` | | `Threading` | `10` | `10` | `0` | ## 元信息覆盖 | 字段 | 页面数 | |------|--------| -| `命名空间` | `222` | -| `类型` | `222` | -| `描述` | `222` | +| `命名空间` | `307` | +| `类型` | `307` | +| `描述` | `307` | | `头文件` | `197` | ## 缺失的平行目录总览页 +- `XCEngine/RHI/Vulkan` - `XCEngine/Scripting` +- `XCEngine/Scripting/Mono` ## 未覆盖的 public headers +- `XCEngine/RHI/Vulkan/VulkanBuffer.h` +- `XCEngine/RHI/Vulkan/VulkanCommandList.h` +- `XCEngine/RHI/Vulkan/VulkanCommandQueue.h` +- `XCEngine/RHI/Vulkan/VulkanCommon.h` +- `XCEngine/RHI/Vulkan/VulkanDescriptorPool.h` +- `XCEngine/RHI/Vulkan/VulkanDescriptorSet.h` +- `XCEngine/RHI/Vulkan/VulkanDevice.h` +- `XCEngine/RHI/Vulkan/VulkanFence.h` +- `XCEngine/RHI/Vulkan/VulkanPipelineLayout.h` +- `XCEngine/RHI/Vulkan/VulkanPipelineState.h` +- `XCEngine/RHI/Vulkan/VulkanResourceView.h` +- `XCEngine/RHI/Vulkan/VulkanSampler.h` +- `XCEngine/RHI/Vulkan/VulkanScreenshot.h` +- `XCEngine/RHI/Vulkan/VulkanSwapChain.h` +- `XCEngine/RHI/Vulkan/VulkanTexture.h` +- `XCEngine/Rendering/RenderMaterialUtility.h` - `XCEngine/Scene/SceneRuntime.h` - `XCEngine/Scripting/IScriptRuntime.h` +- `XCEngine/Scripting/Mono/MonoScriptRuntime.h` - `XCEngine/Scripting/NullScriptRuntime.h` - `XCEngine/Scripting/ScriptComponent.h` - `XCEngine/Scripting/ScriptEngine.h`