新增调试绘制体系:DebugDraw 服务 + 物理调试 pass + 脚本侧力学线

通用图元(线/箭头四翼头/球/胶囊/盒)瞬时+持久双缓冲聚合;
物理 pass 画刚体速度/重力/接触反力;角色 collider 与力学
线归位(引擎只画资产、脚本画行为);线宽按设备上限钳制
This commit is contained in:
2026-10-08 17:02:27 +08:00
parent ff955b9e63
commit 36b1fc0566
10 changed files with 903 additions and 44 deletions
+52
View File
@@ -0,0 +1,52 @@
//
// ScriptDebug.h:脚本侧力学调试封装(Unity Gizmos/Debug.DrawRay 的
// 游戏内同位物)。把调试惯例固化成默认值,脚本不用记参数序:
// - width = 像素(≥1),默认 3px
// - duration 恒 0(OnUpdate 每帧重画,事件类需求再裸调 Debug::)
// - depthTest 恒 false(跟角色跑的线被模型遮挡就失去意义)
// - headSize = 世界单位(箭头翼头空间尺寸),默认 0.08
//
#pragma once
#include <Engine/Debug/DebugDraw.h>
#include <glm/glm.hpp>
namespace ScriptDbg {
// 基础箭头:colorHex 语义色见下(重力红/支撑黄/意图蓝/真实绿)
inline void Arrow(const glm::vec3& from, const glm::vec3& to,
Engine::u32 colorHex, float width = 3.0f) {
Engine::Debug::Arrow(from, to, Engine::Debug::Rgb(colorHex),
/*headSize*/ 0.08f, /*width*/ width,
/*duration*/ 0.0f, /*depthTest*/ false);
}
// 重力:anchor=脚底,len=视觉长度。建议 ∝ gravityScale,且与支撑
// 力同长——静置时红(下)黄(上)等长 = 力平衡,一眼可读
inline void Gravity(const glm::vec3& anchor, float len) {
Arrow(anchor, anchor - glm::vec3(0.0f, len, 0.0f), 0xDC3C3C);
}
// 接触反力(法向支撑):只在有接触时存在——空中传 len=0 内部直接
// 不画(力学语义:离地即失支撑)
inline void Support(const glm::vec3& anchor, float len) {
if (len > 1e-3f) {
Arrow(anchor, anchor + glm::vec3(0.0f, len, 0.0f), 0xD9C33C);
}
}
// 加速度(深橙):脚本侧差分出的测量加速度(F=ma 的 a)。scale =
// m/s² → 世界长度换算;mag 低于 minMag 视为静置/匀速("没有线"=
// 没有加速度,本身就是信息),len 钳到 maxLen 防冲击尖峰爆屏
inline void Accel(const glm::vec3& anchor, const glm::vec3& accel,
float scale = 0.05f, float maxLen = 2.0f) {
const float mag = glm::length(accel);
if (mag > 0.1f) {
const float len = glm::min(mag * scale, maxLen);
Arrow(anchor, anchor + (accel / mag) * len, 0xE0640A);
}
}
} // namespace ScriptDbg
+174 -15
View File
@@ -1,10 +1,13 @@
//
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
//
// 移动语义分派:
// 移动语义分派(按实体配置自动选择,优先级从上到下):
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity
// CharacterController.Move 同构)
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
// - 无刚体 → 直接平移 Transform
// - 都没有 → 直接平移 Transform
//
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
@@ -12,14 +15,18 @@
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Core/Input.h>
#include <Engine/Debug/DebugDraw.h>
#include "ScriptDebug.h"
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine; // Input 门面(统一输入入口)
namespace {
@@ -28,10 +35,27 @@ namespace {
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(升降/跳跃归物理或将来扩展)。
// 键鼠查询统一走 Input 门面(脚本层零 GLFW 依赖)
// 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
// 键盘时门面静默。要手柄/运行时改键再换动作层
//(Input::ActionValue2("Move")——Unity6 新 Input System /
// UE5 Enhanced Input 对应物),本示例保持最简
const glm::quat cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
@@ -39,32 +63,167 @@ namespace {
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);
glm::vec3 move(0.0f);
if (Input::IsKeyDown(Input::Key::W)) move += fwd;
if (Input::IsKeyDown(Input::Key::S)) move -= fwd;
if (Input::IsKeyDown(Input::Key::D)) move += right;
if (Input::IsKeyDown(Input::Key::A)) move -= right;
// 约定: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 glm::vec3 vel = move * m_Speed;
// 走/跑
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; // 期望速度(输入意图)
if (auto* rb = TryGetComponent<Engine::ECS::RigidBodyComponent>();
auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
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<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
//(整体喂 0 会抵消重力 → 空中物体悬浮)
auto& phy = App().GetPhysics();
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z});
phy.SetLinearVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else {
Transform().SetPosition(Transform().GetPosition() + vel * dt);
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<Engine::ECS::AnimationComponent>();
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 = 5.0f; // m/s
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)