diff --git a/App/scripts/FractureLib.h b/App/scripts/FractureLib.h new file mode 100644 index 0000000..0c2a51a --- /dev/null +++ b/App/scripts/FractureLib.h @@ -0,0 +1,562 @@ +// +// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。 +// +// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D +// Voronoi 分割: +// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、 +// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面 +// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂 +// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接 +// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU +// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体 +// 都是真的,落地翻滚/堆积接触自然。 +// +// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处 +// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛 +// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在 +// 墙脚"的观感。 +// +// 用法(宿主 = 挂在可碎实体上的脚本): +// Fracture::Params p_; // DeclareParams 直绑成员(pb.Field) +// Fracture::Burst burst_; // 碎片簿记 + 生命周期 +// HostView HV() { // 桥接(脚本自身成员函数内可访问 +// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API) +// [this](auto& n) { return Spawn(n); }, +// [this](auto e) { Destroy(e); }}; +// } +// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal) +// OnUpdate → burst_.Tick(HV(), dt) +// OnDestroy → burst_.Shutdown(HV()) +// 外观:Fire 前改 burst_.look(默认砖色不透明;玻璃靶 = 半透明 +// + 自发光) +// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全) +// +// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界 +// 尺寸烘焙、碎片实体复用宿主 world pos/rot(scale 恒 1,防 +// ConvexHull 顶点二次缩放)——宿主须无旋转摆放。 +// + +#ifndef HELLOVULKAN_FRACTURE_LIB_H +#define HELLOVULKAN_FRACTURE_LIB_H + +#include +#include +#include +#include // LoadModel(门面只前向声明) +#include // ParsedModel/ParsedMesh(程序化碎片几何) +#include // Vertex 聚合布局 +#include + +#include +#include + +#include +#include +#include +#include + +namespace Engine::Fracture { + + // bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验) + inline ECS::Entity EntityFromBits(u32 bits) { + return static_cast(bits); + } + + // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 + // 需要任意实体的同款换算) + inline u32 BitsOf(const ECS::EntityHandle& e) { + return static_cast( + entt::to_integral(static_cast(e))); + } + + inline f32 Rand(f32 lo, f32 hi) { + thread_local std::mt19937 rng{std::random_device{}()}; + return std::uniform_real_distribution(lo, hi)(rng); + } + + // 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次 + // 命中都重新随机生成形状(命中点量化键会让同位置重复命中 + // 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了) + inline u32& GenSeq() { + static u32 s = 0; + return s; + } + + // ---- 宿主桥接(脚本侧现填;库内只走公共 API)---- + // spawn/destroy 走脚本成员(内部才能访问 ScriptBehavior 的 + // protected 底座),app/实体走数据。std::function 捕 this 无 + // 分配(小对象优化),随调用栈存亡,无悬垂面 + struct HostView { + Application& app; + u32 entityBits = 0; + ECS::Entity entity() const { return EntityFromBits(entityBits); } + std::function spawn; + std::function destroy; + }; + + // ---- 2D Voronoi(碎裂裂纹)---- + // + // cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。 + // Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧 + //(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。 + // 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。 + inline std::vector ClipByBisector(std::vector poly, + const glm::vec2& p, + const glm::vec2& q) { + const glm::vec2 d = q - p; + const f32 len = glm::length(d); + if (len < 1e-6f) return poly; + const glm::vec2 a = d / len; + const f32 b = glm::dot(a, p) + 0.5f * len; + + std::vector out; + out.reserve(poly.size() + 1); + for (size_t i = 0; i < poly.size(); ++i) { + const glm::vec2& c = poly[i]; + const glm::vec2& n = poly[(i + 1) % poly.size()]; + const f32 dc = glm::dot(a, c) - b; + const f32 dn = glm::dot(a, n) - b; + if (dc <= 0.0f) out.push_back(c); + if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点 + const f32 t = dc / (dc - dn); + out.push_back(c + t * (n - c)); + } + } + return out; + } + + // 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形) + inline std::vector VoronoiCell(const glm::vec2& p, + const std::vector& seeds, + f32 hw, f32 hh) { + std::vector poly = { + {-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}}; + for (const glm::vec2& q : seeds) { + if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身 + poly = ClipByBisector(std::move(poly), p, q); + if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见) + } + return poly; + } + + // cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱: + // 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh + // 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。 + struct PrismMesh { + std::vector vertices; + std::vector indices; + std::vector cpuPositions; + }; + + inline PrismMesh CellToPrism(const std::vector& cell, f32 hz) { + PrismMesh m; + const size_t n = cell.size(); + const size_t topBase = 0; + const size_t botBase = n; + m.vertices.reserve(n * 2); + + for (const glm::vec2& p : cell) { + const Engine::Vertex v{ + {p.x, p.y, hz}, {0.0f, 0.0f, 1.0f}, + {p.x, p.y}, {1.0f, 0.0f, 0.0f}}; + m.vertices.push_back(v); + } + for (const glm::vec2& p : cell) { + const Engine::Vertex v{ + {p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f}, + {p.x, p.y}, {1.0f, 0.0f, 0.0f}}; + m.vertices.push_back(v); + } + + // top fan(CCW 从 +Z 看朝外)+ bottom fan(反向) + for (size_t i = 1; i + 1 < n; ++i) { + m.indices.push_back(topBase); + m.indices.push_back(topBase + i); + m.indices.push_back(topBase + i + 1); + m.indices.push_back(botBase); + m.indices.push_back(botBase + i + 1); + m.indices.push_back(botBase + i); + } + + // 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向 + for (size_t i = 0; i < n; ++i) { + const size_t j = (i + 1) % n; + const glm::vec2 e = cell[j] - cell[i]; + const f32 el = glm::length(e); + const glm::vec3 sn = el > 1e-6f + ? glm::vec3{e.y / el, -e.x / el, 0.0f} + : glm::vec3{0.0f, -1.0f, 0.0f}; + const size_t b = m.vertices.size(); + const Engine::Vertex q0{ + {cell[i].x, cell[i].y, hz}, sn, + {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}}; + const Engine::Vertex q1{ + {cell[i].x, cell[i].y, -hz}, sn, + {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}}; + const Engine::Vertex q2{ + {cell[j].x, cell[j].y, -hz}, sn, + {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}}; + const Engine::Vertex q3{ + {cell[j].x, cell[j].y, hz}, sn, + {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}}; + m.vertices.push_back(q0); + m.vertices.push_back(q1); + m.vertices.push_back(q2); + m.vertices.push_back(q3); + m.indices.insert(m.indices.end(), + {u32(b), u32(b + 1), u32(b + 2), + u32(b), u32(b + 2), u32(b + 3)}); + } + + // 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集) + m.cpuPositions.reserve(m.vertices.size()); + for (const auto& v : m.vertices) { + m.cpuPositions.push_back(v.position); + } + return m; + } + + // ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)---- + struct Params { + i32 count = 30; // Voronoi 种子数 = 碎片数 + f32 seedRadius = 0.9f; // 加密半径 m + f32 minSeedDist = 0.12f; // 种子最小间距 m + f32 burstSpeed = 1.2f; // 近点径向初速 m/s + f32 falloff = 1.0f; // 距离衰减率 1/m + f32 normalPop = 0.4f; // 法线外推 m/s + f32 upBias = 0.3f; // 上抛偏置 m/s + f32 lifetime = 6.0f; // 碎片寿命(秒) + i32 maxFragments = 128; // 活碎片上限 + f32 minImpactSpeed = 2.0f; // 触发阈值(m/s) + }; + + // ---- 碎片外观(Fire 时套用到全部碎片网格;默认砖色不透明 = + // 原 WallFracture 观感)---- + struct Appearance { + glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f, + 72.0f / 255.0f, 1.0f}; + bool transparent = false; // 透明绘制队列(玻璃碎片) + glm::vec3 emissive{0.0f}; // 自发光颜色(黑 = 不发光) + f32 emissiveStrength = 1.0f; + }; + + // ---- 碎裂执行体(宿主脚本持有;一次 Fire 管一整批碎片)---- + class Burst { + public: + Params p; // 宿主 DeclareParams 直绑 + Appearance look; // 碎片外观(Fire 前设置) + + // 触发流程一次做完:一次性保护 → 对方校验(活 Dynamic + + // 速度阈值)→ 藏宿主 → 切分 → spawn 碎片群。未触发返回 + // false(速度不够/重复触发),宿主无需再写判定 + bool TryFire(const HostView& host, + u32 other, const glm::vec3& point, + const glm::vec3& normal) { + if (m_Fired) return false; + + // 对方须是活的 Dynamic 刚体且速度够快(过滤静止贴靠/ + // 角色轻碰——子弹/抛体必中) + auto& registry = host.app.GetScene().GetRegistry(); + const ECS::Entity otherE = EntityFromBits(other); + if (!registry.valid(otherE)) return false; + const auto* otherRb = + registry.try_get(otherE); + if (!otherRb || + otherRb->type != + Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { + return false; + } + const f32 speed = glm::length( + host.app.GetPhysics().GetLinearVelocity(other)); + if (speed < p.minImpactSpeed) return false; + + m_Fired = true; + Fire(host, point, normal); + return true; + } + + // 每帧簿记(宿主 OnUpdate 直调;未 Fire 过无簿记可理) + void Tick(const HostView& host, f32 dt) { + if (!m_Fired) return; + ApplyPendingUnload(host); + ApplyPendingVelocities(host); + AgeAndCleanup(host, dt); + } + + // 宿主销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡 + // 处理,独占碎片模型统一批量回收(模型不被快照恢复的场景 + // 引用——唯一代键;批量接口内部 GPU 停飞后再销毁) + void Shutdown(const HostView& host) { + if (!m_Fired) return; // 从未 Fire:无碎片无模型 + std::vector paths; + paths.reserve(m_Fragments.size() + m_PendingUnload.size()); + for (const Fragment& f : m_Fragments) { + if (!f.modelPath.empty()) paths.push_back(f.modelPath); + } + m_Fragments.clear(); + paths.insert(paths.end(), + std::make_move_iterator(m_PendingUnload.begin()), + std::make_move_iterator(m_PendingUnload.end())); + m_PendingUnload.clear(); + if (!paths.empty()) { + host.app.GetAssets().UnloadModelsByPaths(paths); + } + } + + bool Fired() const { return m_Fired; } + + private: + // ---- 触发碎裂:藏宿主 + Voronoi 切分 + spawn 碎片群 ---- + void Fire(const HostView& host, + const glm::vec3& hitPoint, const glm::vec3& hitNormal) { + auto& registry = host.app.GetScene().GetRegistry(); + const ECS::Entity self = host.entity(); + + // 藏宿主:渲染/拾取跳过 + body 对账销毁(组件级 active, + // 与 Unity Renderer.enabled + Rigidbody 摘除同语义) + if (auto* mr = + registry.try_get( + self)) { + mr->active = false; + } + if (auto* rb = + registry.try_get( + self)) { + rb->active = false; + } + + // 宿主几何:碎片网格顶点按宿主世界尺寸烘焙(面 = XY 平面, + // 厚度沿 Z;宿主须无旋转摆放,见头注释占位局限) + const auto& wall = registry.get( + self); + const glm::vec3 wallPos = wall.GetWorldPosition(); + const glm::vec3 wallSize = wall.GetWorldScale(); + const f32 hw = 0.5f * wallSize.x; + const f32 hh = 0.5f * wallSize.y; + const f32 hz = 0.5f * wallSize.z; + + // 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标 + const glm::quat invRot = glm::conjugate( + wall.GetWorldRotationQuat()); + const glm::vec3 localHit = invRot * (hitPoint - wallPos); + const glm::vec2 uvHit{localHit.x, localHit.y}; + + // ---- 种子:40% 命中点半径内加密 + 60% 全面均匀,最小 + // 间距拒绝采样(防碎片退化为细条)---- + const u32 count = std::clamp(p.count, 4, 96); + const u32 nearCount = count * 2 / 5; + std::vector seeds; + seeds.reserve(count); + const f32 minD2 = p.minSeedDist * p.minSeedDist; + auto tryPush = [&](const glm::vec2& s) { + if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return; + for (const glm::vec2& e : seeds) { + if (glm::dot(e - s, e - s) < minD2) return; + } + seeds.push_back(s); + }; + // 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀) + for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount; + ++i) { + const f32 a = Rand(0.0f, glm::two_pi()); + const f32 r = p.seedRadius * std::sqrt(Rand(0.0f, 1.0f)); + tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)}); + } + // 全面均匀补足(同款拒绝采样) + for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) { + tryPush({Rand(-hw, hw), Rand(-hh, hh)}); + } + if (seeds.size() < 4) return; // 面太小采样失败:不碎 + + // ---- 每种子 → cell → 棱柱 mesh → ParsedModel ---- + // 唯一代键(见 GenSeq 注释):形状每次随机;模型回收走 + // Fragment.modelPath + 待卸载队列(ApplyPendingUnload) + const std::string keyBase = + "procedural://vorfrag/" + + std::to_string(GenSeq()++) + '/'; + + auto& assets = host.app.GetAssets(); + std::vector refs; + std::vector centroids; // 碎片质心(世界系,初速径向用) + std::vector paths; // 模型缓存键(回收用,与 refs 对齐) + refs.reserve(seeds.size()); + centroids.reserve(seeds.size()); + paths.reserve(seeds.size()); + for (size_t s = 0; s < seeds.size(); ++s) { + const std::vector cell = + VoronoiCell(seeds[s], seeds, hw, hh); + if (cell.size() < 3) { + refs.push_back({}); // 退化 cell:占位,spawn 跳过 + centroids.emplace_back(); + paths.emplace_back(); + continue; + } + PrismMesh pm = CellToPrism(cell, hz); + glm::vec2 c2{0.0f}; + for (const glm::vec2& q : cell) c2 += q; + c2 /= static_cast(cell.size()); + centroids.push_back( + wallPos + wall.GetWorldRotationQuat() * + glm::vec3{c2.x, c2.y, 0.0f}); + + Engine::ParsedModel parsed; + parsed.path = keyBase + std::to_string(s); + Engine::ParsedMesh& mesh = + parsed.meshes.emplace_back(); + mesh.vertices = std::move(pm.vertices); + mesh.lods.push_back(std::move(pm.indices)); + mesh.cpuPositions = std::move(pm.cpuPositions); + mesh.name = "frag" + std::to_string(s); + mesh.params.baseColor = look.baseColor; + mesh.transparent = look.transparent; + mesh.params.emissiveFactor = + glm::vec4{look.emissive, 1.0f}; + mesh.params.surface.z = look.emissiveStrength; + + const Engine::ModelRef ref = + assets.LoadModel(parsed.path, std::move(parsed)); + refs.push_back(ref); + paths.push_back(parsed.path); + } + + // ---- spawn 碎片实体:pos/rot 复用宿主位姿(mesh 已真 + // 尺寸,scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂 + // 瞬间完美拼合原面,动起来后沿裂纹自然分离 ---- + for (size_t s = 0; s < refs.size(); ++s) { + if (!refs[s].Valid()) continue; + + auto frag = host.spawn("fragment"); + auto& tr = frag.GetComponent< + Engine::ECS::TransformComponent>(); + tr.SetPosition(wallPos); + tr.SetRotationQuat(wall.GetWorldRotationQuat()); + tr.SetScale(glm::vec3{1.0f}); + + auto& fmr = + frag.AddComponent< + Engine::ECS::MeshRendererComponent>(); + fmr.model = refs[s]; + + // Dynamic MeshCollider → ConvexHull(数据源 = 刚上传 + // 的 cpuPositions;Box 字段不参与 Mesh 形状) + auto& frb = + frag.AddComponent< + Engine::ECS::RigidBodyComponent>(); + frb.type = + Engine::ECS::RigidBodyComponent::BodyType::Dynamic; + frb.collider = + Engine::ECS::RigidBodyComponent::ColliderType::Mesh; + frb.mass = 0.3f; + frb.friction = 0.7f; + frb.restitution = 0.05f; + + // 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中 + // 点,离命中点越远越接近静止(指数衰减)——只有孔洞 + // 周边的碎片被冲击波推动;法线外推让碎片朝弹侧剥出 + glm::vec3 dir = centroids[s] - hitPoint; + const f32 len = glm::length(dir); + if (len < 1e-3f) { + dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f, + Rand(-1.0f, 1.0f)}; + } else { + dir /= len; + } + const f32 falloff = std::exp(-len * p.falloff); + const glm::vec3 vel = + dir * p.burstSpeed * falloff * + Rand(0.4f, 1.1f) + + hitNormal * p.normalPop * Rand(0.3f, 0.8f) + + glm::vec3{0.0f, p.upBias * Rand(0.0f, 0.5f), + 0.0f}; + + // 总量上限:超限立即回收最老碎片(防泄漏的兜底闸; + // 寿命清理才是常态回收)。模型延迟一帧卸载 + const u32 bits = BitsOf(frag); + if (m_Fragments.size() >= + static_cast(p.maxFragments)) { + host.destroy(EntityFromBits(m_Fragments.front().bits)); + if (!m_Fragments.front().modelPath.empty()) { + m_PendingUnload.push_back( + std::move(m_Fragments.front().modelPath)); + } + m_Fragments.erase(m_Fragments.begin()); + } + m_Fragments.push_back( + Fragment{bits, vel, 0.0f, false, paths[s]}); + } + } + + // ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟, + // 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧 + // 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先 + // WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)---- + void ApplyPendingUnload(const HostView& host) { + if (m_PendingUnload.empty()) return; + host.app.GetAssets().UnloadModelsByPaths(m_PendingUnload); + m_PendingUnload.clear(); + } + + // ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ---- + void ApplyPendingVelocities(const HostView& host) { + auto& registry = host.app.GetScene().GetRegistry(); + for (auto it = m_Fragments.begin(); + it != m_Fragments.end();) { + if (it->applied) { ++it; continue; } + const ECS::Entity e = EntityFromBits(it->bits); + if (!registry.valid(e)) { // 实体没了(外部销毁) + // 模型是独占资源(一代一键),实体没了模型也回收 + if (!it->modelPath.empty()) { + m_PendingUnload.push_back( + std::move(it->modelPath)); + } + it = m_Fragments.erase(it); + continue; + } + const auto* rb = registry.try_get< + Engine::ECS::RigidBodyComponent>(e); + if (rb && rb->bodyValid) { + host.app.GetPhysics().SetLinearVelocity( + it->bits, it->velocity); + it->applied = true; + } + ++it; // body 未建:留待下一帧(对账晚一帧必到) + } + } + + // ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)---- + void AgeAndCleanup(const HostView& host, f32 dt) { + auto& registry = host.app.GetScene().GetRegistry(); + for (auto it = m_Fragments.begin(); + it != m_Fragments.end();) { + it->age += dt; + if (it->age < p.lifetime) { ++it; continue; } + const ECS::Entity e = EntityFromBits(it->bits); + if (registry.valid(e)) { + host.destroy(e); + } + // 模型延迟一帧卸载(本帧渲染收集还引用它) + if (!it->modelPath.empty()) { + m_PendingUnload.push_back(std::move(it->modelPath)); + } + it = m_Fragments.erase(it); + } + } + + struct Fragment { + u32 bits = 0; // PhysicsWorld 键(实体位模式) + glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) + f32 age = 0.0f; // 存活秒数(寿命清理用) + bool applied = false; // 初速已注入 + std::string modelPath; // 独占模型缓存键(回收时按路径卸载) + }; + + bool m_Fired = false; // 一次性触发保护 + std::vector m_Fragments; // 活碎片簿记 + // 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟, + // 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针) + std::vector m_PendingUnload; + }; + +} // namespace Engine::Fracture + +#endif //HELLOVULKAN_FRACTURE_LIB_H diff --git a/App/scripts/GameDirector.cpp b/App/scripts/GameDirector.cpp new file mode 100644 index 0000000..f8a4f95 --- /dev/null +++ b/App/scripts/GameDirector.cpp @@ -0,0 +1,386 @@ +// +// GameDirector.cpp:60 秒限时打靶场导演(玩法原型验证侧线)。 +// +// 完整玩法循环(全现有能力,零引擎改动): +// 开局 → 程序化搭玻璃靶阵(行对齐网格,居中逐行收缩)→ 60 秒 +// 倒计时 → 射击碎靶计分(Target 报分,跨脚本参数通道)→ 时间到 +// 收靶结算 → R 重开(任意时刻可重置) +// +// 计分协议:Score/AcceptHits = 导演的声明参数——Target 经 +// ScriptRuntime::GetParam/SetParam 跨实体读写(参数层即按名消息, +// 与序列化同一条路)。Score 写穿 Item.params,PIE Inspector 实时 +// 反映;AcceptHits=false 后 Target 停止报分(双保险,收靶为主)。 +// +// HUD:引擎暂无文字渲染(缺失能力,已记录),倒计时/得分用 +// Debug 图元在世界空间凑——顶部倒计时条(绿→黄→红随剩余时间) +// + 条下一排金色得分点(1 分 = 1 点)。挂在靶阵上方,depthTest +// 关闭保证穿墙可见。 +// +// 靶子模型:程序化玻璃立方(ParsedModel 手工装配,半透明 + 自发 +// 光),缓存键 = 颜色/尺寸参数量化——同配置共享一份 GPU 模型, +// 跨回合复用;改参数重开局才重建。碎片几何由 FractureLib 每靶 +// 现场生成(唯一代键,自管回收)。 +// +// 【演示步骤】任意实体挂 GameDirector,相机实体挂 TestShoot → +// Play → RMB 飞行瞄准靶阵 → 左键射击:靶子玻璃式碎裂 + 得分点 +// 亮起,倒计时条流逝,时间到结算(控制台输出得分),R 重开。 +// +// 【缺失能力记录(玩法侧线的下一批依据)】 +// - UI/HUD 文字(倒计时数字/得分数字只得用图元条凑) +// - 音效(碎裂/射击/倒计时提示全程无声) +// - 准星(靠弹道调试十字凑合) +// - 游戏状态机/关卡流(重开 = 脚本内手搓,无引擎级 GameMode) +// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FractureLib.h" // BitsOf/EntityFromBits(碎片簿记同款换算) + +#include + +#include +#include +#include +#include + +using namespace Engine; // Input/Debug 门面 + 基础类型 + +namespace { + + // ---- 玻璃立方 ParsedModel 装配(半透明 + 自发光;6 面 quad + // 手工展开,外向 CCW:u × v = n 定切向基)---- + Engine::ParsedModel BuildGlassCube(const glm::vec3& he, + const glm::vec3& color, + const std::string& path) { + Engine::ParsedModel parsed; + parsed.path = path; + auto& mesh = parsed.meshes.emplace_back(); + + // 面:法线 n + 切向 u/v(u×v=n 保证外向 CCW 索引序) + static constexpr glm::vec3 kN[6] = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, + {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}; + static constexpr glm::vec3 kU[6] = { + {0, 0, -1}, {0, 0, 1}, {1, 0, 0}, + {1, 0, 0}, {1, 0, 0}, {1, 0, 0}}; + static constexpr glm::vec3 kV[6] = { + {0, 1, 0}, {0, 1, 0}, {0, 0, -1}, + {0, 0, 1}, {0, 1, 0}, {0, 1, 0}}; + + // 轴向半长:轴 a 上的偏移 = a ∘ he(分量积;单位轴即取该轴 + // 分量),面心 = n ∘ he + auto along = [&](const glm::vec3& a) { + return glm::vec3{a.x * he.x, a.y * he.y, a.z * he.z}; + }; + + mesh.vertices.reserve(24); + for (int f = 0; f < 6; ++f) { + const u32 base = static_cast(mesh.vertices.size()); + const glm::vec3 center = along(kN[f]); + // quad 角序:(−u,−v) (+u,−v) (+u,+v) (−u,+v)——CCW + for (int c = 0; c < 4; ++c) { + const f32 su = (c == 1 || c == 2) ? 1.0f : -1.0f; + const f32 sv = (c >= 2) ? 1.0f : -1.0f; + const glm::vec3 pos = center + su * along(kU[f]) + + sv * along(kV[f]); + const Engine::Vertex v{ + pos, kN[f], + {(su + 1.0f) * 0.5f, (sv + 1.0f) * 0.5f}, kU[f]}; + mesh.vertices.push_back(v); + } + mesh.lods.emplace_back(); + auto& idx = mesh.lods.back(); + idx.insert(idx.end(), + {base, base + 1, base + 2, + base, base + 2, base + 3}); + } + + // 玻璃材质:低金属度高光洁 + alpha 混合 + 靶色自发光 + mesh.name = "target"; + mesh.params.factors = {0.05f, 0.15f, 0.0f, 0.0f}; + mesh.params.baseColor = glm::vec4{color, 0.42f}; + mesh.params.surface = {1.5f, 0.0f, 2.0f, 0.0f}; + mesh.params.emissiveFactor = glm::vec4{color * 0.85f, 1.0f}; + mesh.transparent = true; + return parsed; + } + + // ---- 打靶场导演 ---- + class GameDirector final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "GameDirector"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + // ---- 回合规则 ---- + pb.Field("RoundTime", &m_RoundTime, 60.0f); // 局时长(秒) + pb.Field("TargetCount", &m_TargetCount, 5); // 靶数 + pb.Field("Score", &m_Score, 0); // 得分(Target 报分写这里) + pb.Field("AcceptHits", &m_AcceptHits, true); // 计分门(Target 读) + // ---- 靶阵布局(Center = 底行中心)---- + pb.Field("Center", &m_Center, + glm::vec3{0.0f, 1.5f, -6.0f}); + pb.Field("PerRow", &m_PerRow, 5); // 每行靶数 + pb.Field("Spacing", &m_Spacing, 1.2f); // 行/列间距 m + // ---- 靶子规格 ---- + pb.Field("TargetSize", &m_TargetSize, + glm::vec3{0.7f, 0.7f, 0.2f}); // 单靶尺寸 m(厚 ≥0.2 防高速弹隧穿) + pb.Field("TargetColor", &m_TargetColor, + glm::vec3{1.0f, 0.3f, 0.25f}); // 玻璃色 + pb.Field("FragCount", &m_FragCount, 22); // 每靶碎片数(写进 Target) + // ---- HUD ---- + pb.Field("HudHeight", &m_HudHeight, 1.6f); // HUD 高于靶阵中心 m + } + + void OnStart() override { StartRound(); } + + void OnUpdate(f32 dt) override { + // R 上升沿重开(整局任意时刻可重置) + const bool rDown = Input::IsKeyDown(Input::Key::R); + const bool restart = rDown && !m_RWasDown; + m_RWasDown = rDown; + if (restart) { + StartRound(); + return; + } + + if (m_AcceptHits) { + m_TimeLeft -= dt; + if (m_TimeLeft <= 0.0f) { + m_TimeLeft = 0.0f; + EndRound(); + } + } + DrawHud(); + } + + // PIE stop:玻璃靶模型是本脚本独占资产(参数键缓存),批量 + // 停飞卸载(同 WallFracture 碎片模型回收先例) + void OnDestroy() override { + if (!m_ModelKey.empty()) { + std::vector paths{m_ModelKey}; + App().GetAssets().UnloadModelsByPaths(paths); + m_ModelKey.clear(); + m_TargetModel = {}; + } + } + + private: + // ---- 开局:清场 → 建模型 → spawn 靶阵 → 计时/计分归零 ---- + void StartRound() { + DestroyRemaining(); + EnsureTargetModel(); + SpawnTargets(); + + m_Score = 0; + SetParam("Score", Engine::Scripting::ParamValue{0}); + m_TimeLeft = m_RoundTime; + m_AcceptHits = true; + SetParam("AcceptHits", Engine::Scripting::ParamValue{true}); + ENGINE_INFO("[GameDirector] 回合开始:{} 个靶 / {} 秒", + m_TargetCount, m_RoundTime); + } + + // ---- 结算:停计分 → 收掉没打碎的靶 → 报分 ---- + void EndRound() { + m_AcceptHits = false; + SetParam("AcceptHits", Engine::Scripting::ParamValue{false}); + DestroyRemaining(); + ENGINE_INFO("[GameDirector] 时间到!得分 {}/{}(R 重开)", + m_Score, m_TargetCount); + } + + // ---- 靶阵:行对齐网格,每行水平居中,行向上堆 ---- + void SpawnTargets() { + const int count = std::max(1, m_TargetCount); + const int perRow = std::max(1, m_PerRow); + for (int i = 0; i < count; ++i) { + const int row = i / perRow; + const int col = i % perRow; + const int inRow = std::min(count - row * perRow, perRow); + glm::vec3 pos = m_Center; + pos.y += static_cast(row) * m_Spacing; + pos.x += static_cast(col) * m_Spacing - + static_cast(inRow - 1) * m_Spacing * 0.5f; + SpawnTarget(pos); + } + } + + void SpawnTarget(const glm::vec3& pos) { + auto t = Spawn("target"); + auto& tr = t.GetComponent(); + tr.SetPosition(pos); // scale 1:尺寸烘焙进模型与碰撞体 + + auto& mr = + t.AddComponent(); + mr.model = m_TargetModel; + + // Static Box:厚 ≥0.2m 时对 25m/s 无 CCD 弹保证检出 + //(检测带 2*(弹半径0.12+靶半厚0.1)=0.44m > 每步位移0.42m) + auto& rb = t.AddComponent(); + rb.type = Engine::ECS::RigidBodyComponent::BodyType::Static; + rb.collider = Engine::ECS::RigidBodyComponent::ColliderType::Box; + rb.boxHalfExtents = m_TargetSize * 0.5f; + rb.friction = 0.5f; + rb.restitution = 0.1f; + + // Target 脚本项:计分去向 + 靶色/碎片档经参数层传入 + auto& sc = t.AddComponent(); + Engine::ECS::ScriptComponent::Item item; + item.name = "Target"; + item.params.push_back( + {"Director", + Engine::Scripting::ParamValue{ + static_cast(EntityBits())}}); + item.params.push_back( + {"Color", Engine::Scripting::ParamValue{m_TargetColor}}); + item.params.push_back( + {"Count", Engine::Scripting::ParamValue{m_FragCount}}); + sc.items.push_back(std::move(item)); + + m_TargetBits.push_back(Engine::Fracture::BitsOf(t)); + } + + void DestroyRemaining() { + auto& registry = App().GetScene().GetRegistry(); + for (const u32 bits : m_TargetBits) { + const ECS::Entity e = + Engine::Fracture::EntityFromBits(bits); + if (registry.valid(e)) { + Destroy(e); // 延迟队列,帧尾统一执行 + } + } + m_TargetBits.clear(); + } + + // ---- 玻璃靶模型:参数量化缓存键(同配置共享一份 GPU 模型, + // 跨回合复用;改色/改尺寸后下次开局重建。注:键按"一个导演 + // 实例独占"设计,双导演同参数会互踩——原型的合理取舍)---- + void EnsureTargetModel() { + const auto q = [](f32 c) { + const int b = static_cast( + std::clamp(c, 0.0f, 1.0f) * 255.0f + 0.5f); + return std::clamp(b, 0, 255); + }; + // 尺寸量化:分米精度(0.1m 步进;同尺寸档共享模型) + const auto qs = [](f32 v) { + return std::clamp( + static_cast(std::max(v, 0.0f) * 10.0f + 0.5f), + 0, 999); + }; + char hex[96]; + std::snprintf(hex, sizeof(hex), + "procedural://rangetarget/%02X%02X%02X_" + "%d_%d_%d", + q(m_TargetColor.r), q(m_TargetColor.g), + q(m_TargetColor.b), + qs(m_TargetSize.x), qs(m_TargetSize.y), + qs(m_TargetSize.z)); + const std::string key = hex; + if (key == m_ModelKey && m_TargetModel.Valid()) return; + + // 配置变了:旧键独占资产先回收 + if (m_TargetModel.Valid()) { + std::vector old{m_ModelKey}; + App().GetAssets().UnloadModelsByPaths(old); + m_TargetModel = {}; + } + + auto parsed = BuildGlassCube(m_TargetSize * 0.5f, + m_TargetColor, key); + m_TargetModel = App().GetAssets().LoadModel( + key, std::move(parsed)); + m_ModelKey = key; + } + + // ---- HUD(图元凑,无文字是已知缺失能力):倒计时条 + //(绿→黄→红)+ 得分点(1 分 = 1 金点);结算态整条红 ---- + void DrawHud() { + const glm::vec3 base = + m_Center + glm::vec3{0.0f, m_HudHeight, 0.0f}; + constexpr f32 kBarW = 3.0f; + const f32 left = base.x - kBarW * 0.5f; + const glm::vec3 z{0.0f, 0.0f, base.z}; + + const f32 frac = m_AcceptHits + ? std::clamp(m_TimeLeft / std::max(m_RoundTime, 0.01f), + 0.0f, 1.0f) + : 0.0f; + // 颜色:>50% 绿→黄,≤50% 黄→红 + const glm::vec4 green{0.24f, 0.86f, 0.52f, 1.0f}; + const glm::vec4 yellow{1.0f, 0.84f, 0.31f, 1.0f}; + const glm::vec4 red{1.0f, 0.32f, 0.32f, 1.0f}; + const glm::vec4 col = frac > 0.5f + ? glm::mix(yellow, green, (frac - 0.5f) * 2.0f) + : glm::mix(red, yellow, frac * 2.0f); + const glm::vec4 dim{0.25f, 0.25f, 0.25f, 1.0f}; + + // 外框 + 端点刻度 + Debug::Line({left, base.y, base.z}, {left + kBarW, base.y, base.z}, + dim, 2.0f, 0.0f, /*depthTest*/ false); + Debug::Line({left, base.y - 0.08f, base.z}, + {left, base.y + 0.08f, base.z}, + dim, 2.0f, 0.0f, false); + Debug::Line({left + kBarW, base.y - 0.08f, base.z}, + {left + kBarW, base.y + 0.08f, base.z}, + dim, 2.0f, 0.0f, false); + + if (m_AcceptHits) { + if (frac > 0.003f) { + Debug::Line({left, base.y, base.z}, + {left + kBarW * frac, base.y, base.z}, + col, 8.0f, 0.0f, false); + } + } else { + // 结算态:整条红 + 上方短刻线(与运行态一眼区分) + Debug::Line({left, base.y, base.z}, + {left + kBarW, base.y, base.z}, + red, 4.0f, 0.0f, false); + Debug::Line({left, base.y + 0.14f, base.z}, + {left + kBarW, base.y + 0.14f, base.z}, + red, 2.0f, 0.0f, false); + } + + // 得分点:1 分 = 1 金点(封顶 24 防溢出条外) + const glm::vec4 gold{1.0f, 0.78f, 0.2f, 1.0f}; + const int pips = std::min(m_Score, 24); + for (i32 i = 0; i < pips; ++i) { + const f32 px = left + 0.18f + + static_cast(i) * 0.24f; + Debug::Cross({px, base.y - 0.3f, base.z}, 0.05f, + gold, 2.0f, 0.0f, false); + } + } + + // ---- 回合状态 ---- + f32 m_RoundTime = 60.0f; + i32 m_TargetCount = 5; + i32 m_Score = 0; + bool m_AcceptHits = true; // 计分门(参数成员,Target 跨脚本读写) + f32 m_TimeLeft = 60.0f; + bool m_RWasDown = false; // R 上升沿基准 + + // ---- 布局/规格 ---- + glm::vec3 m_Center{0.0f, 1.5f, -6.0f}; + i32 m_PerRow = 5; + f32 m_Spacing = 1.2f; + glm::vec3 m_TargetSize{0.7f, 0.7f, 0.2f}; + glm::vec3 m_TargetColor{1.0f, 0.3f, 0.25f}; + i32 m_FragCount = 22; + f32 m_HudHeight = 1.6f; + + // ---- 靶子簿记 ---- + std::vector m_TargetBits; // 本局靶实体(重开收靶用) + Engine::ModelRef m_TargetModel; // 玻璃立方(参数键缓存共享) + std::string m_ModelKey; + }; + +} // namespace + +ENGINE_REGISTER_SCRIPT(GameDirector) diff --git a/App/scripts/Target.cpp b/App/scripts/Target.cpp new file mode 100644 index 0000000..f208988 --- /dev/null +++ b/App/scripts/Target.cpp @@ -0,0 +1,118 @@ +// +// Target.cpp:打靶场玻璃靶子脚本(侧线"玩法原型验证")。 +// +// 职责(挂靶子实体:MeshRenderer 玻璃立方 + RigidBody Static Box + +// 本脚本;通常由 GameDirector 运行时成批 spawn,也可编辑器手动摆): +// OnCollisionEnter → 玻璃外观套用(半透明 + 靶色自发光)→ +// Fracture::Burst::TryFire(一次性保护 + Dynamic 速度校验 + +// 藏靶 + Voronoi 切分 + spawn 玻璃碎片群,共享库内完成) +// ReportScore:向 Director 实体的 GameDirector 脚本报分(Score +1, +// 写穿 Item.params,PIE Inspector 实时反映);Director=0 表示 +// 独立靶不计分;导演 AcceptHits=false(回合已结束)不报 +// OnUpdate/OnDestroy → Burst::Tick/Shutdown(碎片施速/寿命/回收) +// +// 覆盖的引擎设施:跨脚本参数访问(ScriptRuntime::GetParam/SetParam, +// GameDirector ↔ Target 的计分通道)、共享碎裂库(FractureLib.h, +// 与 WallFracture 同一条碎片路径)、玻璃材质(透明队列 + 自发光)。 +// +// 演示步骤:任意实体挂 GameDirector → Play → 左键射击靶阵: +// 靶子玻璃式碎裂 + 计分,60 秒倒计时,时间到结算,R 重开。 +// + +#include +#include // GetScripts() 跨脚本参数访问 +#include + +#include "FractureLib.h" + +using namespace Engine; // i32/f32/u32 基础类型(同 WallFracture) + +namespace { + + // ---- 玻璃靶子 ---- + class Target final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "Target"; } + + void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { + // 击碎报分去向:Director 实体 bits(0 = 独立靶不计分); + // GameDirector spawn 时写进脚本项参数 + pb.Field("Director", &m_Director, 0); + // 靶色(玻璃染色 + 自发光基色;碎片同色系) + pb.Field("Color", &m_Color, + glm::vec3{1.0f, 0.3f, 0.25f}); + // 碎裂参数(同 WallFracture 语义;靶面小 → 种子更密) + pb.Field("Count", &m_Burst.p.count, 22); + pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.35f); + pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.07f); + pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.6f); + pb.Field("Falloff", &m_Burst.p.falloff, 2.0f); + pb.Field("NormalPop", &m_Burst.p.normalPop, 0.6f); + pb.Field("UpBias", &m_Burst.p.upBias, 0.4f); + pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); + pb.Field("MaxFragments", &m_Burst.p.maxFragments, 96); + pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); + } + + void OnCollisionEnter(u32 other, const glm::vec3& point, + const glm::vec3& normal) override { + // 玻璃外观:半透明 + 靶色自发光(Fire 前套用,碎片沿用; + // 每次覆写代价可忽略,且吃 Inspector 实时改色) + m_Burst.look.baseColor = glm::vec4{m_Color, 0.45f}; + m_Burst.look.transparent = true; + m_Burst.look.emissive = m_Color * 0.7f; + m_Burst.look.emissiveStrength = 2.0f; + + if (m_Burst.TryFire(HV(), other, point, normal)) { + ReportScore(); + } + } + + void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); } + + // 靶销毁(PIE stop / 重开 / 手动删除):碎片模型批量回收 + void OnDestroy() override { m_Burst.Shutdown(HV()); } + + private: + // 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/ + // Destroy),库内只走公共 API——见 FractureLib.h HostView 注 + Engine::Fracture::HostView HV() { + return {App(), EntityBits(), + [this](const std::string& n) { return Spawn(n); }, + [this](Engine::ECS::Entity e) { Destroy(e); }}; + } + // 击碎报分:读导演 AcceptHits 门(回合结束不报)→ Score +1。 + // 参数层 = 跨脚本通信协议(与序列化同一条路,C# 反射缺失下 + // 的按名消息);导演实例未建(罕见:同帧开局碰撞)时忽略 + void ReportScore() { + if (m_Director == 0) return; + auto& scripts = App().GetScripts(); + const u32 director = static_cast(m_Director); + // 参数层按名访问(ScriptRuntime 只认 ParamValue 变体, + // 取值自行 std::get_if 校型) + using Engine::Scripting::ParamValue; + ParamValue v{true}; + if (scripts.GetParam(director, "GameDirector", + "AcceptHits", v)) { + if (const auto* b = std::get_if(&v.v); b && !*b) { + return; // 回合已结束(Director 收靶前的漏网命中) + } + } + i32 score = 0; + if (scripts.GetParam(director, "GameDirector", "Score", v)) { + if (const auto* s = std::get_if(&v.v)) { + score = *s; + } + } + scripts.SetParam(director, "GameDirector", "Score", + ParamValue{static_cast(score + 1)}); + } + + i32 m_Director = 0; // 导演实体 bits(0 = 独立靶) + glm::vec3 m_Color{1.0f, 0.3f, 0.25f}; + Engine::Fracture::Burst m_Burst; // 碎片簿记(look 每次命中前套) + }; + +} // namespace + +ENGINE_REGISTER_SCRIPT(Target) diff --git a/App/scripts/WallFracture.cpp b/App/scripts/WallFracture.cpp index d818bbd..fe8f61c 100644 --- a/App/scripts/WallFracture.cpp +++ b/App/scripts/WallFracture.cpp @@ -1,44 +1,15 @@ // -// WallFracture.cpp:侧线A 验收脚本——可碎墙 + 碎片刚体群 + 寿命清理。 +// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h +//(打靶场 Target 复用同一条路径),本文件只剩宿主职责: +// DeclareParams 直绑 Fracture::Params(参数名/默认值与历史档位 +// 逐一同名同值,旧场景不受影响) +// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方 Dynamic +// 速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成) +// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收) +// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载) // -// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D -// Voronoi 分割: -// 碎裂瞬间在墙面上撒种子点(命中点周边加密 → 孔洞周围碎片小、 -// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面 -// 交集得到不规则凸多边形 cell,沿墙厚挤出成凸棱柱——碎片在碎裂 -// 前一刻完美拼合成整面墙,碎开后沿裂纹分离。碰撞形状不走 Box, -// 直接用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 -// CPU 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连 -// 碰撞体都是真的,落地翻滚/堆积接触自然。 -// -// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处 -// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛 -// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在 -// 墙脚"的观感。 -// -// WallFracture(挂墙实体:Static Box 刚体 + MeshRenderer 立方体): -// OnCollisionEnter(Dynamic 体以足够速度命中)→ 触发一次: -// 藏墙(MeshRenderer/RigidBody.active = false,PhysicsWorld 对账 -// 自然消化:不渲染 + body 销毁)→ Voronoi 切分 → 每碎片运行时 -// 构建独立 ParsedModel(AssetManager::LoadModel(path, parsed) -// 同步 GPU 构建,缓存去重)+ spawn 碎片实体 -// OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet -// OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy; -// 总量上限防泄漏 -// 碎片模型生命周期:每代唯一缓存键(形状每次随机)+ 碎片销毁的 -// 下一帧 UnloadModelByPath 回收(Destroy 帧尾延迟,本帧渲染收集 -// 仍引用模型,同帧卸载 = 渲染悬空)+ OnDestroy 兜底 -// -// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界 -// 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull -// 顶点二次缩放)——墙须无旋转摆放。 -// -// 覆盖的引擎设施: -// - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁 -// - 组件运行时装配:MeshRenderer(运行时 ParsedModel 程序化资产)+ -// RigidBody(MeshCollider → ConvexHull;active=false 对账销毁) -// - OnCollisionEnter:命中点/法线 + 对方实线速度查询 -// - SetLinearVelocity:body 建好后的初速注入 +// 外观默认 = 砖色不透明(原观感);玻璃/发光碎片换 Target.cpp 的 +// 用法(Fire 前改 Burst::look)。 // // 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube + // RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里 @@ -48,164 +19,15 @@ // #include -#include #include -#include // LoadModel(门面只前向声明) -#include // ParsedModel/ParsedMesh(程序化碎片几何) -#include // Vertex 聚合布局 -#include -#include -#include +#include "FractureLib.h" -#include -#include -#include - -using namespace Engine; // u32/f32/Entity 等基础类型 + 门面(同 TestShoot) +using namespace Engine; // u32/f32/i32 基础类型(历史脚本同款) namespace { - // bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验) - ECS::Entity EntityFromBits(u32 bits) { - return static_cast(bits); - } - - // 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次 - // 命中都重新随机生成形状(命中点量化键会让同位置重复命中 - // 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了) - u32 s_GenSeq = 0; - - // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 - // 需要任意实体的同款换算) - u32 BitsOf(const ECS::EntityHandle& e) { - return static_cast( - entt::to_integral(static_cast(e))); - } - - // ---- 2D Voronoi(碎裂裂纹)---- - // - // cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。 - // Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧 - //(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。 - // 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。 - std::vector ClipByBisector(std::vector poly, - const glm::vec2& p, - const glm::vec2& q) { - const glm::vec2 d = q - p; - const f32 len = glm::length(d); - if (len < 1e-6f) return poly; - const glm::vec2 a = d / len; - const f32 b = glm::dot(a, p) + 0.5f * len; - - std::vector out; - out.reserve(poly.size() + 1); - for (size_t i = 0; i < poly.size(); ++i) { - const glm::vec2& c = poly[i]; - const glm::vec2& n = poly[(i + 1) % poly.size()]; - const f32 dc = glm::dot(a, c) - b; - const f32 dn = glm::dot(a, n) - b; - if (dc <= 0.0f) out.push_back(c); - if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点 - const f32 t = dc / (dc - dn); - out.push_back(c + t * (n - c)); - } - } - return out; - } - - // 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形) - std::vector VoronoiCell(const glm::vec2& p, - const std::vector& seeds, - f32 hw, f32 hh) { - std::vector poly = { - {-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}}; - for (const glm::vec2& q : seeds) { - if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身 - poly = ClipByBisector(std::move(poly), p, q); - if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见) - } - return poly; - } - - // cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱: - // 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh - // 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。 - struct PrismMesh { - std::vector vertices; - std::vector indices; - std::vector cpuPositions; - }; - - PrismMesh CellToPrism(const std::vector& cell, f32 hz) { - PrismMesh m; - const size_t n = cell.size(); - const size_t topBase = 0; - const size_t botBase = n; - m.vertices.reserve(n * 2); - - for (const glm::vec2& p : cell) { - const Engine::Vertex v{ - {p.x, p.y, hz}, {0.0f, 0.0f, 1.0f}, - {p.x, p.y}, {1.0f, 0.0f, 0.0f}}; - m.vertices.push_back(v); - } - for (const glm::vec2& p : cell) { - const Engine::Vertex v{ - {p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f}, - {p.x, p.y}, {1.0f, 0.0f, 0.0f}}; - m.vertices.push_back(v); - } - - // top fan(CCW 从 +Z 看朝外)+ bottom fan(反向) - for (size_t i = 1; i + 1 < n; ++i) { - m.indices.push_back(topBase); - m.indices.push_back(topBase + i); - m.indices.push_back(topBase + i + 1); - m.indices.push_back(botBase); - m.indices.push_back(botBase + i + 1); - m.indices.push_back(botBase + i); - } - - // 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向 - for (size_t i = 0; i < n; ++i) { - const size_t j = (i + 1) % n; - const glm::vec2 e = cell[j] - cell[i]; - const f32 el = glm::length(e); - const glm::vec3 sn = el > 1e-6f - ? glm::vec3{e.y / el, -e.x / el, 0.0f} - : glm::vec3{0.0f, -1.0f, 0.0f}; - const size_t b = m.vertices.size(); - const Engine::Vertex q0{ - {cell[i].x, cell[i].y, hz}, sn, - {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}}; - const Engine::Vertex q1{ - {cell[i].x, cell[i].y, -hz}, sn, - {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}}; - const Engine::Vertex q2{ - {cell[j].x, cell[j].y, -hz}, sn, - {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}}; - const Engine::Vertex q3{ - {cell[j].x, cell[j].y, hz}, sn, - {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}}; - m.vertices.push_back(q0); - m.vertices.push_back(q1); - m.vertices.push_back(q2); - m.vertices.push_back(q3); - m.indices.insert(m.indices.end(), - {u32(b), u32(b + 1), u32(b + 2), - u32(b), u32(b + 2), u32(b + 3)}); - } - - // 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集) - m.cpuPositions.reserve(m.vertices.size()); - for (const auto& v : m.vertices) { - m.cpuPositions.push_back(v.position); - } - return m; - } - - // ---- 可碎墙(挂墙实体;触发后自管一整批碎片)---- + // ---- 可碎墙(挂墙实体;触发后由 Burst 管一整批碎片)---- class WallFracture final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "WallFracture"; } @@ -213,327 +35,45 @@ namespace { void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { // Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞 // 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化) - pb.Field("Count", &m_Count, 30); - pb.Field("SeedRadius", &m_SeedRadius, 0.9f); // 加密半径 m - pb.Field("MinSeedDist", &m_MinSeedDist, 0.12f); // 种子最小间距 m + pb.Field("Count", &m_P.count, 30); + pb.Field("SeedRadius", &m_P.seedRadius, 0.9f); // 加密半径 m + pb.Field("MinSeedDist", &m_P.minSeedDist, 0.12f); // 种子最小间距 m // 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散: // 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线 // 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导 - pb.Field("BurstSpeed", &m_BurstSpeed, 1.2f); // 近点径向初速 m/s - pb.Field("Falloff", &m_Falloff, 1.0f); // 距离衰减率 1/m - pb.Field("NormalPop", &m_NormalPop, 0.4f); // 法线外推 m/s - pb.Field("UpBias", &m_UpBias, 0.3f); // 上抛偏置 m/s - pb.Field("Lifetime", &m_Lifetime, 6.0f); // 碎片寿命(秒) - pb.Field("MaxFragments", &m_MaxFragments, 128); // 活碎片上限 - pb.Field("MinImpactSpeed", &m_MinImpactSpeed, 2.0f); // 触发阈值 + pb.Field("BurstSpeed", &m_P.burstSpeed, 1.2f); // 近点径向初速 m/s + pb.Field("Falloff", &m_P.falloff, 1.0f); // 距离衰减率 1/m + pb.Field("NormalPop", &m_P.normalPop, 0.4f); // 法线外推 m/s + pb.Field("UpBias", &m_P.upBias, 0.3f); // 上抛偏置 m/s + pb.Field("Lifetime", &m_P.lifetime, 6.0f); // 碎片寿命(秒) + pb.Field("MaxFragments", &m_P.maxFragments, 128); // 活碎片上限 + pb.Field("MinImpactSpeed", &m_P.minImpactSpeed, 2.0f); // 触发阈值 } void OnCollisionEnter(u32 other, const glm::vec3& point, const glm::vec3& normal) override { - if (m_Fired) return; - - // 触发条件:对方是活的 Dynamic 刚体且速度够快(过滤 - // 静止贴靠/角色轻碰——子弹/抛体必中) - auto& scene = App().GetScene(); - const ECS::Entity otherE = EntityFromBits(other); - if (!scene.GetRegistry().valid(otherE)) return; - const auto* otherRb = - scene.GetRegistry().try_get( - otherE); - if (!otherRb || - otherRb->type != - Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { - return; - } - const f32 speed = glm::length( - App().GetPhysics().GetLinearVelocity(other)); - if (speed < m_MinImpactSpeed) return; - - Fire(point, normal); + // 触发条件在库内:对方须是活的 Dynamic 刚体且速度够快 + //(过滤静止贴靠/角色轻碰——子弹/抛体必中) + m_Burst.TryFire(HV(), other, point, normal); } - void OnUpdate(f32 dt) override { - ApplyPendingUnload(); - ApplyPendingVelocities(); - AgeAndCleanup(dt); - } + void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); } // 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理, - // 独占碎片模型统一批量回收(模型不被快照恢复的场景引用—— - // 唯一代键;批量接口内部 GPU 停飞后再销毁) - void OnDestroy() override { - std::vector paths; - paths.reserve(m_Fragments.size() + m_PendingUnload.size()); - for (const Fragment& f : m_Fragments) { - if (!f.modelPath.empty()) paths.push_back(f.modelPath); - } - m_Fragments.clear(); - paths.insert(paths.end(), - std::make_move_iterator(m_PendingUnload.begin()), - std::make_move_iterator(m_PendingUnload.end())); - m_PendingUnload.clear(); - App().GetAssets().UnloadModelsByPaths(paths); - } + // 独占碎片模型统一批量回收 + void OnDestroy() override { m_Burst.Shutdown(HV()); } private: - // ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ---- - void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) { - m_Fired = true; - - // 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active, - // 与 Unity Renderer.enabled + Rigidbody 摘除同语义) - if (auto* mr = TryGetComponent< - Engine::ECS::MeshRendererComponent>()) { - mr->active = false; - } - if (auto* rb = TryGetComponent< - Engine::ECS::RigidBodyComponent>()) { - rb->active = false; - } - - // 墙几何:碎片网格顶点按墙世界尺寸烘焙(墙面 = XY 平面, - // 厚度沿 Z;墙须无旋转摆放,见头注释占位局限) - const auto& wall = Transform(); - const glm::vec3 wallPos = wall.GetWorldPosition(); - const glm::vec3 wallSize = wall.GetWorldScale(); - const f32 hw = 0.5f * wallSize.x; - const f32 hh = 0.5f * wallSize.y; - const f32 hz = 0.5f * wallSize.z; - - // 命中点 → 墙局部(逆旋转 + 平移),XY 即裂纹平面坐标 - const glm::quat invRot = glm::conjugate( - wall.GetWorldRotationQuat()); - const glm::vec3 localHit = invRot * (hitPoint - wallPos); - const glm::vec2 uvHit{localHit.x, localHit.y}; - - // ---- 种子:40% 命中点半径内加密 + 60% 全墙均匀,最小 - // 间距拒绝采样(防碎片退化为细条)---- - const u32 count = std::clamp(m_Count, 4, 96); - const u32 nearCount = count * 2 / 5; - std::vector seeds; - seeds.reserve(count); - const f32 minD2 = m_MinSeedDist * m_MinSeedDist; - auto tryPush = [&](const glm::vec2& s) { - if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return; - for (const glm::vec2& e : seeds) { - if (glm::dot(e - s, e - s) < minD2) return; - } - seeds.push_back(s); - }; - // 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀) - for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount; - ++i) { - const f32 a = Rand(0.0f, glm::two_pi()); - const f32 r = m_SeedRadius * std::sqrt(Rand(0.0f, 1.0f)); - tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)}); - } - // 全墙均匀补足(同款拒绝采样) - for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) { - tryPush({Rand(-hw, hw), Rand(-hh, hh)}); - } - if (seeds.size() < 4) return; // 墙面太小采样失败:不碎 - - // ---- 每种子 → cell → 棱柱 mesh → ParsedModel ---- - // 唯一代键(见 s_GenSeq 注释):形状每次随机;模型回收走 - // Fragment.modelPath + 待卸载队列(ApplyPendingUnload) - const std::string keyBase = - "procedural://vorfrag/" + - std::to_string(s_GenSeq++) + '/'; - // 砖色 baseColor(glTF baseColorFactor 语义:rgb 乘进 albedo) - const glm::vec4 brick{140.0f / 255.0f, 92.0f / 255.0f, - 72.0f / 255.0f, 1.0f}; - - auto& assets = App().GetAssets(); - std::vector refs; - std::vector centroids; // 碎片质心(世界系,初速径向用) - std::vector paths; // 模型缓存键(回收用,与 refs 对齐) - refs.reserve(seeds.size()); - centroids.reserve(seeds.size()); - paths.reserve(seeds.size()); - for (size_t s = 0; s < seeds.size(); ++s) { - const std::vector cell = - VoronoiCell(seeds[s], seeds, hw, hh); - if (cell.size() < 3) { - refs.push_back({}); // 退化 cell:占位,spawn 跳过 - centroids.emplace_back(); - paths.emplace_back(); - continue; - } - PrismMesh pm = CellToPrism(cell, hz); - glm::vec2 c2{0.0f}; - for (const glm::vec2& p : cell) c2 += p; - c2 /= static_cast(cell.size()); - centroids.push_back( - wallPos + wall.GetWorldRotationQuat() * - glm::vec3{c2.x, c2.y, 0.0f}); - - Engine::ParsedModel parsed; - parsed.path = keyBase + std::to_string(s); - Engine::ParsedMesh& mesh = - parsed.meshes.emplace_back(); - mesh.vertices = std::move(pm.vertices); - mesh.lods.push_back(std::move(pm.indices)); - mesh.cpuPositions = std::move(pm.cpuPositions); - mesh.name = "frag" + std::to_string(s); - mesh.params.baseColor = brick; - - const Engine::ModelRef ref = - assets.LoadModel(parsed.path, std::move(parsed)); - refs.push_back(ref); - paths.push_back(parsed.path); - } - - // ---- spawn 碎片实体:pos/rot 复用墙位姿(mesh 已真尺寸, - // scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂瞬间完美 - // 拼合墙面,动起来后沿裂纹自然分离 ---- - for (size_t s = 0; s < refs.size(); ++s) { - if (!refs[s].Valid()) continue; - - auto frag = Spawn("fragment"); - auto& tr = frag.GetComponent< - Engine::ECS::TransformComponent>(); - tr.SetPosition(wallPos); - tr.SetRotationQuat(wall.GetWorldRotationQuat()); - tr.SetScale(glm::vec3{1.0f}); - - auto& fmr = - frag.AddComponent< - Engine::ECS::MeshRendererComponent>(); - fmr.model = refs[s]; - - // Dynamic MeshCollider → ConvexHull(数据源 = 刚上传 - // 的 cpuPositions;Box 字段不参与 Mesh 形状) - auto& frb = - frag.AddComponent< - Engine::ECS::RigidBodyComponent>(); - frb.type = - Engine::ECS::RigidBodyComponent::BodyType::Dynamic; - frb.collider = - Engine::ECS::RigidBodyComponent::ColliderType::Mesh; - frb.mass = 0.3f; - frb.friction = 0.7f; - frb.restitution = 0.05f; - - // 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中点, - // 离命中点越远越接近静止(指数衰减)——只有孔洞周边的 - // 碎片被冲击波推动;法线外推让碎片朝弹侧剥出 - glm::vec3 dir = centroids[s] - hitPoint; - const f32 len = glm::length(dir); - if (len < 1e-3f) { - dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f, - Rand(-1.0f, 1.0f)}; - } else { - dir /= len; - } - const f32 falloff = std::exp(-len * m_Falloff); - const glm::vec3 vel = - dir * m_BurstSpeed * falloff * - Rand(0.4f, 1.1f) + - hitNormal * m_NormalPop * Rand(0.3f, 0.8f) + - glm::vec3{0.0f, m_UpBias * Rand(0.0f, 0.5f), - 0.0f}; - - // 总量上限:超限立即回收最老碎片(防泄漏的兜底闸; - // 寿命清理才是常态回收)。模型延迟一帧卸载 - const u32 bits = BitsOf(frag); - if (m_Fragments.size() >= - static_cast(m_MaxFragments)) { - Destroy(EntityFromBits(m_Fragments.front().bits)); - if (!m_Fragments.front().modelPath.empty()) { - m_PendingUnload.push_back( - std::move(m_Fragments.front().modelPath)); - } - m_Fragments.erase(m_Fragments.begin()); - } - m_Fragments.push_back( - Fragment{bits, vel, 0.0f, false, paths[s]}); - } + // 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/ + // Destroy),库内只走公共 API——见 FractureLib.h HostView 注 + Engine::Fracture::HostView HV() { + return {App(), EntityBits(), + [this](const std::string& n) { return Spawn(n); }, + [this](Engine::ECS::Entity e) { Destroy(e); }}; } - // ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟, - // 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧 - // 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先 - // WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)---- - void ApplyPendingUnload() { - App().GetAssets().UnloadModelsByPaths(m_PendingUnload); - m_PendingUnload.clear(); - } - - // ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ---- - void ApplyPendingVelocities() { - auto& registry = App().GetScene().GetRegistry(); - for (auto it = m_Fragments.begin(); - it != m_Fragments.end();) { - if (it->applied) { ++it; continue; } - const ECS::Entity e = EntityFromBits(it->bits); - if (!registry.valid(e)) { // 实体没了(外部销毁) - // 模型是独占资源(一代一键),实体没了模型也回收 - if (!it->modelPath.empty()) { - m_PendingUnload.push_back( - std::move(it->modelPath)); - } - it = m_Fragments.erase(it); - continue; - } - const auto* rb = registry.try_get< - Engine::ECS::RigidBodyComponent>(e); - if (rb && rb->bodyValid) { - App().GetPhysics().SetLinearVelocity(it->bits, - it->velocity); - it->applied = true; - } - ++it; // body 未建:留待下一帧(对账晚一帧必到) - } - } - - // ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)---- - void AgeAndCleanup(f32 dt) { - auto& registry = App().GetScene().GetRegistry(); - for (auto it = m_Fragments.begin(); - it != m_Fragments.end();) { - it->age += dt; - if (it->age < m_Lifetime) { ++it; continue; } - const ECS::Entity e = EntityFromBits(it->bits); - if (registry.valid(e)) { - Destroy(e); - } - // 模型延迟一帧卸载(本帧渲染收集还引用它) - if (!it->modelPath.empty()) { - m_PendingUnload.push_back(std::move(it->modelPath)); - } - it = m_Fragments.erase(it); - } - } - - static f32 Rand(f32 lo, f32 hi) { - thread_local std::mt19937 rng{std::random_device{}()}; - return std::uniform_real_distribution(lo, hi)(rng); - } - - struct Fragment { - u32 bits = 0; // PhysicsWorld 键(实体位模式) - glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) - f32 age = 0.0f; // 存活秒数(寿命清理用) - bool applied = false; // 初速已注入 - std::string modelPath; // 独占模型缓存键(回收时按路径卸载) - }; - - int m_Count = 30; - f32 m_SeedRadius = 0.9f; - f32 m_MinSeedDist = 0.12f; - f32 m_BurstSpeed = 1.2f; - f32 m_Falloff = 1.0f; - f32 m_NormalPop = 0.4f; - f32 m_UpBias = 0.3f; - f32 m_Lifetime = 6.0f; - int m_MaxFragments = 128; - f32 m_MinImpactSpeed = 2.0f; - - bool m_Fired = false; // 一次性触发保护 - std::vector m_Fragments; // 活碎片簿记 - // 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟, - // 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针) - std::vector m_PendingUnload; + Engine::Fracture::Params m_P; // 参数(DeclareParams 直绑) + Engine::Fracture::Burst m_Burst; // 碎片簿记 + 生命周期(look 默认砖色) }; } // namespace