// // WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。 // // 移动语义分派(按实体配置自动选择,优先级从上到下): // - CharacterControllerComponent → 角色控制器路径(扫掠运动学: // 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity // CharacterController.Move 同构) // - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理—— // 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义) // - 都没有 → 直接平移 Transform // // 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读 // 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。 // #include #include #include #include #include #include "ScriptDebug.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 OnUpdate(float dt) override { // 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene // view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。 // 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获 // 键盘时门面静默。要手柄/运行时改键再换动作层 //(Input::ActionValue2("Move")——Unity6 新 Input System / // UE5 Enhanced Input 对应物),本示例保持最简 const glm::quat 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) glm::vec2 mv(0.0f); if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f; if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f; if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f; if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f; if (glm::length(mv) > 1.0f) mv = glm::normalize(mv); glm::vec3 move = fwd * (-mv.y) + right * mv.x; move.y = 0.0f; if (glm::length(move) > 1e-4f) { move = glm::normalize(move); } // 走/跑 const bool moving = glm::length(mv) > 0.1f; const bool running = moving && Input::IsKeyDown(Input::Key::LeftShift); 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 //(起跳后不宜急转弯)。指数逼近,帧率无关 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}); // 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住 // 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防 // 空中连跳 const bool jumpDown = Input::IsKeyDown(Input::Key::Space); if (jumpDown && !m_JumpWasDown && phy.IsCharacterGrounded(EntityBits())) { phy.Jump(EntityBits(), m_JumpSpeed); } m_JumpWasDown = jumpDown; } 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))); } // ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)---- // 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以 // 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无 // 条件 Play 会不停重置播放头,动画永远播不完 const std::string& target = !moving ? m_IdleClip : (running ? 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(边沿检测基准) bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测) glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准) glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图) }; ENGINE_REGISTER_SCRIPT(WASDMove) } // namespace