// // WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。 // // 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 + // 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作 //(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。 // // 移动语义分派(按实体配置自动选择,优先级从上到下): // - CharacterControllerComponent → 角色控制器路径(扫掠运动学: // 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity // CharacterController.Move 同构) // - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理—— // 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义) // - 都没有 → 直接平移 Transform // // 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读 // 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。 // #include #include #include #include #include #include "ScriptDebug.h" #include "ScriptLog.h" #include #include #include #include #include using namespace Engine; // Input 门面(统一输入入口) namespace { class WASDMove final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "WASDMove"; } void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { pb.Field("Speed", &m_Speed, 3.0f); // 走路 m/s pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s pb.Field("TurnResponse", &m_TurnResponse, 12.0f); // 转向阻尼 pb.Field("AccelResponse", &m_AccelResponse, 8.0f); // 加速/转向响应 pb.Field("StopResponse", &m_StopResponse, 14.0f); // 松手制动响应 pb.Field("JumpSpeed", &m_JumpSpeed, 6.0f); // 起跳初速 m/s // clip 按名引用(Unity anim.Play("Walk") 语义)——名字在 // Inspector 里改,attach 顺序变了脚本零改动 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 { SLOG_TRACE("hello"); SLOG_INFO("hello"); SLOG_DEBUG("hello"); SLOG_WARN("hello"); SLOG_ERROR("hello"); SLOG_INFO("[WASDMove] 就绪:走 {:.1f} m/s / 跑 {:.1f} m/s / 跳 {:.1f} m/s", m_Speed, m_RunSpeed, m_JumpSpeed); } void OnUpdate(float dt) override { // 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene // view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。 // 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input // 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 + // 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘 // 手柄一套代码,将来改键/加绑定不进脚本 // 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它 // 的朝向——玩家看到的视角就是移动参考);无相机实体时 //(纯独立形态)回退编辑器相机 glm::quat cq; bool refCam = false; auto camView = App().GetScene() .GetRegistry() .view(); for (auto e : camView) { const auto& cam = camView.get(e); if (cam.primary && cam.active) { cq = App().GetScene() .GetRegistry() .get(e) .GetRotationQuat(); refCam = true; break; } } if (!refCam) { cq = App().GetRenderer() .GetEditorCamera() .GetTransform() .GetRotationQuat(); } const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f); const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f); // 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X) const glm::vec2 mvRaw = Input::ActionValue2("Move"); glm::vec2 mv = mvRaw; if (glm::length(mv) > 1.0f) mv = glm::normalize(mv); glm::vec3 move = fwd * (-mv.y) + right * mv.x; move.y = 0.0f; // 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度: // 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速) if (glm::length(move) > 1.0f) { move = glm::normalize(move); } // 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺 // 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0, // 那样判会把走路误判成跑 const bool running = Input::ActionDown("Run"); const float speed = running ? m_RunSpeed : m_Speed; const glm::vec3 desired = move * speed; // 期望速度(输入意图) auto& phy = App().GetPhysics(); const bool hasCC = TryGetComponent() != nullptr; const bool grounded = hasCC ? phy.IsCharacterGrounded(EntityBits()) : true; // ---- 动量整形(惯性的本体,纯脚本):真实速度逐帧逼近期 // 望速度——起步渐快、转向沿弧线漂移、折返经零减速再反向、 // 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的 // StopResponse(角色要"想停就停");空中操纵性降到 1/4 //(起跳后不宜急转弯)。指数逼近,帧率无关 const bool moving = glm::length(mvRaw) > 0.1f; float response = moving ? m_AccelResponse : m_StopResponse; if (!grounded) { response *= 0.25f; } m_PlanarVel += (desired - m_PlanarVel) * (1.0f - std::exp(-response * dt)); if (glm::length(desired - m_PlanarVel) < 0.02f) { m_PlanarVel = desired; // 贴齐,防低速蠕动 } // ---- 路径 1:角色控制器(挂了 CharacterControllerComponent)---- // 水平喂动量整形后的真实速度;Y 保留现值——重力由控制器每 // 步自动积分,每帧覆写 Y 会把下落速度清零(空中变悬浮) if (hasCC) { const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits()); phy.SetCharacterVelocity( EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z}); // 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘 // Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳 if (Input::ActionPressed("Jump") && phy.IsCharacterGrounded(EntityBits())) { phy.Jump(EntityBits(), m_JumpSpeed); SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)", m_JumpSpeed); } } else if (auto* rb = TryGetComponent(); rb != nullptr && rb->bodyValid && rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { // 物理驱动:只覆写水平速度,Y 保留物理积分离散结果 //(整体喂 0 会抵消重力 → 空中物体悬浮) const glm::vec3 cur = phy.GetLinearVelocity(EntityBits()); phy.SetLinearVelocity( EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z}); } else { Transform().SetPosition(Transform().GetPosition() + m_PlanarVel * dt); } // ---- 朝向:面向"真实速度"方向(不是输入方向,模型默认 +Z)---- // 这是消"平移感"的关键:动量整形后速度滞后于输入,若面朝 // 输入方向,身体朝前脚却在侧滑——正是割裂感的来源。面朝 // 真实速度 = 脚底朝向与位移一致,转向呈现弧线漂移。速度 // 低于阈值(起步瞬间/折返过零)锁定上帧朝向:折返表现成 // U 弯回转而不是原地瞬转。只取水平面(角色不俯仰翻滚) 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))); } // ---- 动画:按真实速度分档(迟滞防临界抖)---- // 判据 = 动量整形后的真实水平速度,不是输入原始值:起步 // 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与 // 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞 //(进 0.9×RunSpeed / 退 0.7×RunSpeed):摇杆在阈值附近晃 // 不抖档。引擎 Play(名) 自带相位对齐 + crossfade,切档 // 突兀感只剩判据本身的问题 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)) { // fade 0.35s:locomotion 混合的经验值(Unity 默认 // 0.25 太短,0.5+ 显拖沓);相位同步在引擎侧做 m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试) } } const glm::vec3 anchor = (hasCC ? phy.GetCharacterPosition(EntityBits()) : Transform().GetPosition()) + glm::vec3(0.0f, 0.05f, 0.0f); // ---- 运动线(ScriptDbg 封装:像素线宽 3px / 当帧瞬时 / // 透视直显已固化为默认值,不用记参数序):蓝 = 输入期 // 望速度,绿 = 动量整形后的真实速度——同一起点,夹角 // 即惯性滞量(转向弧线/U 弯/刹车可视化) ScriptDbg::Arrow(anchor, anchor + desired * 0.2f, 0x2B5FD0); ScriptDbg::Arrow(anchor, anchor + m_PlanarVel * 0.2f, 0x1E9E42); // ---- 力学线:重力(红,下)+ 接触反力(黄,上,仅着地)。 // 长度 ∝ gravityScale:静置时红黄等长 = 力平衡;起跳 // 离地黄箭头消失 = 离地即失支撑;gravityScale 调大两 // 箭同步变长(重量感可视化) float gs = 1.0f; if (auto* cc = TryGetComponent< Engine::ECS::CharacterControllerComponent>()) { gs = cc->gravityScale; } const float gLen = 0.5f * gs; ScriptDbg::Gravity(anchor, gLen); ScriptDbg::Support(anchor, grounded ? gLen : 0.0f); const glm::vec3 vel = hasCC ? phy.GetCharacterVelocity(EntityBits()) : m_PlanarVel; if (dt > 1e-5f) { m_AccelS = glm::mix(m_AccelS, (vel - m_PrevVel) / dt, 0.35f); } m_PrevVel = vel; ScriptDbg::Accel(anchor, m_AccelS); } void DebugShow() { } private: float m_Speed = 3.0f; // 走路 m/s(声明默认;存档值覆盖) float m_RunSpeed = 6.0f; // 跑步 m/s(Shift 按住) float m_TurnResponse = 12.0f; // 转向阻尼强度(越大越跟手) float m_AccelResponse = 8.0f; // 加速/转向响应(动量整形,越小越"重") float m_StopResponse = 14.0f; // 松手制动响应(越大刹车越硬) glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度(动量状态) float m_JumpSpeed = 6.0f; // 起跳初速(配合组件 gravityScale 调手感) std::string m_IdleClip = "idle"; // clip 名(Inspector 可改) std::string m_WalkClip = "walk"; std::string m_RunClip = "run"; std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准) glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准) glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图) }; ENGINE_REGISTER_SCRIPT(WASDMove) } // namespace