// // ControllerEditorPanel 实现:见头文件注释。 // #include "ControllerEditorPanel.h" #include #include #include #include #include #include namespace { ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); } // 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空) std::string FirstClipName(const Engine::Model* model) { if (model == nullptr || model->GetClips().empty()) return ""; return model->GetClips()[0].name; } // clip 引用编辑:选中实体有模型 → combo(模型 clip 列表); // 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩) bool ClipRefEdit(const char* label, std::string& clip, const Engine::Model* model) { if (model == nullptr || model->GetClips().empty()) { char buf[128]; snprintf(buf, sizeof(buf), "%s", clip.c_str()); ImGui::SetNextItemWidth(-1.0f); if (ImGui::InputText(label, buf, sizeof(buf))) { clip = buf; return true; } return false; } std::vector names; names.reserve(model->GetClips().size()); int sel = -1; for (const auto& c : model->GetClips()) { names.push_back(c.name.c_str()); if (c.name == clip) sel = static_cast(names.size()) - 1; } if (sel < 0) sel = 0; if (ImGui::Combo(label, &sel, names.data(), static_cast(names.size()))) { clip = model->GetClips()[static_cast(sel)].name; return true; } return false; } } // namespace void ControllerEditorPanel::Draw(const EditorContext& ctx) { if (!m_Open || m_Path.empty()) return; Engine::Animation::AnimationController* ctrl = ctx.assets->FindController(m_Path); if (!ImGui::Begin("Controller Editor", &m_Open)) { ImGui::End(); return; } if (ctrl == nullptr) { ImGui::TextColored(WarnCol(), "'%s' failed to load", m_Path.c_str()); if (ImGui::Button("Reload")) { ctx.assets->ReloadController(m_Path); } ImGui::End(); return; } // ---- 头部:路径 + Save(落盘 + Reload 重编译条件)---- ImGui::TextUnformatted(m_Path.c_str()); ImGui::SameLine(); if (ImGui::Button("Save")) { if (ctrl->Save(m_Path)) { ctx.assets->ReloadController(m_Path); ctrl = ctx.assets->FindController(m_Path); } } ImGui::SameLine(); ImGui::TextDisabled("edits apply live; Save writes the file"); ImGui::Separator(); DrawPreview(ctx); ImGui::Separator(); DrawStates(ctx, ctrl); ImGui::Separator(); DrawTransitions(ctx, ctrl); ImGui::Separator(); DrawParams(ctx, ctrl); ImGui::End(); } // ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)---- void ControllerEditorPanel::DrawPreview(const EditorContext& ctx) { // 选中实体的 AnimationComponent(Controller 模式才驱动) Engine::ECS::AnimationComponent* anim = nullptr; const Engine::Model* model = nullptr; if (ctx.selected != nullptr && ctx.scene != nullptr && ctx.scene->GetRegistry().valid(*ctx.selected)) { auto& reg = ctx.scene->GetRegistry(); if (auto* a = reg.try_get(*ctx.selected)) { anim = a; if (auto* mr = reg.try_get( *ctx.selected)) { if (mr->model.Valid()) { model = ctx.assets->GetModel(mr->model); } } } } if (anim == nullptr) { ImGui::TextDisabled("select an entity with an Animation component " "to preview"); return; } Engine::Animation::AnimationController* ctrl = anim->controllerPath.empty() ? nullptr : ctx.assets->FindController(anim->controllerPath); if (ctrl == nullptr) return; // 面板选中状态的 2D 树(预览读出;拖动写的是实体参数) const Engine::Animation::ControllerState* padState = nullptr; const auto& order = ctrl->GetStateOrder(); if (m_SelState >= 0 && m_SelState < static_cast(order.size())) { const auto& s = ctrl->GetStates().at(order[static_cast( m_SelState)]); if (s.tree.type == Engine::Animation::BlendTree::Type::Directional2D) { padState = &s; } } if (padState == nullptr) { ImGui::TextDisabled("preview: select a 2DDirectional state"); return; } const float S = 190.0f; const float rad = S * 0.5f - 14.0f; const ImVec2 org = ImGui::GetCursorScreenPos(); const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255)); dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f); dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y), IM_COL32(60, 60, 68, 255)); dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad), IM_COL32(60, 60, 68, 255)); const auto& ch = padState->tree.children; for (const auto& c : ch) { const bool isOrigin = c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f; const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad); dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f, isOrigin ? IM_COL32(90, 140, 255, 255) : IM_COL32(220, 220, 225, 255)); } // mode=Direct 的实体不跑状态机——参数写进去没有任何效果, // 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时 // 参数面板不可编辑,同语义) const bool isCtrlMode = anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller; if (!isCtrlMode) { ImGui::TextColored(WarnCol(), "entity is in Direct mode - switch Mode to " "Controller to preview"); return; } ImGui::InvisibleButton("##dirpad", ImVec2(S, S)); const auto getF = [&](const std::string& n) -> float { auto it = anim->floatParams.find(n); return it != anim->floatParams.end() ? it->second : 0.0f; }; float cx = getF(padState->tree.xParam); float cz = getF(padState->tree.zParam); if (ImGui::IsItemActivated()) { // 预览闭环(Unity Animator 窗口语义:选中状态即可预览该 // 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D // 树状态,否则 SetFloat 对 Single 状态完全无效 if (anim->currentState != order[static_cast(m_SelState)]) { anim->CrossFade(order[static_cast(m_SelState)], 0.1f); } } if (ImGui::IsItemActive()) { const ImVec2 m = ImGui::GetIO().MousePos; float vx = (m.x - ctr.x) / rad; float vz = -(m.y - ctr.y) / rad; const float len = glm::length(glm::vec2(vx, vz)); if (len > 1.0f) { vx /= len; vz /= len; } anim->SetFloat(padState->tree.xParam, vx); anim->SetFloat(padState->tree.zParam, vz); cx = vx; cz = vz; } else if (ImGui::IsItemDeactivatedAfterEdit()) { anim->SetFloat(padState->tree.xParam, 0.0f); anim->SetFloat(padState->tree.zParam, 0.0f); cx = cz = 0.0f; } if (ImGui::IsItemHovered()) { const ImVec2 m = ImGui::GetIO().MousePos; for (const auto& c : ch) { const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad); const float dx = m.x - p.x; const float dy = m.y - p.y; if (dx * dx + dy * dy < 64.0f) { ImGui::SetTooltip("%s", c.clip.c_str()); break; } } } const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad); dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255)); dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f); // 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学) ImGui::SameLine(); ImGui::BeginGroup(); ImGui::Text("preview: %s", anim->currentState.c_str()); ImGui::Text("%.2f / %.2f", cx, cz); std::vector> ws; Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws); std::sort(ws.begin(), ws.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); for (const auto& [idx, w] : ws) { ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w); } ImGui::EndGroup(); } // ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ---- void ControllerEditorPanel::DrawStates( const EditorContext& ctx, Engine::Animation::AnimationController* ctrl) { if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen)) { return; } auto& order = ctrl->StateOrderMut(); auto& states = ctrl->StatesMut(); // 选中实体模型(children 的 clip 下拉来源) const Engine::Model* model = nullptr; if (ctx.selected != nullptr && ctx.scene != nullptr && ctx.scene->GetRegistry().valid(*ctx.selected)) { if (auto* mr = ctx.scene->GetRegistry() .try_get( *ctx.selected)) { if (mr->model.Valid()) { model = ctx.assets->GetModel(mr->model); } } } // 状态列表 + Add int delIdx = -1; for (int i = 0; i < static_cast(order.size()); ++i) { const bool isEntry = order[static_cast(i)] == ctrl->GetEntryState(); ImGui::PushID(i); if (ImGui::Selectable( (order[static_cast(i)] + std::string(isEntry ? " [entry]" : "")) .c_str(), i == m_SelState)) { m_SelState = i; } ImGui::PopID(); } { static char s_NewName[64] = ""; ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName)); ImGui::SameLine(); if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' && states.find(s_NewName) == states.end()) { const std::string n = s_NewName; states.emplace(n, Engine::Animation::ControllerState{n}); order.push_back(n); m_SelState = static_cast(order.size()) - 1; s_NewName[0] = '\0'; } } if (m_SelState >= static_cast(order.size())) { m_SelState = static_cast(order.size()) - 1; } if (m_SelState < 0 || order.empty()) { ImGui::TreePop(); return; } // ---- 选中状态详情 ---- const std::string selName = order[static_cast(m_SelState)]; auto it = states.find(selName); if (it == states.end()) { ImGui::TreePop(); return; } auto& st = it->second; ImGui::Indent(); ImGui::TextUnformatted("editing:"); ImGui::SameLine(); ImGui::TextUnformatted(selName.c_str()); if (selName != ctrl->GetEntryState() && ImGui::SmallButton("Set as Entry")) { ctrl->SetEntry(selName); } ImGui::SameLine(); ImGui::PushID("delstate"); if (ImGui::SmallButton("x Delete")) { delIdx = m_SelState; } ImGui::PopID(); // 重命名(级联:transitions / transitionsOnEnd / entry 引用同步) { static char s_Rename[64] = ""; ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##rename", s_Rename, sizeof(s_Rename)); ImGui::SameLine(); if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' && states.find(s_Rename) == states.end()) { const std::string oldN = selName; const std::string newN = s_Rename; auto node = states.extract(oldN); if (node) { node.key() = newN; states.insert(std::move(node)); order[static_cast(m_SelState)] = newN; for (auto& t : ctrl->TransitionsMut()) { if (t.from == oldN) t.from = newN; if (t.to == oldN) t.to = newN; } for (auto& [n, s] : states) { for (auto& e : s.transitionsOnEnd) { if (e == oldN) e = newN; } } if (ctrl->GetEntryState() == oldN) { ctrl->SetEntry(newN); } s_Rename[0] = '\0'; } } } // ---- 播放分组(speed/loop/播完出口)---- if (ImGui::TreeNode("Playback")) { ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f"); ImGui::Checkbox("loop", &st.loop); if (!st.loop) { // 播完出口列表(按序首个可用)+ 下拉追加 for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei) { ImGui::PushID(static_cast(ei) + 1000); ImGui::BulletText("%s", st.transitionsOnEnd[ei].c_str()); ImGui::SameLine(); if (ImGui::SmallButton("x")) { st.transitionsOnEnd.erase( st.transitionsOnEnd.begin() + static_cast(ei)); ImGui::PopID(); break; } ImGui::PopID(); } if (!order.empty()) { static int s_OnEndSel = 0; s_OnEndSel = std::clamp( s_OnEndSel, 0, static_cast(order.size()) - 1); ImGui::SetNextItemWidth(120.0f); ImGui::Combo( "##onend", &s_OnEndSel, [](void* d, int i) -> const char* { return (*static_cast*>( d))[static_cast(i)] .c_str(); }, &order, static_cast(order.size())); ImGui::SameLine(); if (ImGui::SmallButton("+ exit")) { st.transitionsOnEnd.push_back( order[static_cast(s_OnEndSel)]); } } } ImGui::TreePop(); } // ---- 混合树分组 ---- if (ImGui::TreeNode("Blend Tree")) { using BTType = Engine::Animation::BlendTree::Type; // 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向 int tt = static_cast(st.tree.type); if (ImGui::Combo("type", &tt, "Single\0Directional 1D\0Directional 2D\0")) { st.tree.type = static_cast(tt); st.tree.children.clear(); if (st.tree.type == BTType::Single) { if (st.tree.clip.empty()) { st.tree.clip = FirstClipName(model); } } else { // 默认一个原点 child(clip 取模型首个;无模型 = 手填) st.tree.children.push_back( {FirstClipName(model), glm::vec2{0.0f, 0.0f}}); } } // x/z 参数绑定(float 参数下拉) { std::vector pnames; for (const auto& p : ctrl->GetParams()) { if (p.type == Engine::Animation::ControllerParam::Type::Float) { pnames.push_back(p.name.c_str()); } } if (!pnames.empty()) { if (st.tree.type != BTType::Single) { int xi = 0; for (int k = 0; k < static_cast(pnames.size()); ++k) { if (st.tree.xParam == pnames[static_cast(k)]) xi = k; } ImGui::SetNextItemWidth(140.0f); if (ImGui::Combo("x param", &xi, pnames.data(), static_cast(pnames.size()))) { st.tree.xParam = pnames[static_cast(xi)]; } } if (st.tree.type == BTType::Directional2D) { int zi = 0; for (int k = 0; k < static_cast(pnames.size()); ++k) { if (st.tree.zParam == pnames[static_cast(k)]) zi = k; } ImGui::SetNextItemWidth(140.0f); if (ImGui::Combo("z param", &zi, pnames.data(), static_cast(pnames.size()))) { st.tree.zParam = pnames[static_cast(zi)]; } } } else if (st.tree.type != BTType::Single) { ImGui::TextColored( WarnCol(), "need a float param for the blend axis"); } } if (st.tree.type == BTType::Single) { ClipRefEdit("clip", st.tree.clip, model); } else { // 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤 // 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是 // T-pose),但"中心=默认动作"的直觉语义要靠原点点承载 if (st.tree.type == BTType::Directional2D) { bool hasOrigin = false; for (const auto& c : st.tree.children) { if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f) hasOrigin = true; } if (!hasOrigin) { ImGui::TextColored( WarnCol(), "! no origin child (at 0,0) - center " "falls back to first child"); } } // children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点; // 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例) int delChild = -1; for (int ci = 0; ci < static_cast(st.tree.children.size()); ++ci) { auto& c = st.tree.children[static_cast(ci)]; ImGui::PushID(ci); ClipRefEdit("##clip", c.clip, model); if (st.tree.type == BTType::Directional2D) { ImGui::SetNextItemWidth(70.0f); ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f, 1.0f, "x %.2f"); ImGui::SameLine(); ImGui::SetNextItemWidth(70.0f); ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f, 1.0f, "y %.2f"); } else { ImGui::SetNextItemWidth(120.0f); ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f, 10.0f, "thr %.2f"); } ImGui::SameLine(); if (ImGui::SmallButton("x")) delChild = ci; ImGui::PopID(); } if (delChild >= 0) { st.tree.children.erase( st.tree.children.begin() + delChild); } if (ImGui::SmallButton("+ Add Child")) { st.tree.children.push_back( {FirstClipName(model), st.tree.type == BTType::Directional2D ? glm::vec2{1.0f, 0.0f} : glm::vec2{0.0f, 0.0f}}); } } ImGui::TreePop(); // Blend Tree } ImGui::Unindent(); if (delIdx >= 0) { const std::string name = order[static_cast(delIdx)]; states.erase(name); order.erase(order.begin() + delIdx); auto& trs = ctrl->TransitionsMut(); for (int i = static_cast(trs.size()) - 1; i >= 0; --i) { if (trs[static_cast(i)].from == name || trs[static_cast(i)].to == name) { trs.erase(trs.begin() + i); } } for (auto& [n, s] : states) { std::erase_if(s.transitionsOnEnd, [&](const std::string& e) { return e == name; }); } if (ctrl->GetEntryState() == name) { ctrl->SetEntry(order.empty() ? "" : order[0]); } m_SelState = 0; } ImGui::TreePop(); } // ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)---- void ControllerEditorPanel::DrawTransitions( const EditorContext& ctx, Engine::Animation::AnimationController* ctrl) { (void)ctx; if (!ImGui::TreeNode("Transitions")) return; const auto& order = ctrl->GetStateOrder(); std::vector snames; snames.reserve(order.size() + 1); snames.push_back("*"); // AnyState for (const auto& n : order) snames.push_back(n.c_str()); auto stateCombo = [&](const char* label, std::string& cur) { int sel = 0; for (int k = 0; k < static_cast(snames.size()); ++k) { if (cur == snames[static_cast(k)]) sel = k; } ImGui::SetNextItemWidth(110.0f); if (ImGui::Combo(label, &sel, snames.data(), static_cast(snames.size()))) { cur = snames[static_cast(sel)]; } }; int delIdx = -1; int ti = 0; for (auto& t : ctrl->TransitionsMut()) { ImGui::PushID(ti++); // 行 1:来源 -> 目标(+ 删除) stateCombo("##from", t.from); ImGui::SameLine(); ImGui::TextUnformatted("->"); ImGui::SameLine(); stateCombo("##to", t.to); ImGui::SameLine(); if (ImGui::SmallButton("x")) delIdx = ti - 1; // 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组) ImGui::Indent(18.0f); // cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译 { const std::string s = t.cond.always ? "" : Engine::Animation::AnimationController::CondToString( t.cond); char buf[96]; snprintf(buf, sizeof(buf), "%s", s.c_str()); ImGui::SetNextItemWidth(170.0f); if (ImGui::InputText("##cond", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit()) { t.cond = Engine::Animation::AnimationController::ParseCond( buf); } if (!t.cond.always && !t.cond.valid) { ImGui::SameLine(); ImGui::TextColored(WarnCol(), "!"); } } ImGui::SameLine(); ImGui::SetNextItemWidth(60.0f); ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f"); ImGui::SameLine(); ImGui::Checkbox("##exit", &t.hasExitTime); if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time"); ImGui::Unindent(18.0f); ImGui::PopID(); } if (delIdx >= 0) { auto& trs = ctrl->TransitionsMut(); trs.erase(trs.begin() + delIdx); } if (ImGui::SmallButton("+ Add") && !order.empty()) { Engine::Animation::Transition tr; tr.from = order[0]; tr.to = order[order.size() > 1 ? 1 : 0]; ctrl->TransitionsMut().push_back(tr); } ImGui::TreePop(); } // ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)---- void ControllerEditorPanel::DrawParams( const EditorContext& ctx, Engine::Animation::AnimationController* ctrl) { (void)ctx; if (!ImGui::TreeNode("Params")) return; auto& ps = ctrl->ParamsMut(); int delIdx = -1; int pi = 0; for (auto& p : ps) { ImGui::PushID(pi++); ImGui::TextUnformatted(p.name.c_str()); ImGui::SameLine(); int ty = static_cast(p.type); ImGui::SetNextItemWidth(80.0f); if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0")) { p.type = static_cast(ty); } ImGui::SameLine(); if (p.type == Engine::Animation::ControllerParam::Type::Float) { ImGui::SetNextItemWidth(90.0f); ImGui::DragFloat("##def", &p.defFloat, 0.05f); } else if (p.type == Engine::Animation::ControllerParam::Type::Bool) { ImGui::Checkbox("##def", &p.defBool); } ImGui::SameLine(); if (ImGui::SmallButton("x")) delIdx = pi - 1; ImGui::PopID(); } if (delIdx >= 0) ps.erase(ps.begin() + delIdx); { static char s_NewName[64] = ""; ImGui::SetNextItemWidth(120.0f); ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName)); ImGui::SameLine(); if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0') { bool dup = false; for (const auto& p : ps) { if (p.name == s_NewName) dup = true; } if (!dup) { Engine::Animation::ControllerParam d; d.name = s_NewName; ps.push_back(d); s_NewName[0] = '\0'; } } } ImGui::TreePop(); }