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:
@@ -123,12 +123,14 @@ namespace Engine::ECS {
|
||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||
struct MeshRendererComponent {
|
||||
ModelRef model;
|
||||
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
|
||||
};
|
||||
|
||||
// 骨骼动画播放状态(纯数据 + 便捷控制)。
|
||||
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
|
||||
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
|
||||
struct AnimationComponent {
|
||||
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
|
||||
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
|
||||
bool playing = true;
|
||||
bool loop = true;
|
||||
@@ -158,6 +160,7 @@ namespace Engine::ECS {
|
||||
|
||||
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
|
||||
struct CameraComponent {
|
||||
bool active = true; // 禁用 = 退出 primary 候选
|
||||
Camera camera;
|
||||
bool primary = false;
|
||||
};
|
||||
@@ -166,6 +169,7 @@ namespace Engine::ECS {
|
||||
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
||||
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
||||
struct LightComponent {
|
||||
bool active = true; // 禁用 = 不进光照
|
||||
enum class Type : u8 { Directional = 0, Point = 1 };
|
||||
Type type = Type::Directional;
|
||||
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
||||
@@ -183,6 +187,7 @@ namespace Engine::ECS {
|
||||
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
|
||||
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
|
||||
struct RigidBodyComponent {
|
||||
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
|
||||
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
|
||||
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
|
||||
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
|
||||
@@ -241,6 +246,7 @@ namespace Engine::ECS {
|
||||
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
|
||||
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
|
||||
struct JointComponent {
|
||||
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
|
||||
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
|
||||
Type type = Type::Fixed;
|
||||
UUID target{}; // 目标实体(0 = 未设置,不建约束)
|
||||
@@ -264,6 +270,7 @@ namespace Engine::ECS {
|
||||
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
|
||||
// PhysicsWorld 的 CharacterController 里(对账重建)
|
||||
struct CharacterControllerComponent {
|
||||
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
|
||||
f32 radius = 0.3f; // 胶囊半径
|
||||
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
|
||||
f32 mass = 70.0f;
|
||||
@@ -278,6 +285,7 @@ namespace Engine::ECS {
|
||||
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
|
||||
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
||||
struct ScriptComponent {
|
||||
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
|
||||
std::string scriptName;
|
||||
|
||||
bool operator==(const ScriptComponent&) const = default;
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace Engine::ECS {
|
||||
CameraComponent* found = nullptr;
|
||||
Entity foundEntity = entt::null;
|
||||
m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
|
||||
if (!found && cc.primary) {
|
||||
if (!found && cc.primary && cc.active) { // 禁用相机退出候选
|
||||
found = &cc;
|
||||
foundEntity = e;
|
||||
}
|
||||
|
||||
@@ -356,6 +356,19 @@ namespace Engine {
|
||||
for (auto [e, rb, tr]: view.each()) {
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
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()) {
|
||||
bi.RemoveBody(found->second.id);
|
||||
@@ -437,12 +450,14 @@ namespace Engine {
|
||||
!= invalidated.end();
|
||||
};
|
||||
|
||||
// 1. owner 实体/组件没了,或任一端 body 本帧被销毁/重建 → 移除
|
||||
// 1. owner 实体/组件没了(或被禁用),或任一端 body 本帧被销毁/重建 → 移除
|
||||
for (auto it = m_Constraints.begin(); it != m_Constraints.end();) {
|
||||
const u32 owner = it->first;
|
||||
const auto ownerEnt = static_cast<entt::entity>(owner);
|
||||
const bool ownerGone = !reg.valid(ownerEnt) ||
|
||||
!reg.all_of<ECS::JointComponent>(ownerEnt);
|
||||
const ECS::JointComponent* jc =
|
||||
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)) {
|
||||
m_System->RemoveConstraint(it->second.constraint.GetPtr());
|
||||
it = m_Constraints.erase(it);
|
||||
@@ -451,10 +466,13 @@ namespace Engine {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 遍历组件:快照变更 → 重建;缺失 → 创建。
|
||||
// 2. 遍历组件:快照变更 → 重建;缺失 → 创建;禁用 → 跳过(上面已清)。
|
||||
// target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试)
|
||||
auto view = reg.view<ECS::JointComponent>();
|
||||
for (auto [e, jc]: view.each()) {
|
||||
if (!jc.active) {
|
||||
continue;
|
||||
}
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
const auto found = m_Constraints.find(bits);
|
||||
if (found != m_Constraints.end()) {
|
||||
@@ -584,22 +602,26 @@ namespace Engine {
|
||||
void PhysicsWorld::SyncCharacters() {
|
||||
auto ® = m_Scene.GetRegistry();
|
||||
|
||||
// 1. 孤儿清理:实体销毁 / 组件被移除(Ref 析构即清理,无 body 负担)
|
||||
// 1. 孤儿清理:实体销毁 / 组件被移除或禁用(Ref 析构即清理,无 body 负担)
|
||||
for (auto it = m_Characters.begin(); it != m_Characters.end();) {
|
||||
const auto e = static_cast<entt::entity>(it->first);
|
||||
if (!reg.valid(e) ||
|
||||
!reg.all_of<ECS::CharacterControllerComponent,
|
||||
ECS::TransformComponent>(e)) {
|
||||
const auto* cc = reg.valid(e)
|
||||
? reg.try_get<ECS::CharacterControllerComponent>(e) : nullptr;
|
||||
if (cc == nullptr || !cc->active ||
|
||||
!reg.all_of<ECS::TransformComponent>(e)) {
|
||||
it = m_Characters.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送
|
||||
// 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送;禁用 → 跳过
|
||||
auto view = reg.view<ECS::CharacterControllerComponent,
|
||||
ECS::TransformComponent>();
|
||||
for (auto [e, cc, tr]: view.each()) {
|
||||
if (!cc.active) {
|
||||
continue;
|
||||
}
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
CharacterController::Params p;
|
||||
p.radius = cc.radius;
|
||||
|
||||
@@ -328,6 +328,7 @@ namespace Engine
|
||||
u32 collectedCount = 0;
|
||||
scene.Each<TransformComponent, LightComponent>(
|
||||
[&](Entity, TransformComponent& t, LightComponent& light) {
|
||||
if (!light.active) return; // 组件禁用 = 不参与光照
|
||||
if (collectedCount >= kMaxLights) return;
|
||||
|
||||
auto& dst = collected[collectedCount++];
|
||||
@@ -392,6 +393,7 @@ namespace Engine
|
||||
m_EntitySkinBase.clear();
|
||||
scene.Each<MeshRendererComponent, AnimationComponent>(
|
||||
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
|
||||
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
|
||||
const Model* model = assets.GetModel(mr.model);
|
||||
if (!model || !model->HasAnimations()) return;
|
||||
const auto& clips = model->GetClips();
|
||||
@@ -513,6 +515,7 @@ namespace Engine
|
||||
|
||||
scene.Each<TransformComponent, MeshRendererComponent>(
|
||||
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
|
||||
if (!mr.active) return; // 组件禁用 = 不渲染不拾取
|
||||
Model* model = assets.GetModel(mr.model);
|
||||
if (!model) return;
|
||||
|
||||
|
||||
@@ -22,9 +22,14 @@ namespace Engine {
|
||||
auto& reg = m_Scene.GetRegistry();
|
||||
for (auto& [bits, inst] : m_Instances) {
|
||||
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 清理
|
||||
}
|
||||
if (!sc->active) {
|
||||
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
|
||||
}
|
||||
if (!inst.created) {
|
||||
inst.behavior->OnCreate();
|
||||
inst.created = true;
|
||||
|
||||
@@ -218,6 +218,7 @@ namespace Engine::Serialization {
|
||||
}
|
||||
out << YAML::Key << "Path" << YAML::Value << path;
|
||||
out << YAML::EndMap; // Model(flow)
|
||||
out << YAML::Key << "Active" << YAML::Value << mr->active;
|
||||
out << YAML::EndMap; // MeshRenderer
|
||||
}
|
||||
else {
|
||||
@@ -233,6 +234,7 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Playing" << YAML::Value << a->playing;
|
||||
out << YAML::Key << "Loop" << YAML::Value << a->loop;
|
||||
out << YAML::Key << "Speed" << YAML::Value << a->speed;
|
||||
out << YAML::Key << "Active" << YAML::Value << a->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -240,6 +242,7 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
|
||||
<< YAML::BeginMap;
|
||||
out << YAML::Key << "Primary" << YAML::Value << c->primary;
|
||||
out << YAML::Key << "Active" << YAML::Value << c->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -254,6 +257,7 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Radius" << YAML::Value << l->radius;
|
||||
out << YAML::Key << "CastShadow" << YAML::Value
|
||||
<< l->castShadow;
|
||||
out << YAML::Key << "Active" << YAML::Value << l->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -304,6 +308,7 @@ namespace Engine::Serialization {
|
||||
<< rb->restitution;
|
||||
out << YAML::Key << "IsSensor" << YAML::Value << rb->isSensor;
|
||||
// bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存
|
||||
out << YAML::Key << "Active" << YAML::Value << rb->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -325,6 +330,7 @@ namespace Engine::Serialization {
|
||||
<< jc->enableLimit;
|
||||
out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin;
|
||||
out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax;
|
||||
out << YAML::Key << "Active" << YAML::Value << jc->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -340,6 +346,7 @@ namespace Engine::Serialization {
|
||||
<< cc->maxSlopeAngle;
|
||||
out << YAML::Key << "StepHeight" << YAML::Value
|
||||
<< cc->stepHeight;
|
||||
out << YAML::Key << "Active" << YAML::Value << cc->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -348,6 +355,7 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
|
||||
out << YAML::Key << "ScriptName" << YAML::Value
|
||||
<< sc->scriptName;
|
||||
out << YAML::Key << "Active" << YAML::Value << sc->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -520,6 +528,8 @@ namespace Engine::Serialization {
|
||||
ref.Valid()) {
|
||||
handle.AddComponent<ECS::MeshRendererComponent>(
|
||||
ECS::MeshRendererComponent{ref});
|
||||
handle.GetComponent<ECS::MeshRendererComponent>().active =
|
||||
n["Active"].as<bool>(true); // 缺省 true 向前兼容
|
||||
}
|
||||
else {
|
||||
ENGINE_WARN("SceneSerializer: model unresolved "
|
||||
@@ -536,11 +546,13 @@ namespace Engine::Serialization {
|
||||
a.playing = n["Playing"].as<bool>(a.playing);
|
||||
a.loop = n["Loop"].as<bool>(a.loop);
|
||||
a.speed = n["Speed"].as<f32>(a.speed);
|
||||
a.active = n["Active"].as<bool>(true);
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["Camera"]) {
|
||||
auto& c = handle.AddComponent<ECS::CameraComponent>();
|
||||
c.primary = n["Primary"].as<bool>(false);
|
||||
c.active = n["Active"].as<bool>(true);
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["Light"]) {
|
||||
@@ -553,6 +565,7 @@ namespace Engine::Serialization {
|
||||
l.color = ReadVec4(n["Color"], l.color);
|
||||
l.radius = n["Radius"].as<f32>(l.radius);
|
||||
l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
|
||||
l.active = n["Active"].as<bool>(true);
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["RigidBody"]) {
|
||||
@@ -604,6 +617,7 @@ namespace Engine::Serialization {
|
||||
rb.friction = n["Friction"].as<f32>(rb.friction);
|
||||
rb.restitution = n["Restitution"].as<f32>(rb.restitution);
|
||||
rb.isSensor = n["IsSensor"].as<bool>(rb.isSensor);
|
||||
rb.active = n["Active"].as<bool>(true);
|
||||
// bodyId/bodyValid/velocity 保持默认(对账重建)
|
||||
}
|
||||
|
||||
@@ -623,6 +637,7 @@ namespace Engine::Serialization {
|
||||
jc.enableLimit = n["EnableLimit"].as<bool>(jc.enableLimit);
|
||||
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
|
||||
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
|
||||
jc.active = n["Active"].as<bool>(true);
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["CharacterController"]) {
|
||||
@@ -634,6 +649,7 @@ namespace Engine::Serialization {
|
||||
cc.mass = n["Mass"].as<f32>(cc.mass);
|
||||
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
||||
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
||||
cc.active = n["Active"].as<bool>(true);
|
||||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||
}
|
||||
|
||||
@@ -641,6 +657,7 @@ namespace Engine::Serialization {
|
||||
if (const YAML::Node n = comps["Script"]) {
|
||||
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
||||
sc.scriptName = n["ScriptName"].as<std::string>("");
|
||||
sc.active = n["Active"].as<bool>(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user