387 lines
17 KiB
C++
387 lines
17 KiB
C++
//
|
||
// 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)
|