feat(ecs): per-component enable/disable + inspector rework

Add `active` flag to the 8 behavior components (Unity enabled / UE
SetActive semantics; structural Id/Name/Transform excluded). Consumers
skip disabled components: renderer (mesh/light/anim), primary camera
election, physics reconciliation (body/constraint/character destroyed
and rebuilt on toggle), scripts keep instance but stop ticking.

Inspector: component headers now carry an enable checkbox and a remove
button, Add-Component menu greys out attached entries, and conflict
hints added (RigidBody <-> CharacterController mutual exclusion, Joint
requires RigidBody, Animation requires MeshRenderer, unregistered
script name). Scenes persist Active per component (default true).
This commit is contained in:
2026-10-03 18:45:03 +08:00
parent cad0481bc0
commit b6d0868e15
7 changed files with 280 additions and 65 deletions
+208 -48
View File
@@ -15,6 +15,7 @@
#include <cstdio> #include <cstdio>
#include <vector> #include <vector>
#include <type_traits>
namespace namespace
{ {
@@ -64,6 +65,44 @@ namespace
ImGui::PopID(); ImGui::PopID();
return changed; return changed;
} }
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"删除"按钮。
// active 传 nullptr = 无启停(结构性组件);removable = 显示删除钮,
// 点击时置 *removeFlag,调用方在区块末尾统一移除(避免边画边删)
bool ComponentHeader(const char* label, bool* active, bool removable,
bool* removeFlag)
{
ImGui::PushID(label);
bool open;
if (active != nullptr)
{
open = ImGui::CollapsingHeader(
label, ImGuiTreeNodeFlags_DefaultOpen |
ImGuiTreeNodeFlags_AllowOverlap);
const float rowH = ImGui::GetItemRectSize().y;
float x = ImGui::GetWindowWidth() - 32.0f;
if (removable)
{
ImGui::SameLine(x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
if (ImGui::Button("-", ImVec2(24.0f, rowH - 4.0f)))
{
*removeFlag = true;
}
x -= 30.0f;
}
ImGui::SameLine(x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
ImGui::Checkbox("##active", active);
}
else
{
open = ImGui::CollapsingHeader(label,
ImGuiTreeNodeFlags_DefaultOpen);
}
ImGui::PopID();
return open;
}
} }
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
@@ -85,61 +124,47 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
} }
ImGui::Separator(); ImGui::Separator();
// ---- Add Component ---- // ---- Add Component(已挂载项置灰:Unity Add Component 语义)----
if (ImGui::Button("Add Component")) if (ImGui::Button("Add Component"))
{ {
ImGui::OpenPopup("add_component"); ImGui::OpenPopup("add_component");
} }
if (ImGui::BeginPopup("add_component")) if (ImGui::BeginPopup("add_component"))
{ {
if (!reg.all_of<Engine::ECS::LightComponent>(selected) && auto menuItem = [&](auto typeTag, const char* label)
ImGui::MenuItem("Light"))
{ {
reg.emplace<Engine::ECS::LightComponent>(selected); using T = std::remove_pointer_t<decltype(typeTag)>;
} const bool has = reg.all_of<T>(selected);
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) && if (has)
ImGui::MenuItem("Camera"))
{ {
reg.emplace<Engine::ECS::CameraComponent>(selected); ImGui::BeginDisabled(true);
} }
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) && if (ImGui::MenuItem(label))
ImGui::MenuItem("Animation"))
{ {
reg.emplace<Engine::ECS::AnimationComponent>(selected); reg.emplace<T>(selected);
} }
if (!reg.all_of<Engine::ECS::NameComponent>(selected) && if (has)
ImGui::MenuItem("Name"))
{ {
reg.emplace<Engine::ECS::NameComponent>(selected); ImGui::EndDisabled();
}
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) &&
ImGui::MenuItem("RigidBody"))
{
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
}
if (!reg.all_of<Engine::ECS::JointComponent>(selected) &&
ImGui::MenuItem("Joint"))
{
reg.emplace<Engine::ECS::JointComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CharacterControllerComponent>(selected) &&
ImGui::MenuItem("Character Controller"))
{
reg.emplace<Engine::ECS::CharacterControllerComponent>(selected);
}
if (!reg.all_of<Engine::ECS::ScriptComponent>(selected) &&
ImGui::MenuItem("Script"))
{
reg.emplace<Engine::ECS::ScriptComponent>(selected);
} }
};
menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
menuItem(static_cast<Engine::ECS::NameComponent*>(nullptr), "Name");
menuItem(static_cast<Engine::ECS::RigidBodyComponent*>(nullptr), "RigidBody");
menuItem(static_cast<Engine::ECS::JointComponent*>(nullptr), "Joint");
menuItem(static_cast<Engine::ECS::CharacterControllerComponent*>(nullptr),
"Character Controller");
menuItem(static_cast<Engine::ECS::ScriptComponent*>(nullptr), "Script");
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::Separator(); ImGui::Separator();
// ---- Transform ---- // ---- Transform(结构性组件:不可删、无启停)----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected)) if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("Transform", nullptr, false, nullptr))
{ {
glm::vec3 pos = t->GetPosition(); glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f)) if (DragVec3Row("position", pos, 0.05f))
@@ -162,9 +187,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// ---- MeshRenderer ---- // ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected)) if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{ {
if (ImGui::CollapsingHeader("MeshRenderer", bool removeMr = false;
ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("MeshRenderer", &mr->active, true, &removeMr))
{ {
if (!mr->active)
{
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
}
if (const Engine::Model* model = ctx.assets->GetModel(mr->model); if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr) model != nullptr)
{ {
@@ -179,14 +208,27 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)"); "model: INVALID (dangling ref)");
} }
if (!mr->active)
{
ImGui::EndDisabled();
}
}
if (removeMr)
{
reg.remove<Engine::ECS::MeshRendererComponent>(selected);
} }
} }
// ---- Light ---- // ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected)) if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) bool removeLight = false;
if (ComponentHeader("Light", &light->active, true, &removeLight))
{ {
if (!light->active)
{
ImGui::BeginDisabled(true);
}
int type = static_cast<int>(light->type); int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0")) if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{ {
@@ -204,14 +246,27 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f); ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)"); ImGui::TextDisabled("(position = entity Transform)");
} }
if (!light->active)
{
ImGui::EndDisabled();
}
}
if (removeLight)
{
reg.remove<Engine::ECS::LightComponent>(selected);
} }
} }
// ---- Camera ---- // ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected)) if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) bool removeCam = false;
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
{ {
if (!cam->active)
{
ImGui::BeginDisabled(true);
}
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary) if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{ {
// primary 互斥:启用本相机时清掉其余实体的 primary // primary 互斥:启用本相机时清掉其余实体的 primary
@@ -237,6 +292,14 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ctx.editorCamera->AlignTo(*tr); ctx.editorCamera->AlignTo(*tr);
} }
} }
if (!cam->active)
{
ImGui::EndDisabled();
}
}
if (removeCam)
{
reg.remove<Engine::ECS::CameraComponent>(selected);
} }
} }
@@ -244,8 +307,21 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效 // type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected)) if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
{ {
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen)) bool removeRb = false;
if (ComponentHeader("RigidBody", &rb->active, true, &removeRb))
{ {
if (!rb->active)
{
ImGui::BeginDisabled(true);
}
// 冲突提示:同实体挂 CharacterController 会同时建 body 与
// CharacterVirtual,两个"运动主体"互相打架
if (reg.all_of<Engine::ECS::CharacterControllerComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! conflicts with Character Controller "
"(disable or remove one)");
}
int type = static_cast<int>(rb->type); int type = static_cast<int>(rb->type);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0")) if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
{ {
@@ -347,14 +423,33 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
s_debugImpulse); s_debugImpulse);
} }
} }
if (!rb->active)
{
ImGui::EndDisabled();
}
}
if (removeRb)
{
reg.remove<Engine::ECS::RigidBodyComponent>(selected);
} }
} }
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)---- // ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected)) if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Joint", ImGuiTreeNodeFlags_DefaultOpen)) bool removeJc = false;
if (ComponentHeader("Joint", &jc->active, true, &removeJc))
{ {
if (!jc->active)
{
ImGui::BeginDisabled(true);
}
// 依赖提示:约束挂在两端刚体上,本体无 RigidBody 无法创建
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a RigidBody on this entity");
}
int type = static_cast<int>(jc->type); int type = static_cast<int>(jc->type);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0")) if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
{ {
@@ -399,15 +494,34 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f); ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
} }
} }
if (!jc->active)
{
ImGui::EndDisabled();
}
}
if (removeJc)
{
reg.remove<Engine::ECS::JointComponent>(selected);
} }
} }
// ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)---- // ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)----
if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected)) if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Character Controller", bool removeCc = false;
ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("Character Controller", &cc->active, true, &removeCc))
{ {
if (!cc->active)
{
ImGui::BeginDisabled(true);
}
// 冲突提示:同实体挂 RigidBody 会同时建 CharacterVirtual 与 body
if (reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! conflicts with RigidBody "
"(disable or remove one)");
}
// 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius) // 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f); ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f); ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
@@ -431,14 +545,27 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
phy.Jump(bits, 5.0f); phy.Jump(bits, 5.0f);
} }
} }
if (!cc->active)
{
ImGui::EndDisabled();
}
}
if (removeCc)
{
reg.remove<Engine::ECS::CharacterControllerComponent>(selected);
} }
} }
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)---- // ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected)) if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) bool removeSc = false;
if (ComponentHeader("Script", &sc->active, true, &removeSc))
{ {
if (!sc->active)
{
ImGui::BeginDisabled(true);
}
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例) // 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
const auto& names = Engine::Scripting::ScriptRegistry::Names(); const auto& names = Engine::Scripting::ScriptRegistry::Names();
std::vector<const char*> items; std::vector<const char*> items;
@@ -464,20 +591,45 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
ImGui::TextDisabled("no script assigned"); ImGui::TextDisabled("no script assigned");
} }
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
else if (cur == 0)
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! script not registered: %s",
sc->scriptName.c_str());
}
if (!sc->active)
{
ImGui::EndDisabled();
}
}
if (removeSc)
{
reg.remove<Engine::ECS::ScriptComponent>(selected);
} }
} }
// ---- Animation ---- // ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected)) if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{ {
if (ImGui::CollapsingHeader("Animation", bool removeAnim = false;
ImGuiTreeNodeFlags_DefaultOpen)) if (ComponentHeader("Animation", &anim->active, true, &removeAnim))
{ {
if (!anim->active)
{
ImGui::BeginDisabled(true);
}
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected); const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model = const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr; mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations()) // 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
if (mr == nullptr)
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! requires a MeshRenderer component");
}
else if (model != nullptr && model->HasAnimations())
{ {
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
std::vector<const char*> clipNames; std::vector<const char*> clipNames;
@@ -508,6 +660,14 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
ImGui::TextDisabled("(no animation clips on model)"); ImGui::TextDisabled("(no animation clips on model)");
} }
if (!anim->active)
{
ImGui::EndDisabled();
}
}
if (removeAnim)
{
reg.remove<Engine::ECS::AnimationComponent>(selected);
} }
} }
+8
View File
@@ -123,12 +123,14 @@ namespace Engine::ECS {
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。 // ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
struct MeshRendererComponent { struct MeshRendererComponent {
ModelRef model; ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
}; };
// 骨骼动画播放状态(纯数据 + 便捷控制)。 // 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者); // 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。 // ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent { struct AnimationComponent {
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
int clipIndex = 0; // 当前 clip(Model::GetClips 下标) int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true; bool playing = true;
bool loop = true; bool loop = true;
@@ -158,6 +160,7 @@ namespace Engine::ECS {
// 相机。一个场景中 primary=true 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent { struct CameraComponent {
bool active = true; // 禁用 = 退出 primary 候选
Camera camera; Camera camera;
bool primary = false; bool primary = false;
}; };
@@ -166,6 +169,7 @@ namespace Engine::ECS {
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光) // Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减 // Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent { struct LightComponent {
bool active = true; // 禁用 = 不进光照
enum class Type : u8 { Directional = 0, Point = 1 }; enum class Type : u8 { Directional = 0, Point = 1 };
Type type = Type::Directional; Type type = Type::Directional;
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光 glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
@@ -183,6 +187,7 @@ namespace Engine::ECS {
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent // 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。 // (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
struct RigidBodyComponent { struct RigidBodyComponent {
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 }; enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider // Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动); // 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
@@ -241,6 +246,7 @@ namespace Engine::ECS {
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查 // target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关 // 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
struct JointComponent { struct JointComponent {
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 }; enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
Type type = Type::Fixed; Type type = Type::Fixed;
UUID target{}; // 目标实体(0 = 未设置,不建约束) UUID target{}; // 目标实体(0 = 未设置,不建约束)
@@ -264,6 +270,7 @@ namespace Engine::ECS {
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在 // 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
// PhysicsWorld 的 CharacterController 里(对账重建) // PhysicsWorld 的 CharacterController 里(对账重建)
struct CharacterControllerComponent { struct CharacterControllerComponent {
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
f32 radius = 0.3f; // 胶囊半径 f32 radius = 0.3f; // 胶囊半径
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r)) f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
f32 mass = 70.0f; f32 mass = 70.0f;
@@ -278,6 +285,7 @@ namespace Engine::ECS {
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。 // ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝) // 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
struct ScriptComponent { struct ScriptComponent {
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
std::string scriptName; std::string scriptName;
bool operator==(const ScriptComponent&) const = default; bool operator==(const ScriptComponent&) const = default;
+1 -1
View File
@@ -81,7 +81,7 @@ namespace Engine::ECS {
CameraComponent* found = nullptr; CameraComponent* found = nullptr;
Entity foundEntity = entt::null; Entity foundEntity = entt::null;
m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) { m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
if (!found && cc.primary) { if (!found && cc.primary && cc.active) { // 禁用相机退出候选
found = &cc; found = &cc;
foundEntity = e; foundEntity = e;
} }
+31 -9
View File
@@ -356,6 +356,19 @@ namespace Engine {
for (auto [e, rb, tr]: view.each()) { for (auto [e, rb, tr]: view.each()) {
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
const auto found = m_Bodies.find(bits); const auto found = m_Bodies.find(bits);
// 禁用 = 视为组件不存在:销毁 body(对账自然消化),重启用即重建
if (!rb.active) {
if (found != m_Bodies.end()) {
bi.RemoveBody(found->second.id);
bi.DestroyBody(found->second.id);
m_Bodies.erase(found);
invalidated.push_back(bits);
}
rb.bodyValid = false;
continue;
}
if (found == m_Bodies.end() || !rb.bodyValid) { if (found == m_Bodies.end() || !rb.bodyValid) {
if (found != m_Bodies.end()) { if (found != m_Bodies.end()) {
bi.RemoveBody(found->second.id); bi.RemoveBody(found->second.id);
@@ -437,12 +450,14 @@ namespace Engine {
!= invalidated.end(); != invalidated.end();
}; };
// 1. owner 实体/组件没了,或任一端 body 本帧被销毁/重建 → 移除 // 1. owner 实体/组件没了(或被禁用),或任一端 body 本帧被销毁/重建 → 移除
for (auto it = m_Constraints.begin(); it != m_Constraints.end();) { for (auto it = m_Constraints.begin(); it != m_Constraints.end();) {
const u32 owner = it->first; const u32 owner = it->first;
const auto ownerEnt = static_cast<entt::entity>(owner); const auto ownerEnt = static_cast<entt::entity>(owner);
const bool ownerGone = !reg.valid(ownerEnt) || const ECS::JointComponent* jc =
!reg.all_of<ECS::JointComponent>(ownerEnt); reg.valid(ownerEnt) ? reg.try_get<ECS::JointComponent>(ownerEnt)
: nullptr;
const bool ownerGone = jc == nullptr || !jc->active;
if (ownerGone || tainted(owner) || tainted(it->second.targetBits)) { if (ownerGone || tainted(owner) || tainted(it->second.targetBits)) {
m_System->RemoveConstraint(it->second.constraint.GetPtr()); m_System->RemoveConstraint(it->second.constraint.GetPtr());
it = m_Constraints.erase(it); it = m_Constraints.erase(it);
@@ -451,10 +466,13 @@ namespace Engine {
} }
} }
// 2. 遍历组件:快照变更 → 重建;缺失 → 创建。 // 2. 遍历组件:快照变更 → 重建;缺失 → 创建;禁用 → 跳过(上面已清)。
// target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试) // target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试)
auto view = reg.view<ECS::JointComponent>(); auto view = reg.view<ECS::JointComponent>();
for (auto [e, jc]: view.each()) { for (auto [e, jc]: view.each()) {
if (!jc.active) {
continue;
}
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
const auto found = m_Constraints.find(bits); const auto found = m_Constraints.find(bits);
if (found != m_Constraints.end()) { if (found != m_Constraints.end()) {
@@ -584,22 +602,26 @@ namespace Engine {
void PhysicsWorld::SyncCharacters() { void PhysicsWorld::SyncCharacters() {
auto &reg = m_Scene.GetRegistry(); auto &reg = m_Scene.GetRegistry();
// 1. 孤儿清理:实体销毁 / 组件被移除(Ref 析构即清理,无 body 负担) // 1. 孤儿清理:实体销毁 / 组件被移除或禁用(Ref 析构即清理,无 body 负担)
for (auto it = m_Characters.begin(); it != m_Characters.end();) { for (auto it = m_Characters.begin(); it != m_Characters.end();) {
const auto e = static_cast<entt::entity>(it->first); const auto e = static_cast<entt::entity>(it->first);
if (!reg.valid(e) || const auto* cc = reg.valid(e)
!reg.all_of<ECS::CharacterControllerComponent, ? reg.try_get<ECS::CharacterControllerComponent>(e) : nullptr;
ECS::TransformComponent>(e)) { if (cc == nullptr || !cc->active ||
!reg.all_of<ECS::TransformComponent>(e)) {
it = m_Characters.erase(it); it = m_Characters.erase(it);
} else { } else {
++it; ++it;
} }
} }
// 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送 // 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送;禁用 → 跳过
auto view = reg.view<ECS::CharacterControllerComponent, auto view = reg.view<ECS::CharacterControllerComponent,
ECS::TransformComponent>(); ECS::TransformComponent>();
for (auto [e, cc, tr]: view.each()) { for (auto [e, cc, tr]: view.each()) {
if (!cc.active) {
continue;
}
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
CharacterController::Params p; CharacterController::Params p;
p.radius = cc.radius; p.radius = cc.radius;
+3
View File
@@ -328,6 +328,7 @@ namespace Engine
u32 collectedCount = 0; u32 collectedCount = 0;
scene.Each<TransformComponent, LightComponent>( scene.Each<TransformComponent, LightComponent>(
[&](Entity, TransformComponent& t, LightComponent& light) { [&](Entity, TransformComponent& t, LightComponent& light) {
if (!light.active) return; // 组件禁用 = 不参与光照
if (collectedCount >= kMaxLights) return; if (collectedCount >= kMaxLights) return;
auto& dst = collected[collectedCount++]; auto& dst = collected[collectedCount++];
@@ -392,6 +393,7 @@ namespace Engine
m_EntitySkinBase.clear(); m_EntitySkinBase.clear();
scene.Each<MeshRendererComponent, AnimationComponent>( scene.Each<MeshRendererComponent, AnimationComponent>(
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) { [&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
const Model* model = assets.GetModel(mr.model); const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return; if (!model || !model->HasAnimations()) return;
const auto& clips = model->GetClips(); const auto& clips = model->GetClips();
@@ -513,6 +515,7 @@ namespace Engine
scene.Each<TransformComponent, MeshRendererComponent>( scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) { [&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
if (!mr.active) return; // 组件禁用 = 不渲染不拾取
Model* model = assets.GetModel(mr.model); Model* model = assets.GetModel(mr.model);
if (!model) return; if (!model) return;
+6 -1
View File
@@ -22,9 +22,14 @@ namespace Engine {
auto& reg = m_Scene.GetRegistry(); auto& reg = m_Scene.GetRegistry();
for (auto& [bits, inst] : m_Instances) { for (auto& [bits, inst] : m_Instances) {
const auto e = static_cast<entt::entity>(bits); const auto e = static_cast<entt::entity>(bits);
if (!reg.valid(e) || !reg.any_of<ECS::ScriptComponent>(e)) { const ECS::ScriptComponent* sc = reg.valid(e)
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
if (!sc) {
continue; // 延迟销毁队列里,下帧 Sync 清理 continue; // 延迟销毁队列里,下帧 Sync 清理
} }
if (!sc->active) {
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
}
if (!inst.created) { if (!inst.created) {
inst.behavior->OnCreate(); inst.behavior->OnCreate();
inst.created = true; inst.created = true;
@@ -218,6 +218,7 @@ namespace Engine::Serialization {
} }
out << YAML::Key << "Path" << YAML::Value << path; out << YAML::Key << "Path" << YAML::Value << path;
out << YAML::EndMap; // Model(flow) out << YAML::EndMap; // Model(flow)
out << YAML::Key << "Active" << YAML::Value << mr->active;
out << YAML::EndMap; // MeshRenderer out << YAML::EndMap; // MeshRenderer
} }
else { else {
@@ -233,6 +234,7 @@ namespace Engine::Serialization {
out << YAML::Key << "Playing" << YAML::Value << a->playing; out << YAML::Key << "Playing" << YAML::Value << a->playing;
out << YAML::Key << "Loop" << YAML::Value << a->loop; out << YAML::Key << "Loop" << YAML::Value << a->loop;
out << YAML::Key << "Speed" << YAML::Value << a->speed; out << YAML::Key << "Speed" << YAML::Value << a->speed;
out << YAML::Key << "Active" << YAML::Value << a->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -240,6 +242,7 @@ namespace Engine::Serialization {
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
<< YAML::BeginMap; << YAML::BeginMap;
out << YAML::Key << "Primary" << YAML::Value << c->primary; out << YAML::Key << "Primary" << YAML::Value << c->primary;
out << YAML::Key << "Active" << YAML::Value << c->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -254,6 +257,7 @@ namespace Engine::Serialization {
out << YAML::Key << "Radius" << YAML::Value << l->radius; out << YAML::Key << "Radius" << YAML::Value << l->radius;
out << YAML::Key << "CastShadow" << YAML::Value out << YAML::Key << "CastShadow" << YAML::Value
<< l->castShadow; << l->castShadow;
out << YAML::Key << "Active" << YAML::Value << l->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -304,6 +308,7 @@ namespace Engine::Serialization {
<< rb->restitution; << rb->restitution;
out << YAML::Key << "IsSensor" << YAML::Value << rb->isSensor; out << YAML::Key << "IsSensor" << YAML::Value << rb->isSensor;
// bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存 // bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存
out << YAML::Key << "Active" << YAML::Value << rb->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -325,6 +330,7 @@ namespace Engine::Serialization {
<< jc->enableLimit; << jc->enableLimit;
out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin; out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin;
out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax; out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax;
out << YAML::Key << "Active" << YAML::Value << jc->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -340,6 +346,7 @@ namespace Engine::Serialization {
<< cc->maxSlopeAngle; << cc->maxSlopeAngle;
out << YAML::Key << "StepHeight" << YAML::Value out << YAML::Key << "StepHeight" << YAML::Value
<< cc->stepHeight; << cc->stepHeight;
out << YAML::Key << "Active" << YAML::Value << cc->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -348,6 +355,7 @@ namespace Engine::Serialization {
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
out << YAML::Key << "ScriptName" << YAML::Value out << YAML::Key << "ScriptName" << YAML::Value
<< sc->scriptName; << sc->scriptName;
out << YAML::Key << "Active" << YAML::Value << sc->active;
out << YAML::EndMap; out << YAML::EndMap;
} }
@@ -520,6 +528,8 @@ namespace Engine::Serialization {
ref.Valid()) { ref.Valid()) {
handle.AddComponent<ECS::MeshRendererComponent>( handle.AddComponent<ECS::MeshRendererComponent>(
ECS::MeshRendererComponent{ref}); ECS::MeshRendererComponent{ref});
handle.GetComponent<ECS::MeshRendererComponent>().active =
n["Active"].as<bool>(true); // 缺省 true 向前兼容
} }
else { else {
ENGINE_WARN("SceneSerializer: model unresolved " ENGINE_WARN("SceneSerializer: model unresolved "
@@ -536,11 +546,13 @@ namespace Engine::Serialization {
a.playing = n["Playing"].as<bool>(a.playing); a.playing = n["Playing"].as<bool>(a.playing);
a.loop = n["Loop"].as<bool>(a.loop); a.loop = n["Loop"].as<bool>(a.loop);
a.speed = n["Speed"].as<f32>(a.speed); a.speed = n["Speed"].as<f32>(a.speed);
a.active = n["Active"].as<bool>(true);
} }
if (const YAML::Node n = comps["Camera"]) { if (const YAML::Node n = comps["Camera"]) {
auto& c = handle.AddComponent<ECS::CameraComponent>(); auto& c = handle.AddComponent<ECS::CameraComponent>();
c.primary = n["Primary"].as<bool>(false); c.primary = n["Primary"].as<bool>(false);
c.active = n["Active"].as<bool>(true);
} }
if (const YAML::Node n = comps["Light"]) { if (const YAML::Node n = comps["Light"]) {
@@ -553,6 +565,7 @@ namespace Engine::Serialization {
l.color = ReadVec4(n["Color"], l.color); l.color = ReadVec4(n["Color"], l.color);
l.radius = n["Radius"].as<f32>(l.radius); l.radius = n["Radius"].as<f32>(l.radius);
l.castShadow = n["CastShadow"].as<bool>(l.castShadow); l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
l.active = n["Active"].as<bool>(true);
} }
if (const YAML::Node n = comps["RigidBody"]) { if (const YAML::Node n = comps["RigidBody"]) {
@@ -604,6 +617,7 @@ namespace Engine::Serialization {
rb.friction = n["Friction"].as<f32>(rb.friction); rb.friction = n["Friction"].as<f32>(rb.friction);
rb.restitution = n["Restitution"].as<f32>(rb.restitution); rb.restitution = n["Restitution"].as<f32>(rb.restitution);
rb.isSensor = n["IsSensor"].as<bool>(rb.isSensor); rb.isSensor = n["IsSensor"].as<bool>(rb.isSensor);
rb.active = n["Active"].as<bool>(true);
// bodyId/bodyValid/velocity 保持默认(对账重建) // bodyId/bodyValid/velocity 保持默认(对账重建)
} }
@@ -623,6 +637,7 @@ namespace Engine::Serialization {
jc.enableLimit = n["EnableLimit"].as<bool>(jc.enableLimit); jc.enableLimit = n["EnableLimit"].as<bool>(jc.enableLimit);
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin); jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax); jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
jc.active = n["Active"].as<bool>(true);
} }
if (const YAML::Node n = comps["CharacterController"]) { if (const YAML::Node n = comps["CharacterController"]) {
@@ -634,6 +649,7 @@ namespace Engine::Serialization {
cc.mass = n["Mass"].as<f32>(cc.mass); cc.mass = n["Mass"].as<f32>(cc.mass);
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle); cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight); cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
cc.active = n["Active"].as<bool>(true);
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存 // 状态(速度/着地)由 PhysicsWorld 对账重建,不存
} }
@@ -641,6 +657,7 @@ namespace Engine::Serialization {
if (const YAML::Node n = comps["Script"]) { if (const YAML::Node n = comps["Script"]) {
auto& sc = handle.AddComponent<ECS::ScriptComponent>(); auto& sc = handle.AddComponent<ECS::ScriptComponent>();
sc.scriptName = n["ScriptName"].as<std::string>(""); sc.scriptName = n["ScriptName"].as<std::string>("");
sc.active = n["Active"].as<bool>(true);
} }
} }