Files
HelloVulkan/App/scripts/GameDirector.cpp
T
atdunbg 334819bf97 修复打靶场射击链路:发射源/玩家控制走 primary 相机实体
- TestShoot 发射源从 EditorCamera(视口 UI 状态,PIE 渲染不用它)
  改为绑定实体位姿,挂 primary 相机实体即第一人称枪口;
  仅光标捕捉态开火,防编辑器交互误射
- 新增 PlayerController:FPS 控制(鼠标捕捉/Esc 释放/点视口重捕、
  yaw+pitch 转向、WASD+Space/Ctrl 移动+Shift 加速),驱动 primary
  相机实体 Transform,PIE 所见即玩家所见
- 靶子/碎片玻璃 alpha 0.42/0.45→0.85、自发光 2→3:低不透明度在
  亮背景下靶子几乎隐形(用户误认 HUD 横条为靶子)
2026-10-09 13:13:52 +08:00

392 lines
17 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.
//
// 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;场景 primary 相机实体
// (CameraComponent.primary=true,PIE 渲染源)挂 PlayerController +
// TestShoot,摆到面向靶阵(如原点朝 -Z 正对 Center 默认位)→ Play:
// 光标自动捕捉(Esc 释放、点视口重捕),WASD/Space/Ctrl 移动 +
// 鼠标转向,左键射击:靶子玻璃式碎裂 + 得分点亮起,倒计时条流逝,
// 时间到结算(控制台输出得分),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 混合 + 靶色自发光
// (alpha 0.85、自发光 3——曾用 alpha 0.42 近乎全透,亮背景
// 下靶子隐形;拉高保靶子在浅色环境里醒目)
mesh.name = "target";
mesh.params.factors = {0.05f, 0.15f, 0.0f, 0.0f};
mesh.params.baseColor = glm::vec4{color, 0.85f};
mesh.params.surface = {1.5f, 0.0f, 3.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)