Files
HelloVulkan/App/editor/InspectorPanel.cpp
T
atdunbg 7d02d344e5 fix(scripts): prevent duplicate script attachment per entity
The per-entity script list allowed picking the same script class
twice; the name-keyed runtime silently ignores duplicates, leaving
the extra rows dead weight with unusable enable bits. Enforce
DisallowMultipleComponent semantics: the dropdown of each row only
offers unattached names (own name kept for echo), "+ Add Script"
disables when every registered script is attached, and scene loading
dedupes as a safety net for hand-edited files.
2026-10-04 00:57:45 +08:00

733 lines
28 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("isTrigger", &rb->isTrigger);
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);
}
// 多脚本列表(Unity 语义):每行 = 脚本下拉 + 独立启停 + 删除;
// 实例生命周期由 ScriptRuntime 对账(删行/改名 → 下帧 OnDestroy)
const auto& names = Engine::Scripting::ScriptRegistry::Names();
int removeIdx = -1;
for (int i = 0; i < static_cast<int>(sc->items.size()); ++i)
{
auto& item = sc->items[static_cast<size_t>(i)];
ImGui::PushID(i);
// 去重下拉(DisallowMultipleComponent 语义):可选项 =
// "<none>" + 未挂名 + 自身名(已挂的别人的名不出现,结构上
// 选不出重复);自身名置于末尾保证已挂项正常回显
std::vector<const char*> options;
options.push_back("<none>");
for (const auto& n : names)
{
if (n == item.name)
{
continue;
}
const bool attached = std::any_of(
sc->items.begin(), sc->items.end(),
[&](const Engine::ECS::ScriptComponent::Item& it)
{ return it.name == n; });
if (!attached)
{
options.push_back(n.c_str());
}
}
if (!item.name.empty())
{
options.push_back(item.name.c_str());
}
int cur = 0;
for (size_t k = 0; k < options.size(); ++k)
{
if (item.name == options[k])
{
cur = static_cast<int>(k);
}
}
ImGui::SetNextItemWidth(
ImGui::GetContentRegionAvail().x - 64.0f);
if (ImGui::Combo("##script", &cur, options.data(),
static_cast<int>(options.size())))
{
item.name = (cur <= 0) ? "" : options[static_cast<size_t>(cur)];
}
ImGui::SameLine();
ImGui::Checkbox("##en", &item.active);
ImGui::SameLine();
if (ImGui::Button("-", ImVec2(24.0f, 0.0f)))
{
removeIdx = i;
}
ImGui::PopID();
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
if (!item.name.empty() && cur == 0)
{
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
"! not registered: %s",
item.name.c_str());
}
}
if (removeIdx >= 0)
{
sc->items.erase(sc->items.begin() + removeIdx);
}
// 已注册脚本全部挂上 → 禁止再加(去重语义,无意义项)
const bool allAttached = !names.empty() &&
std::all_of(names.begin(), names.end(),
[&](const std::string& n)
{
return std::any_of(
sc->items.begin(), sc->items.end(),
[&](const Engine::ECS::ScriptComponent::Item& it)
{ return it.name == n; });
});
if (allAttached)
{
ImGui::BeginDisabled(true);
ImGui::Button("+ Add Script");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::TextDisabled("all registered scripts attached");
}
else if (ImGui::Button("+ Add Script"))
{
sc->items.push_back({});
}
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();
}