Files
HelloVulkan/App/editor/InspectorPanel.cpp
T
atdunbg b6d0868e15 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).
2026-10-03 18:45:03 +08:00

676 lines
26 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// InspectorPanel.cpp:组件编辑实现。
//
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
//
#include "InspectorPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include <imgui.h>
#include <cstdio>
#include <vector>
#include <type_traits>
namespace
{
// InputText 需要 char 缓冲;提交时写回 std::string
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
// 任一轴变化返回 true
bool DragVec3Row(const char* label, glm::vec3& v, float speed,
float mn = -3.4e38f, float mx = 3.4e38f)
{
ImGui::TextUnformatted(label);
ImGui::SameLine(72.0f); // 固定标签列宽 → 三行输入框对齐
ImGui::PushID(label);
constexpr ImVec4 axisCol[3] = {
{0.72f, 0.24f, 0.24f, 1.00f}, // X 红
{0.30f, 0.68f, 0.34f, 1.00f}, // Y 绿
{0.24f, 0.40f, 0.88f, 1.00f}, // Z 蓝
};
const float boxW =
(ImGui::GetContentRegionAvail().x
- 2.0f * ImGui::GetStyle().ItemInnerSpacing.x) / 3.0f;
bool changed = false;
for (int i = 0; i < 3; ++i)
{
ImGui::PushStyleColor(ImGuiCol_FrameBg, axisCol[i]);
ImGui::SetNextItemWidth(boxW);
changed |= ImGui::DragFloat(
i == 0 ? "##x" : i == 1 ? "##y" : "##z",
&v[i], speed, mn, mx, "%.2f");
ImGui::PopStyleColor();
if (i < 2) ImGui::SameLine();
}
ImGui::PopID();
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)
{
ImGui::Begin("Inspector");
auto& reg = ctx.scene->GetRegistry();
if (selected == entt::null || !reg.valid(selected))
{
ImGui::TextDisabled("(no entity selected)");
ImGui::End();
return;
}
// ---- Name ----
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
{
InputTextString("Name", name->name);
}
ImGui::Separator();
// ---- Add Component(已挂载项置灰:Unity Add Component 语义)----
if (ImGui::Button("Add Component"))
{
ImGui::OpenPopup("add_component");
}
if (ImGui::BeginPopup("add_component"))
{
auto menuItem = [&](auto typeTag, const char* label)
{
using T = std::remove_pointer_t<decltype(typeTag)>;
const bool has = reg.all_of<T>(selected);
if (has)
{
ImGui::BeginDisabled(true);
}
if (ImGui::MenuItem(label))
{
reg.emplace<T>(selected);
}
if (has)
{
ImGui::EndDisabled();
}
};
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::Separator();
// ---- Transform(结构性组件:不可删、无启停)----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{
if (ComponentHeader("Transform", nullptr, false, nullptr))
{
glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 派生欧拉(度)
if (DragVec3Row("rotation", rot, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (DragVec3Row("scale", scl, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
}
}
// ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{
bool removeMr = false;
if (ComponentHeader("MeshRenderer", &mr->active, true, &removeMr))
{
if (!mr->active)
{
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
}
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
if (!mr->active)
{
ImGui::EndDisabled();
}
}
if (removeMr)
{
reg.remove<Engine::ECS::MeshRendererComponent>(selected);
}
}
// ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{
bool removeLight = false;
if (ComponentHeader("Light", &light->active, true, &removeLight))
{
if (!light->active)
{
ImGui::BeginDisabled(true);
}
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
}
ImGui::ColorEdit3("color", &light->color.x);
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
if (light->type == Engine::ECS::LightComponent::Type::Directional)
{
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("castShadow", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
if (!light->active)
{
ImGui::EndDisabled();
}
}
if (removeLight)
{
reg.remove<Engine::ECS::LightComponent>(selected);
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
bool removeCam = false;
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
{
if (!cam->active)
{
ImGui::BeginDisabled(true);
}
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为游戏形态渲染视角)
reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c)
{
if (e != selected)
{
c.primary = false;
}
});
}
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
// SceneRenderer,编辑器形态恒用编辑器相机)
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
{
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
selected))
{
ctx.editorCamera->AlignTo(*tr);
}
}
if (!cam->active)
{
ImGui::EndDisabled();
}
}
if (removeCam)
{
reg.remove<Engine::ECS::CameraComponent>(selected);
}
}
// ---- RigidBody(Jolt 刚体:动态体 Transform 由物理每帧写回)----
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
{
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);
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
{
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
}
int collider = static_cast<int>(rb->collider);
if (ImGui::Combo("collider", &collider,
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
{
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
}
if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Box)
{
ImGui::DragFloat3("halfExtents", &rb->boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Sphere)
{
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
{
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
{
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
}
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
{
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
ImGui::SameLine();
if (ImGui::Button("+ add"))
{
rb->compound.emplace_back();
}
int remove = -1;
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
{
auto& c = rb->compound[i];
ImGui::PushID(i);
ImGui::Separator();
int sh = static_cast<int>(c.shape);
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
{
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
}
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
{
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
}
else
{
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
{
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
}
}
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
if (ImGui::Button("remove"))
{
remove = i;
}
ImGui::PopID();
}
if (remove >= 0)
{
rb->compound.erase(rb->compound.begin() + remove);
}
}
else
{
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
// Dynamic=凸包近似),无尺寸可调
ImGui::TextDisabled("follows MeshRenderer geometry");
}
ImGui::DragFloat("mass", &rb->mass, 0.1f, 0.1f, 1000.0f, "%.1f");
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
ImGui::Checkbox("isSensor", &rb->isSensor);
if (rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
{
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
rb->velocity.x, rb->velocity.y, rb->velocity.z);
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
if (ImGui::Button("Apply Impulse"))
{
ctx.app->GetPhysics().AddImpulse(
static_cast<uint32_t>(entt::to_integral(selected)),
s_debugImpulse);
}
}
if (!rb->active)
{
ImGui::EndDisabled();
}
}
if (removeRb)
{
reg.remove<Engine::ECS::RigidBodyComponent>(selected);
}
}
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
{
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);
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
{
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
}
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
std::string items;
std::vector<Engine::UUID> uuids;
int current = -1;
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().each(
[&](entt::entity e, const Engine::ECS::IdComponent& id,
const Engine::ECS::NameComponent& n)
{
if (e == selected)
{
return; // 自连无意义
}
if (id.uuid == jc->target)
{
current = static_cast<int>(uuids.size());
}
uuids.push_back(id.uuid);
items.append(n.name);
items.push_back('\0');
});
items.push_back('\0'); // ImGui Combo 双 \0 结尾
if (ImGui::Combo("target", &current, items.c_str()) && current >= 0)
{
jc->target = uuids[static_cast<size_t>(current)];
}
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
{
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
ImGui::Checkbox("enable limit", &jc->enableLimit);
if (jc->enableLimit)
{
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.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 运行时)----
if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected))
{
bool removeCc = false;
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)
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
ImGui::DragFloat("max slope deg", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
ImGui::DragFloat("step height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
// 遥测(只读):运行时状态在 PhysicsWorld 的 CharacterController
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
{
const uint32_t bits =
static_cast<uint32_t>(entt::to_integral(selected));
ImGui::Text("grounded: %s",
phy.IsCharacterGrounded(bits) ? "yes" : "no");
const glm::vec3 v = phy.GetCharacterVelocity(bits);
ImGui::Text("vel: %.2f, %.2f, %.2f m/s", v.x, v.y, v.z);
// 跳跃调试按钮
if (ImGui::Button("Jump"))
{
phy.Jump(bits, 5.0f);
}
}
if (!cc->active)
{
ImGui::EndDisabled();
}
}
if (removeCc)
{
reg.remove<Engine::ECS::CharacterControllerComponent>(selected);
}
}
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
{
bool removeSc = false;
if (ComponentHeader("Script", &sc->active, true, &removeSc))
{
if (!sc->active)
{
ImGui::BeginDisabled(true);
}
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
const auto& names = Engine::Scripting::ScriptRegistry::Names();
std::vector<const char*> items;
items.push_back("<none>");
for (const auto& n : names)
{
items.push_back(n.c_str());
}
int cur = 0;
for (size_t i = 0; i < names.size(); ++i)
{
if (names[i] == sc->scriptName)
{
cur = static_cast<int>(i) + 1;
}
}
if (ImGui::Combo("script", &cur, items.data(),
static_cast<int>(items.size())))
{
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
}
if (sc->scriptName.empty())
{
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 ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{
bool removeAnim = false;
if (ComponentHeader("Animation", &anim->active, true, &removeAnim))
{
if (!anim->active)
{
ImGui::BeginDisabled(true);
}
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
// 依赖提示:动画作用在 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();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
for (const auto& c : clips)
{
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
ImGui::SameLine();
ImGui::Checkbox("loop", &anim->loop);
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
if (anim->clipIndex >= 0 &&
anim->clipIndex < static_cast<int>(clips.size()))
{
ImGui::Text("t: %.2f / %.2f s", anim->time,
clips[anim->clipIndex].duration);
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
if (!anim->active)
{
ImGui::EndDisabled();
}
}
if (removeAnim)
{
reg.remove<Engine::ECS::AnimationComponent>(selected);
}
}
ImGui::End();
}