60 秒限时打靶场玩法原型:碎裂逻辑提取 FractureLib 共享库(HostView 桥接 protected 底座)+ 玻璃靶 Target(跨脚本参数计分)+ GameDirector(靶阵/倒计时/重开/图元 HUD)

This commit is contained in:
2026-10-09 13:00:18 +08:00
parent 6854d53f11
commit f15b104e77
4 changed files with 1103 additions and 497 deletions
+386
View File
@@ -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 <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Core/Log.h>
#include <Engine/Core/Types.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Model.h>
#include "FractureLib.h" // BitsOf/EntityFromBits(碎片簿记同款换算)
#include <glm/glm.hpp>
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
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<u32>(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<std::string> 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<f32>(row) * m_Spacing;
pos.x += static_cast<f32>(col) * m_Spacing -
static_cast<f32>(inRow - 1) * m_Spacing * 0.5f;
SpawnTarget(pos);
}
}
void SpawnTarget(const glm::vec3& pos) {
auto t = Spawn("target");
auto& tr = t.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(pos); // scale 1:尺寸烘焙进模型与碰撞体
auto& mr =
t.AddComponent<Engine::ECS::MeshRendererComponent>();
mr.model = m_TargetModel;
// Static Box:厚 ≥0.2m 时对 25m/s 无 CCD 弹保证检出
//(检测带 2*(弹半径0.12+靶半厚0.1)=0.44m > 每步位移0.42m)
auto& rb = t.AddComponent<Engine::ECS::RigidBodyComponent>();
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>();
Engine::ECS::ScriptComponent::Item item;
item.name = "Target";
item.params.push_back(
{"Director",
Engine::Scripting::ParamValue{
static_cast<i32>(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<int>(
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<int>(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<std::string> 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<i32>(m_Score, 24);
for (i32 i = 0; i < pips; ++i) {
const f32 px = left + 0.18f +
static_cast<f32>(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<u32> m_TargetBits; // 本局靶实体(重开收靶用)
Engine::ModelRef m_TargetModel; // 玻璃立方(参数键缓存共享)
std::string m_ModelKey;
};
} // namespace
ENGINE_REGISTER_SCRIPT(GameDirector)