Files
HelloVulkan/Engine/include/Engine/ECS/Components.h
T

531 lines
27 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 <Engine/Scripting/ScriptParam.h>
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <algorithm>
#include <string>
#include <unordered_map>
#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 FTransform 语义):TRS 分量 = 相对父的 **local** 值
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent {
// ---- local 读取(相对父;UI/游戏逻辑用)----
const glm::vec3& GetPosition() const { return m_Position; }
const glm::quat& GetRotationQuat() const { return m_Rotation; }
const glm::vec3& GetScale() const { return m_Scale; }
// ---- world 缓存读取(要求已由 UpdateTransforms 计算)----
const glm::mat4& World() const { return m_World; }
const glm::vec3& GetWorldPosition() const { return m_WorldPosition; }
const glm::quat& GetWorldRotationQuat() const { return m_WorldRotation; }
const glm::vec3& GetWorldScale() const { return m_WorldScale; }
// 欧拉角(度)视图:从 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)
// 万向锁分支阈值 1e-6:asin 在 |sin|→1 处导数发散(sin90 的
// float 表示差 6e-8 → asin 放大成 0.02° 显示噪声,用户实测
// 90 → 89.98)。贴锁区改走组合角 atan2(分子分母同源缩放,
// 比值稳定)→ 手输 90.0 显示 90.0。落入该窄区(±0.00057°)
// 的真实 yaw/roll 被归并为组合角,UI 输入不可达,可接受
glm::vec3 euler = std::abs(sp) < 1.0f - 1e-6f
? glm::degrees(glm::vec3(
std::asin(sp),
std::atan2(R[2][0], R[2][2]),
std::atan2(R[0][1], R[1][1])))
: (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));
// 显示约定 = UE Details 的 0-360(-90 → 270)。逐轴 fmod wrap
// 与 [-180,180] 数学严格等价(每轴旋转周期 360°),矩阵重建类
// 消费端(Camera::EulerRotMat 等)不受影响;需要连续域做
// clamp/lerp 的调用方自行 wrap 回 [-180,180](如自由视角)
for (int i = 0; i < 3; ++i) {
euler[i] = std::fmod(euler[i], 360.0f);
if (euler[i] < 0.0f) euler[i] += 360.0f;
if (euler[i] >= 360.0f) euler[i] = 0.0f; // 浮点边界
}
return euler;
}
// ---- 写入(标脏,脏实体由 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 航向序,语义不同)。输入域无限制:三角函数周期性
// 自动归一(720 ≡ -45 ≡ 315),编辑器拖动累计值直接喂
TransformComponent& SetRotation(const glm::vec3& r)
{
return SetRotationQuat(QuatFromEuler(r));
}
// 欧拉(度)→ quat 的唯一转换点(qy·qx·qz)。static 供编辑器
// mirror 的旋转等价检测复用(同一公式构造 → dot 判定严格)
static glm::quat QuatFromEuler(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 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; }
// 显式标脏:层级系统改父/挂载后触发 world 重算用
//(local 值未变但 world 缓存已失效)
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
// 勿手动
TransformComponent& Compose()
{
m_WorldPosition = m_Position;
m_WorldRotation = m_Rotation;
m_WorldScale = m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
// 子实体版:world = 父 world SRT 组合(UE FTransform 语义):
// wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl
// 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。
// 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动
TransformComponent& ComposeFromParent(const TransformComponent& parent)
{
m_WorldPosition = parent.m_WorldPosition +
parent.m_WorldRotation * (parent.m_WorldScale * m_Position);
m_WorldRotation = parent.m_WorldRotation * m_Rotation;
m_WorldScale = parent.m_WorldScale * m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
private:
void RebuildWorldMatrix()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition);
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale);
m_World = t * glm::mat4_cast(m_WorldRotation) * s;
}
// ---- local TRS(相对父)----
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};
// ---- world 缓存(UpdateTransforms 重算)----
glm::vec3 m_WorldPosition{0.0f};
glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 m_WorldScale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
};
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
struct HierarchyComponent {
entt::entity parent = entt::null; // entt::null = 根实体
std::vector<entt::entity> children; // 挂载顺序
};
// 网格渲染体:引用共享的 Model 资产。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
//
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
struct MeshRendererComponent {
ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
// 单 mesh 子引用:-1 = 整模型全部;N = 只画第 N 个
//(GetMeshes 下标)。物理 Mesh 碰撞体自动跟随(BuildMeshShape
// 读本组件)。序列化缺省键 = -1 向前兼容
int meshIndex = -1;
std::unordered_map<u32, std::string> materialOverrides;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)——Unity Animator 组件 /
// UE SkeletalMeshComponent 动画侧的对应物。
//
// 职责分层(2026-10-06 重构,对齐 UE/Unity):
// Model(资产:node/bone/clip)→ AnimationComponent(状态,本结构)
// → Animation::AnimationSystem(调度:推进播放头 + 姿态求值 +
// palette 产出)→ SceneRenderer(渲染只读查表消费)。
// 本组件不含任何求值逻辑;ImGui 面板经 Play()/字段直接驱动。
// 无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
// ---- 播放设置(序列化落盘)----
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
// 根运动开关(Unity Animator "Apply Root Motion" 同语义):
// true = 保留根节点位移轨道(跑动 clip 自带位移会漂出锚点);
// false = 原地播放(丢弃动画子树根节点位移,旋转保留)——
// 位移归脚本/物理驱动的角色(CharacterController 等)用 false
bool applyRootMotion = true;
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
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; // 过渡总时长(秒)
// 按名播放的运行时反指(瞬态,不落盘不序列化):AnimationSystem
// 每帧回填(同 RigidBody::bodyValid 模式)。空 = 模型未就绪
const Model* runtimeModel = nullptr;
// ---- 资产引用(序列化落盘)----
// 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。
// clips 本体运行时并入 Model 资产(共享);重启后由 Inspector
// 按此列表幂等重放
std::vector<std::string> attachedAnimPaths;
// ---- Controller 模式(Unity Animator 语义)----
// Direct = 现状单 clip 直控(clipIndex/playing 等);Controller =
// .animctrl 状态机驱动(参数 + 过渡 + BlendTree)。两模式字段
// 独立,切换互不清除(来回切配置不丢)
enum class AnimMode : u8 { Direct = 0, Controller = 1 };
AnimMode mode = AnimMode::Direct; // 落盘;缺键回退 Direct
std::string controllerPath; // .animctrl 资产路径(落盘)
// 参数实例值(落盘 = 设计意图:调好的值重启还在;缺键 = 用
// Controller 声明的默认值)。Trigger 是一次性信号不落盘
std::unordered_map<std::string, float> floatParams;
std::unordered_map<std::string, bool> boolParams;
std::vector<std::string> pendingTriggers; // 消费即清(瞬态)
// 阻尼参数推进状态(瞬态,不落盘——重启从 floatParams 当前值
// 重新起步):current 逐帧指数逼近 target,写回 floatParams 供
// 状态机条件统一读取
struct DampedParam {
float current = 0.0f;
float target = 0.0f;
float response = 8.0f; // 逼近速率(1/s,越大越快)
};
std::unordered_map<std::string, DampedParam> dampedParams;
// 状态机瞬态(不落盘,重启从 entry state 开始)
std::string currentState; // 空 = 未初始化(System 首帧置 entry)
f32 stateTime = 0.0f; // 当前状态播放头(秒)
std::string transFrom; // 过渡来源状态(空 = 无过渡)
f32 transFromTime = 0.0f; // 来源状态播放头(过渡期间继续推进)
f32 transElapsed = 0.0f; // 已过渡时长
f32 transFade = 0.25f; // 过渡总时长(秒)
// ---- 脚本/编辑器共用 API(Unity Animator.SetXxx 语义)----
void SetFloat(const std::string& name, float value)
{
floatParams[name] = value;
}
// 阻尼变体(Unity SetFloat(name, target, dampTime) 同构):登记
// 目标值,AnimationSystem Controller 驱动期逐帧指数逼近(帧率
// 无关)。首次登记从 floatParams 当前值起步(无则取目标),
// 避免跳变。与 SetFloat 同名混用 = 直接 set 会被下一帧阻尼值
// 覆盖,别混
void SetFloatDamped(const std::string& name, float target, float response)
{
auto [it, inserted] = dampedParams.try_emplace(name);
it->second.target = target;
it->second.response = glm::max(response, 0.01f);
if (inserted) {
const auto fp = floatParams.find(name);
it->second.current =
fp != floatParams.end() ? fp->second : target;
}
}
void SetBool(const std::string& name, bool value)
{
boolParams[name] = value;
}
// 一次性信号:被状态机条件评估消费后清除(同名去重——误配资产
// 无消费条件时防每帧 SetTrigger 无限堆积)
void SetTrigger(const std::string& name)
{
if (std::find(pendingTriggers.begin(), pendingTriggers.end(),
name) == pendingTriggers.end()) {
pendingTriggers.push_back(name);
}
}
// 强制切状态(CrossFade 语义:来源状态淡出、目标状态淡入,
// fade<=0 = 硬切)。编辑器下拉与脚本共用
void CrossFade(const std::string& stateName, f32 fadeSeconds)
{
if (stateName == currentState) return;
transFrom = currentState;
transFromTime = stateTime;
transElapsed = 0.0f;
transFade = glm::max(fadeSeconds, 1e-3f);
currentState = stateName;
stateTime = 0.0f;
}
// 切换 clip 并开始播放(Unity Animator.Play 语义:指派即播,
// 编辑态选中 clip 直接预览;crossfade 越界/空 clip 保护在消费端)。
// 脚本 API 与编辑器 Inspector 共用同一入口
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) {
playing = true; // 重复指派 = 确保在播
return;
}
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
playing = true;
}
// 按名播放(Unity Animator.Play("Walk") 同形,推荐):clip 名经
// runtimeModel 的 clip 表解析,消掉下标硬耦合(attach 顺序变了
// 脚本不改)。未命中/模型未就绪返回 false。实现住
// AnimationSystem.cpp——需要 Model 完整类型,组件头保持零资产
// 依赖(Model.h 拖 vulkan/assimp,进本头会污染全部包含者)
bool Play(const std::string& clipName, f32 fadeSeconds = 0.3f);
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
bool active = true; // 禁用 = 退出 primary 候选
Camera camera;
bool primary = false;
};
// 灯光。类型:
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent {
bool active = true; // 禁用 = 不进光照
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 {
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
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 / UE Overlap Events 语义):只产生
// Added/Removed 事件,无碰撞解算;底层 Jolt sensor
bool isTrigger = 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 {
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
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 {
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
f32 radius = 0.3f; // 胶囊半径
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
f32 gravityScale = 2.0f;
bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
};
// 脚本组件:多脚本挂载(Unity 语义——一个 GameObject 挂 N 个
// MonoBehaviour,各自有 enabled)。每项 = 脚本类名(注册表键,
// 将来 Lua/C# 同键解析)+ 独立启停位;组件级 active 是总开关。
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
struct ScriptComponent {
// 单个脚本项
struct Item {
std::string name;
bool active = true; // 禁用 = 实例保留但停 OnUpdate
// 参数(权威数据,随场景序列化)。脚本成员 = 运行时真值,
// 实例化期由 ScriptRuntime 按 schema 灌入:键缺失 → 声明
// 默认,多余键忽略;运行时 SetParam 写穿回这里(PIE 实时
// 反映,Stop 由快照天然回滚)
std::vector<Engine::Scripting::ScriptParam> params;
bool operator==(const Item&) const = default;
};
bool active = true; // 组件总开关(全部停 tick,实例保留)
std::vector<Item> items;
bool operator==(const ScriptComponent&) const = default;
};
// 天空盒/环境背景(UE Skybox Actor 语义:环境是场景里的 Actor +
// 组件,不是常驻全局设置)。envPath = 环境贴图资产(.hdr,经
// AssetManager 加载、Renderer 预积分后进 IBL/Skybox pass);空 =
// 只清屏(ClearColor 露出)。多实例取第一个 active(entt 视图序 =
// 组件创建序,首个命中即稳定)
struct SkyboxComponent {
bool active = true; // 禁用 = 不画天空(清屏色露出)
std::string envPath;
};
// 环境光(UE SkyLight 语义:全局漫射环境照明)。多实例取第一个 active
struct SkyLightComponent {
bool active = true;
glm::vec4 ambientColor{1.0f}; // 旧 Settings 默认
};
} // namespace Engine::ECS
#endif //HELLOVULKAN_COMPONENTS_H