Files
HelloVulkan/Engine/include/Engine/ECS/Components.h
T
atdunbg e1cef53158 physics: character controller (Jolt CharacterVirtual)
- CharacterControllerComponent: capsule params (radius/halfHeight, mass,
  max slope, step height); entity origin = feet via shape offset
- CharacterController per-entity wrapper: gravity integration + grounded
  clamp, ExtendedUpdate (sweep/slide + stick-to-floor + walk-stairs),
  SetLinearVelocity/Jump/IsGrounded facade
- PhysicsWorld: SyncCharacters reconcile (create/rebuild on param
  change, editor-drag teleport), UpdateCharacters after physics Update
  (Jolt recommended order), writes back to Transform
- Inspector: params + grounded/velocity telemetry + jump debug button
- Serialization: params only, state rebuilt by reconcile
2026-10-03 16:34:20 +08:00

280 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Created by atdunbg on 2026/9/22.
//
// Components:内置组件集合。
// 组件是纯数据(POD 倾向),逻辑在系统/应用层。
//
#ifndef HELLOVULKAN_COMPONENTS_H
#define HELLOVULKAN_COMPONENTS_H
#include <Engine/Core/Types.h>
#include <Engine/Core/UUID.h>
#include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <algorithm>
#include <string>
#include <vector>
namespace Engine {
class Model;
}
namespace Engine::ECS {
// 实体稳定身份:CreateEntity 时自动分配,用于序列化/跨引用。
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent {
UUID uuid;
operator UUID() const { return uuid; }
};
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
struct NameComponent {
std::string name;
};
// 世界变换:UE USceneComponent 的 encapsulation 模式——
// SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
// 层级(父子变换)留给后续的层级同步系统。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent {
// ---- 读取 ----
const glm::vec3& GetPosition() const { return m_Position; }
const glm::quat& GetRotationQuat() const { return m_Rotation; }
const glm::vec3& GetScale() const { return m_Scale; }
const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算
// 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
// x = asin(-R[2][1])、y = atan2(R[2][0], R[2][2])、z = atan2(R[0][1], R[1][1])。
// cx 在 atan2 分子分母中约掉,pitch 贴 ±90° 依然精确
glm::vec3 GetRotation() const
{
const glm::mat3 R = glm::mat3_cast(m_Rotation);
const f32 sp = std::clamp(-R[2][1], -1.0f, 1.0f); // = sin(pitch)
if (std::abs(sp) < 1.0f) {
return glm::degrees(glm::vec3(
std::asin(sp),
std::atan2(R[2][0], R[2][2]),
std::atan2(R[0][1], R[1][1])));
}
// ±90° 万向锁:yaw/roll 只剩组合角,约定 roll=0(UE 同策略)
// 归入 yaw;两支组合角符号不同
return sp > 0.0f
? glm::vec3(90.0f,
glm::degrees(std::atan2(-R[0][2], R[1][2])), 0.0f)
: glm::vec3(-90.0f,
glm::degrees(std::atan2(-R[0][2], -R[1][2])), 0.0f);
}
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
// 那是 ZYX 航向序,语义不同)
TransformComponent& SetRotation(const glm::vec3& r)
{
const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0));
const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0));
const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1));
return SetRotationQuat(qy * qx * qz);
}
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
// 不经欧拉往返 → 零漂移、无万向锁
TransformComponent& SetRotationQuat(const glm::quat& q) { if (m_Rotation != q) { m_Rotation = q; m_Dirty = true; } return *this; }
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
bool IsDirty() const { return m_Dirty; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动
TransformComponent& Compose()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
m_World = t * glm::mat4_cast(m_Rotation) * s;
m_Dirty = false;
return *this;
}
private:
glm::vec3 m_Position{0.0f};
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
glm::vec3 m_Scale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
};
// 网格渲染体:引用共享的 Model 资产。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
struct MeshRendererComponent {
ModelRef model;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// ---- crossfade(clip 切换平滑过渡)----
// 来源 clip 淡出期间与目标 clip 并行采样,局部 TRS 层混合
//(palette 数量不变,只是求值端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) return;
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
}
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
Camera camera;
bool primary = false;
};
// 灯光。类型:
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent {
enum class Type : u8 { Directional = 0, Point = 1 };
Type type = Type::Directional;
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f}; // rgb = 颜色,a = 强度
f32 radius = 10.0f; // 仅点光:衰减半径
bool castShadow = false; // 方向光可投影(引擎取第一个 castShadow 方向光为 CSM 光源)
};
// 刚体(Jolt 后端)。纯数据 POD——头文件零 Jolt 依赖,
// bodyId 存 JPH::BodyID 的裸位模式,翻译在 PhysicsWorld。
// 生命周期:PhysicsWorld::SyncScene 每帧对账(创建/销毁/参数同步),
// 动态体的 Transform 由物理写回(Writeback 标脏),静态/运动学的
// Transform 由编辑器驱动同步进 body。
// 扩展路线:软体/布料 = SoftBodyComponent(Jolt SoftBodySharedSettings),
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
struct RigidBodyComponent {
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
enum class ColliderType : u8 {
Box = 0, Sphere = 1, Mesh = 2,
Capsule = 3, Cylinder = 4, Compound = 5
};
// Compound 子形状:局部 pos/rot 相对刚体原点,不递归嵌套
//(child 不能再是 Compound)。尺寸语义同顶层(× Transform 缩放烘焙)
struct CompoundChild {
enum class Shape : u8 { Box = 0, Sphere = 1, Capsule = 2 };
Shape shape = Shape::Box;
glm::vec3 boxHalfExtents{0.25f, 0.25f, 0.25f};
f32 radius = 0.25f; // Sphere 半径 / Capsule 半径
f32 capsuleHalfHeight = 0.25f; // 仅 Capsule:圆柱段半高
glm::vec3 position{0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
bool operator==(const CompoundChild&) const = default;
};
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
ColliderType collider = ColliderType::Box;
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
// 程序化模型顶点烘焙尺寸,不能从 scale 反推。
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
f32 sphereRadius = 0.5f;
f32 capsuleRadius = 0.5f; // Unity CapsuleCollider 默认值同构
f32 capsuleHalfHeight = 0.5f;
f32 cylinderRadius = 0.5f;
f32 cylinderHalfHeight = 0.5f;
std::vector<CompoundChild> compound; // 仅 ColliderType::Compound
// 动态体参数
f32 mass = 1.0f;
f32 gravityScale = 1.0f;
f32 friction = 0.5f;
f32 restitution = 0.2f;
// 传感器(Unity isTrigger 语义):只产生 Added/Removed 事件,无碰撞解算
bool isSensor = false;
// 运行时(PhysicsWorld 维护,勿手改)
u32 bodyId = 0; // JPH::BodyID 裸位
bool bodyValid = false;
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
};
// 约束/关节(Jolt TwoBodyConstraint 家族)。纯数据 POD 零 Jolt 依赖;
// 创建/对账在 PhysicsWorld::SyncConstraints(kPhyDbgConstraints 出图元)。
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
struct JointComponent {
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
Type type = Type::Fixed;
UUID target{}; // 目标实体(0 = 未设置,不建约束)
// 锚点:各体 Transform 局部系(Unity connectedAnchor 语义,
// × 本体缩放烘焙)。Fixed/Hinge/Ball 都连这两点
glm::vec3 anchorA{0.0f}; // 本体(挂组件的实体)
glm::vec3 anchorB{0.0f}; // 目标体
// Hinge 专用:转轴(本体局部系,方向量不吃缩放)+ 角度限位(度)
glm::vec3 hingeAxis{1.0f, 0.0f, 0.0f};
bool enableLimit = false;
f32 limitMin = -90.0f; // Jolt 语义 min∈[-180,0] max∈[0,180]
f32 limitMax = 90.0f;
bool operator==(const JointComponent&) const = default;
};
// 角色控制器参数(Jolt CharacterVirtual 扫掠运动学,非刚体——不受
// 物理模拟驱动,游戏逻辑经 PhysicsWorld 门面喂速度/跳跃)。本组件
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
// PhysicsWorld 的 CharacterController 里(对账重建)
struct CharacterControllerComponent {
f32 radius = 0.3f; // 胶囊半径
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
};
} // namespace Engine::ECS
#endif //HELLOVULKAN_COMPONENTS_H