// // Follower.cpp:跟随者行为(NPC 自动跟随主角,UE AIController 的简化位)。 // // 挂在每个 NPC 实体上(各自 CharacterController + Animation)。主角由 // TargetName 指定(默认 "player",按 NameComponent 名称匹配——引擎无实体 // 引用参数类型,名称匹配是标准做法)。逻辑分四层,与 WASDMove 同一套手感: // - 距离闸门(迟滞):拉开到 StopDistance+ResumeGap 才起步,靠近到 // StopDistance 即停——"到附近就停止跟随",边界不抖动 // - 速度分档(迟滞):距离超过 RunDistance 改跑,回落到 0.85× 才退回 // 走——"距离越大走/跑越快" // - 动量整形:真实速度逐帧逼近期望速度(起步渐快、到点刹停),帧率无关 // - 动画按真实速度分档(idle/walk/run),迟滞防抖 // 移动驱动走 PhysicsWorld 的 CharacterController 门面(水平喂意图速度, // Y 归物理重力);无 CC 时回退直接平移。多个跟随者用 Offset 错开站位。 // #include #include #include #include "ScriptLog.h" #include #include #include #include using namespace Engine; namespace { class Follower final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "Follower"; } void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { pb.Field("TargetName", &m_TargetName, std::string("player")); // 相对目标的站位偏移(世界系;多个跟随者错开,防叠一起) pb.Field("Offset", &m_Offset, glm::vec3{0.0f}); pb.Field("WalkSpeed", &m_WalkSpeed, 3.0f); // 走路 m/s pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s pb.Field("StopDistance", &m_StopDistance, 2.5f); // 到点停(米) pb.Field("ResumeGap", &m_ResumeGap, 1.5f); // 再起步的距离余量 pb.Field("RunDistance", &m_RunDistance, 10.0f); // 超过即改跑(米) pb.Field("TurnResponse", &m_TurnResponse, 10.0f);// 转向阻尼 pb.Field("AccelResponse", &m_AccelResponse, 6.0f); // 加速响应 pb.Field("StopResponse", &m_StopResponse, 12.0f); // 制动响应 // clip 按名引用(Unity anim.Play("Walk") 语义) pb.Field("IdleClip", &m_IdleClip, std::string("idle")); pb.Field("WalkClip", &m_WalkClip, std::string("walk")); pb.Field("RunClip", &m_RunClip, std::string("run")); } void OnStart() override { if (ResolveTarget()) { SLOG_INFO("[Follower] 跟随目标 '{}' 就绪", m_TargetName); } else { SLOG_WARN("[Follower] 未找到目标 '{}'(按 NameComponent 匹配)", m_TargetName); } } void OnUpdate(float dt) override { auto& reg = App().GetScene().GetRegistry(); // 目标失效(被销毁/未就绪)→ 重解析;仍无则静止待命 if (m_Target == entt::null || !reg.valid(m_Target)) { m_Target = entt::null; if (!ResolveTarget()) { return; } } const glm::vec3 tp = reg.get(m_Target) .GetPosition(); const glm::vec3 self = Transform().GetPosition(); glm::vec3 delta = (tp + m_Offset) - self; delta.y = 0.0f; // 只取水平面(角色不俯仰) const float dist = glm::length(delta); // ---- 距离闸门(迟滞):到点停、拉开距离再起步 ---- if (!m_Moving) { if (dist > m_StopDistance + m_ResumeGap) { m_Moving = true; } } else if (dist <= m_StopDistance) { m_Moving = false; } // ---- 速度分档(迟滞):远则跑、近则走 ---- glm::vec3 desired{0.0f}; if (m_Moving && dist > 1e-4f) { if (!m_Sprinting && dist >= m_RunDistance) { m_Sprinting = true; } else if (m_Sprinting && dist <= m_RunDistance * 0.85f) { m_Sprinting = false; } const float speed = m_Sprinting ? m_RunSpeed : m_WalkSpeed; desired = (delta / dist) * speed; } // ---- 动量整形:真实速度逐帧逼近期望(起步渐快/到点刹停)---- const float response = glm::length(desired) > 0.01f ? m_AccelResponse : m_StopResponse; m_PlanarVel += (desired - m_PlanarVel) * (1.0f - std::exp(-response * dt)); if (glm::length(desired - m_PlanarVel) < 0.02f) { m_PlanarVel = desired; // 贴齐,防低速蠕动 } // ---- 驱动:角色控制器喂水平速度(Y 归物理);无则直接平移 ---- auto& phy = App().GetPhysics(); const bool hasCC = TryGetComponent() != nullptr; if (hasCC) { const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits()); phy.SetCharacterVelocity( EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z}); } else { Transform().SetPosition(self + m_PlanarVel * dt); } // ---- 朝向:面向真实速度方向(模型默认 +Z),低速锁定 ---- if (glm::length(m_PlanarVel) > 0.3f) { const float targetYaw = std::atan2(m_PlanarVel.x, m_PlanarVel.z); const glm::vec3 modelFwd = Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f); const float curYaw = std::atan2(modelFwd.x, modelFwd.z); const float dyaw = std::atan2(std::sin(targetYaw - curYaw), std::cos(targetYaw - curYaw)); const float k = 1.0f - std::exp(-m_TurnResponse * dt); Transform().SetRotationQuat( glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0))); } // ---- 动画:按真实速度分档(迟滞防临界抖)---- const float planarSpeed = glm::length(m_PlanarVel); std::string target; if (planarSpeed < 0.15f) { target = m_IdleClip; } else if (m_CurClip == m_RunClip) { target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip : m_WalkClip; } else { target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip : m_WalkClip; } if (target != m_CurClip) { if (auto* anim = TryGetComponent(); anim != nullptr && anim->Play(target, 0.35f)) { m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试) } } } private: // 按 NameComponent 名称解析目标(跳过自身);失败返回 false bool ResolveTarget() { auto& reg = App().GetScene().GetRegistry(); auto view = reg.view(); for (auto e : view) { if (static_cast(entt::to_integral(e)) == EntityBits()) { continue; } if (view.get(e).name == m_TargetName) { m_Target = e; return true; } } return false; } std::string m_TargetName = "player"; glm::vec3 m_Offset{0.0f}; float m_WalkSpeed = 3.0f; float m_RunSpeed = 6.0f; float m_StopDistance = 2.5f; float m_ResumeGap = 1.5f; float m_RunDistance = 10.0f; float m_TurnResponse = 10.0f; float m_AccelResponse = 6.0f; float m_StopResponse = 12.0f; std::string m_IdleClip = "idle"; std::string m_WalkClip = "walk"; std::string m_RunClip = "run"; entt::entity m_Target = entt::null; // 运行时状态(非参数) bool m_Moving = false; // 距离闸门状态(迟滞) bool m_Sprinting = false; // 速度档状态(迟滞) glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度 std::string m_CurClip = "idle"; // 当前 clip(边沿检测基准) }; ENGINE_REGISTER_SCRIPT(Follower) } // namespace