// // 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)