阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx

- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对),
  指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis
  行走基朝向与跨骨架去重、根运动通道
- 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与
  字符冲量判定
- 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、
  不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖
- 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增
  ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target
- 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板
- 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
This commit is contained in:
2026-10-11 22:22:19 +08:00
parent 334819bf97
commit beba491bcb
57 changed files with 43200 additions and 1378 deletions
+203
View File
@@ -0,0 +1,203 @@
//
// Follower.cpp:跟随者行为(NPC 自动跟随主角,UE AIController 的简化位)。
//
// 挂在每个 NPC 实体上(各自 CharacterController + Animation)。主角由
// TargetName 指定(默认 "player",按 NameComponent 名称匹配——引擎无实体
// 引用参数类型,名称匹配是标准做法)。逻辑分四层,与 WASDMove 同一套手感:
// - 距离闸门(迟滞):拉开到 StopDistance+ResumeGap 才起步,靠近到
// StopDistance 即停——"到附近就停止跟随",边界不抖动
// - 速度分档(迟滞):距离超过 RunDistance 改跑,回落到 0.85× 才退回
// 走——"距离越大走/跑越快"
// - 动量整形:真实速度逐帧逼近期望速度(起步渐快、到点刹停),帧率无关
// - 动画按真实速度分档(idle/walk/run),迟滞防抖
// 移动驱动走 PhysicsWorld 的 CharacterController 门面(水平喂意图速度,
// Y 归物理重力);无 CC 时回退直接平移。多个跟随者用 Offset 错开站位。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include "ScriptLog.h"
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine;
namespace {
class Follower final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Follower"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 相对目标的站位偏移(世界系;多个跟随者错开,防叠一起)
pb.Field("Offset", &m_Offset, glm::vec3{0.0f});
pb.Field("WalkSpeed", &m_WalkSpeed, 3.0f); // 走路 m/s
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
pb.Field("StopDistance", &m_StopDistance, 2.5f); // 到点停(米)
pb.Field("ResumeGap", &m_ResumeGap, 1.5f); // 再起步的距离余量
pb.Field("RunDistance", &m_RunDistance, 10.0f); // 超过即改跑(米)
pb.Field("TurnResponse", &m_TurnResponse, 10.0f);// 转向阻尼
pb.Field("AccelResponse", &m_AccelResponse, 6.0f); // 加速响应
pb.Field("StopResponse", &m_StopResponse, 12.0f); // 制动响应
// clip 按名引用(Unity anim.Play("Walk") 语义)
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
if (ResolveTarget()) {
SLOG_INFO("[Follower] 跟随目标 '{}' 就绪", m_TargetName);
} else {
SLOG_WARN("[Follower] 未找到目标 '{}'(按 NameComponent 匹配)",
m_TargetName);
}
}
void OnUpdate(float dt) override {
auto& reg = App().GetScene().GetRegistry();
// 目标失效(被销毁/未就绪)→ 重解析;仍无则静止待命
if (m_Target == entt::null || !reg.valid(m_Target)) {
m_Target = entt::null;
if (!ResolveTarget()) {
return;
}
}
const glm::vec3 tp =
reg.get<Engine::ECS::TransformComponent>(m_Target)
.GetPosition();
const glm::vec3 self = Transform().GetPosition();
glm::vec3 delta = (tp + m_Offset) - self;
delta.y = 0.0f; // 只取水平面(角色不俯仰)
const float dist = glm::length(delta);
// ---- 距离闸门(迟滞):到点停、拉开距离再起步 ----
if (!m_Moving) {
if (dist > m_StopDistance + m_ResumeGap) {
m_Moving = true;
}
} else if (dist <= m_StopDistance) {
m_Moving = false;
}
// ---- 速度分档(迟滞):远则跑、近则走 ----
glm::vec3 desired{0.0f};
if (m_Moving && dist > 1e-4f) {
if (!m_Sprinting && dist >= m_RunDistance) {
m_Sprinting = true;
} else if (m_Sprinting && dist <= m_RunDistance * 0.85f) {
m_Sprinting = false;
}
const float speed = m_Sprinting ? m_RunSpeed : m_WalkSpeed;
desired = (delta / dist) * speed;
}
// ---- 动量整形:真实速度逐帧逼近期望(起步渐快/到点刹停)----
const float response =
glm::length(desired) > 0.01f ? m_AccelResponse : m_StopResponse;
m_PlanarVel +=
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
if (glm::length(desired - m_PlanarVel) < 0.02f) {
m_PlanarVel = desired; // 贴齐,防低速蠕动
}
// ---- 驱动:角色控制器喂水平速度(Y 归物理);无则直接平移 ----
auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
nullptr;
if (hasCC) {
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else {
Transform().SetPosition(self + m_PlanarVel * dt);
}
// ---- 朝向:面向真实速度方向(模型默认 +Z),低速锁定 ----
if (glm::length(m_PlanarVel) > 0.3f) {
const float targetYaw =
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
const glm::vec3 modelFwd =
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
std::cos(targetYaw - curYaw));
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
Transform().SetRotationQuat(
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:按真实速度分档(迟滞防临界抖)----
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim =
TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
}
}
}
private:
// 按 NameComponent 名称解析目标(跳过自身);失败返回 false
bool ResolveTarget() {
auto& reg = App().GetScene().GetRegistry();
auto view = reg.view<Engine::ECS::NameComponent>();
for (auto e : view) {
if (static_cast<u32>(entt::to_integral(e)) == EntityBits()) {
continue;
}
if (view.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
return true;
}
}
return false;
}
std::string m_TargetName = "player";
glm::vec3 m_Offset{0.0f};
float m_WalkSpeed = 3.0f;
float m_RunSpeed = 6.0f;
float m_StopDistance = 2.5f;
float m_ResumeGap = 1.5f;
float m_RunDistance = 10.0f;
float m_TurnResponse = 10.0f;
float m_AccelResponse = 6.0f;
float m_StopResponse = 12.0f;
std::string m_IdleClip = "idle";
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
entt::entity m_Target = entt::null; // 运行时状态(非参数)
bool m_Moving = false; // 距离闸门状态(迟滞)
bool m_Sprinting = false; // 速度档状态(迟滞)
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度
std::string m_CurClip = "idle"; // 当前 clip(边沿检测基准)
};
ENGINE_REGISTER_SCRIPT(Follower)
} // namespace
+165 -23
View File
@@ -17,8 +17,10 @@
// 墙脚"的观感。
//
// 用法(宿主 = 挂在可碎实体上的脚本):
// Fracture::Params p_; // DeclareParams 直绑成员(pb.Field)
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
// 绑到独立副本会让 Count/Lifetime 等全部失效
// HostView HV() { // 桥接(脚本自身成员函数内可访问
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
// [this](auto& n) { return Spawn(n); },
@@ -27,8 +29,9 @@
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
// OnUpdate → burst_.Tick(HV(), dt)
// OnDestroy → burst_.Shutdown(HV())
// 外观:Fire 前改 burst_.look(默认砖色不透明;玻璃靶 = 半透明
// + 自发光)
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
@@ -148,23 +151,32 @@ namespace Engine::Fracture {
std::vector<glm::vec3> cpuPositions;
};
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz) {
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
// 贴图与墙面连续,不会各碎片自成一格
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
f32 hw, f32 hh) {
PrismMesh m;
const size_t n = cell.size();
const size_t topBase = 0;
const size_t botBase = n;
m.vertices.reserve(n * 2);
const f32 invW = hw > 1e-6f ? 0.5f / hw : 0.0f;
const f32 invH = hh > 1e-6f ? 0.5f / hh : 0.0f;
const auto uvOf = [&](const glm::vec2& p) {
return glm::vec2{p.x * invW + 0.5f, p.y * invH + 0.5f};
};
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}};
uvOf(p), {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}};
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
@@ -216,6 +228,47 @@ namespace Engine::Fracture {
return m;
}
// ---- 程序化纯色还原(引擎零改动:颜色只存在于伪路径里)----
// 程序化 Cube/Plane 把颜色烘进 1x1 sRGB 纹理,MaterialParams 留白、
// 内建材质快照也没有颜色字段,故从伪路径尾段 "r,g,b[,a]" 还原线性
// 底色——与 GPU 采样 sRGB 纹理的硬件线性化同式,直乘 baseColor 才等价
inline f32 SrgbByteToLinear(u8 c) {
const f32 s = static_cast<f32>(c) / 255.0f;
return s <= 0.04045f ? s / 12.92f
: std::pow((s + 0.055f) / 1.055f, 2.4f);
}
// "procedural://cube/0.5/140,92,72" → 取最后一个 '/' 之后的纯数字
// 逗号段;段数 < 3 或含非数字(如 halfSize 段)判失败
inline bool ParseProceduralColor(const std::string& path, glm::vec4& out) {
if (path.rfind("procedural://", 0) != 0) return false;
const size_t slash = path.find_last_of('/');
if (slash == std::string::npos) return false;
const std::string tail = path.substr(slash + 1);
int c[3] = {0, 0, 0};
int n = 0;
size_t pos = 0;
while (n < 3 && pos < tail.size()) {
const size_t comma = tail.find(',', pos);
const size_t end =
(comma == std::string::npos) ? tail.size() : comma;
if (end == pos) return false;
int v = 0;
for (size_t i = pos; i < end; ++i) {
const char ch = tail[i];
if (ch < '0' || ch > '9') return false;
v = v * 10 + (ch - '0');
}
c[n++] = v;
if (comma == std::string::npos) break;
pos = comma + 1;
}
if (n < 3) return false;
out = glm::vec4{SrgbByteToLinear(static_cast<u8>(c[0])),
SrgbByteToLinear(static_cast<u8>(c[1])),
SrgbByteToLinear(static_cast<u8>(c[2])), 1.0f};
return true;
}
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
struct Params {
i32 count = 30; // Voronoi 种子数 = 碎片数
@@ -230,9 +283,15 @@ namespace Engine::Fracture {
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
};
// ---- 碎片外观(Fire 时套用到全部碎片网格;默认砖色不透明 =
// 原 WallFracture 观感)----
// ---- 碎片外观 ----
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
// SceneRenderer 收集期的 resolve 同序
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
// 的显式字段(Fire 前设置)
struct Appearance {
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
72.0f / 255.0f, 1.0f};
bool transparent = false; // 透明绘制队列(玻璃碎片)
@@ -254,20 +313,36 @@ namespace Engine::Fracture {
const glm::vec3& normal) {
if (m_Fired) return false;
// 对方须是活的 Dynamic 刚体且速度够快(过滤静止贴靠/
// 角色轻碰——子弹/抛体必中)
// 对方须"够快"且是可动的撞击来源,二选一:
// - 动态刚体(子弹/抛体):用实时线速度
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
// 实时速度往往已归零,会漏判(见
// PhysicsWorld::GetCharacterImpactVelocity)
// 速度低于阈值(静止贴靠/轻碰)则忽略
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity otherE = EntityFromBits(other);
if (!registry.valid(otherE)) return false;
const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(otherE);
if (!otherRb ||
otherRb->type !=
f32 speed = 0.0f;
if (const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
otherE)) {
if (otherRb->type !=
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
return false;
return false; // 静态/运动学体不算撞击
}
speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
} else if (registry.all_of<
Engine::ECS::CharacterControllerComponent>(
otherE)) {
// 玩家/角色撞击:角色碰撞事件由 PhysicsWorld 合成
// (CharacterVirtual 不进 ContactListener)
speed = glm::length(
host.app.GetPhysics().GetCharacterImpactVelocity(other));
} else {
return false; // 无物理来源(纯 Transform 实体)
}
const f32 speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
if (speed < p.minImpactSpeed) return false;
m_Fired = true;
@@ -335,6 +410,52 @@ namespace Engine::Fracture {
const f32 hh = 0.5f * wallSize.y;
const f32 hz = 0.5f * wallSize.z;
// ---- 碎片外观:解析宿主(墙)的生效材质 ----
// 与 SceneRenderer 收集期 resolve 同序:实体级覆盖
//(.material 资产,按 mesh 下标查表)优先,模型内建材质兜底
//(MeshMaterialInfo 为权威只读描述)。不继承则碎片只剩默认
// 砖色,与墙面目视不符
Engine::MaterialAssetData effMat;
glm::vec4 effSolid{0.0f}; // 程序化纯色 albedo(a>0 = 有效)
std::string hostOverridePath; // 宿主 .material 覆盖(空 = 内建材质)
bool hasEffMat = false;
if (look.inheritFromHost) {
const auto* mr = registry.try_get<
Engine::ECS::MeshRendererComponent>(self);
const int slot =
(mr && mr->meshIndex >= 0) ? mr->meshIndex : 0;
if (mr) {
const auto oIt =
mr->materialOverrides.find(static_cast<u32>(slot));
if (oIt != mr->materialOverrides.end()) {
if (auto* asset =
host.app.GetAssets().FindMaterialAsset(
oIt->second)) {
effMat = asset->GetData();
hostOverridePath = oIt->second;
hasEffMat = true;
}
}
}
if (!hasEffMat && mr && mr->model.Valid()) {
if (auto* model =
host.app.GetAssets().GetModel(mr->model)) {
const auto& infos = model->GetMeshMaterialInfos();
if (slot < static_cast<int>(infos.size())) {
effMat.texPaths = infos[slot].texPaths;
effMat.params = infos[slot].params;
effMat.transparent = infos[slot].transparent;
hasEffMat = true;
}
}
// 程序化纯色墙:颜色在生成的 1x1 纹理里(快照无颜色
// 字段),改由脚本从模型伪路径还原线性底色
ParseProceduralColor(
host.app.GetAssets().Models().Path(mr->model),
effSolid);
}
}
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
const glm::quat invRot = glm::conjugate(
wall.GetWorldRotationQuat());
@@ -391,7 +512,7 @@ namespace Engine::Fracture {
paths.emplace_back();
continue;
}
PrismMesh pm = CellToPrism(cell, hz);
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
glm::vec2 c2{0.0f};
for (const glm::vec2& q : cell) c2 += q;
c2 /= static_cast<f32>(cell.size());
@@ -407,11 +528,27 @@ namespace Engine::Fracture {
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;
if (hasEffMat) {
// 继承墙的生效材质:底色/贴图/UV 变换/透明全套沿用,
// 碎块与墙面目视一致(UV 已在 CellToPrism 归一到墙面)
mesh.params = effMat.params;
mesh.texPaths = effMat.texPaths;
mesh.transparent = effMat.transparent;
// 程序化纯色墙(无贴图路径):用脚本解析出的线性底色当
// baseColor——碎片 albedo 走默认白纹理,直乘即该色;
// 不走这步会退回 MaterialParams 的白色默认值
if (effSolid.a > 0.0f) {
mesh.params.baseColor = effSolid;
}
} else {
// 无宿主材质可继承(或 look.inheritFromHost=false):
// 用显式外观(玻璃靶 = 半透明 + 自发光)
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));
@@ -436,6 +573,11 @@ namespace Engine::Fracture {
frag.AddComponent<
Engine::ECS::MeshRendererComponent>();
fmr.model = refs[s];
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
//(含透明通道)与墙完全一致,且不需要引擎改判定
if (!hostOverridePath.empty()) {
fmr.materialOverrides[0] = hostOverridePath;
}
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
-391
View File
@@ -1,391 +0,0 @@
//
// 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)
-151
View File
@@ -1,151 +0,0 @@
//
// PlayerController.cpp:FPS 玩家控制脚本——挂在场景 primary 相机实体
// (CameraComponent.primary=true)上,直接驱动该实体的 Transform。
//
// 为什么驱动实体而不是"编辑器相机":PIE 内嵌播放/游戏形态的渲染源
// 是 primary 相机实体(SceneRenderer::BuildFrame 分派,main.cpp 亦
// 注明"PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱")。
// 实体位姿一动,视口所见即玩家所见——这才是 Unity/UE 的正道;
// EditorCamera 是视口 UI 状态,不是场景实体,脚本够不着也不该够。
//
// 能力(全部走 Input 门面,零引擎改动):
// - 鼠标捕捉:OnStart 捕捉(FPS 语义:光标隐入、无限转向);
// Esc 释放(交还编辑器交互);未捕捉时点视口重新捕捉
// - 视角:GetMouseDelta 增量 → yaw 绕世界 up 左乘 + pitch 绕本地
// X 右乘(±89° 钳制,与编辑器相机 Camera::Update 同语义)
// - 移动:WASD 水平 + Space 升 / Ctrl 降(飞行式,无重力——
// 打靶场微调机位用);Shift 加速
//
// 注意:捕捉态下编辑器 UI 收不到鼠标——这是游戏体验的代价;
// Esc 是唯一退路(释放后可点 Inspector/改参数)。
//
// 配套:TestShoot(挂同一实体)射击,同样只在捕捉态开火。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Input.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
using namespace Engine; // Input 门面
using Engine::Scripting::ParamBuilder;
namespace {
class PlayerController final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PlayerController"; }
void DeclareParams(ParamBuilder& pb) override {
pb.Field("LookSensitivity", &m_Sensitivity, 0.12f); // 度/像素
pb.Field("MoveSpeed", &m_MoveSpeed, 4.0f); // 平移速度 m/s
pb.Field("RunSpeed", &m_RunSpeed, 8.0f); // Shift 加速 m/s
pb.Field("CaptureOnStart", &m_CaptureOnStart, true);
}
void OnStart() override {
if (m_CaptureOnStart) {
Input::SetCursorCaptured(true);
}
}
void OnUpdate(f32 dt) override {
// ---- 捕捉管理:Esc 释放 / 点视口重捕 ----
bool captured = Input::IsCursorCaptured();
if (captured && Input::IsKeyDown(Input::Key::Escape)) {
Input::SetCursorCaptured(false);
captured = false;
}
const bool leftDown =
Input::IsMouseButtonDown(Input::MouseButton::Left);
if (!captured) {
// 未捕捉时点击(点在视口内才收得到——ImGui 捕获的
// 区域静默)→ 重捕进入操作态
if (leftDown && !m_LeftWasDown) {
Input::SetCursorCaptured(true);
}
m_LeftWasDown = leftDown;
return; // 释放态:不转向不移动(编辑器交互让路)
}
m_LeftWasDown = leftDown;
Look();
Move(dt);
}
private:
void Look() {
f32 dx = 0.0f, dy = 0.0f;
Input::GetMouseDelta(dx, dy);
if (dx == 0.0f && dy == 0.0f) return;
auto& tr = Transform();
const glm::quat cur = tr.GetRotationQuat();
// yaw:绕世界 up 左乘(不随俯仰改变旋转轴)
const glm::quat yaw =
glm::angleAxis(glm::radians(-dx * m_Sensitivity),
glm::vec3(0.0f, 1.0f, 0.0f));
// pitch:绕本地 X 右乘,钳制 ±89°(防过顶翻转)
const glm::vec3 fwd = cur * glm::vec3(0.0f, 0.0f, -1.0f);
const f32 pitch =
glm::asin(glm::clamp(fwd.y, -1.0f, 1.0f));
const f32 kPitchLimit = glm::radians(89.0f);
const f32 pitchTarget =
glm::clamp(pitch + glm::radians(-dy * m_Sensitivity),
-kPitchLimit, kPitchLimit);
const glm::quat pitchStep =
glm::angleAxis(pitchTarget - pitch,
glm::vec3(1.0f, 0.0f, 0.0f));
tr.SetRotationQuat(yaw * (cur * pitchStep));
}
void Move(f32 dt) {
glm::vec3 axis{0.0f};
if (Input::IsKeyDown(Input::Key::W))
axis.z += 1.0f;
if (Input::IsKeyDown(Input::Key::S))
axis.z -= 1.0f;
if (Input::IsKeyDown(Input::Key::D))
axis.x += 1.0f;
if (Input::IsKeyDown(Input::Key::A))
axis.x -= 1.0f;
if (Input::IsKeyDown(Input::Key::Space))
axis.y += 1.0f;
if (Input::IsKeyDown(Input::Key::LeftControl))
axis.y -= 1.0f;
if (glm::dot(axis, axis) < 1e-6f) return;
const glm::quat r = Transform().GetRotationQuat();
const glm::vec3 fwd = r * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 right = r * glm::vec3(1.0f, 0.0f, 0.0f);
// 水平走视线投影(含俯仰 = 飞行感),升降用世界 Y
glm::vec3 move =
fwd * axis.z + right * axis.x
+ glm::vec3(0.0f, axis.y, 0.0f);
const f32 len = glm::length(move);
if (len < 1e-6f) return;
move /= len;
const f32 speed = Input::IsKeyDown(Input::Key::LeftShift)
? m_RunSpeed
: m_MoveSpeed;
auto& tr = Transform();
tr.SetPosition(tr.GetWorldPosition() + move * speed * dt);
}
f32 m_Sensitivity = 0.12f;
f32 m_MoveSpeed = 4.0f;
f32 m_RunSpeed = 8.0f;
bool m_CaptureOnStart = true;
bool m_LeftWasDown = false; // 重捕用的左键上升沿基准
};
ENGINE_REGISTER_SCRIPT(PlayerController)
} // namespace
+17
View File
@@ -0,0 +1,17 @@
//
// ScriptLog.h:脚本日志门面(ScriptDbg 的文字版同位物)。
//
// 脚本打日志统一走这里——直接转发引擎 spdlog 默认 logger(终端
// + HelloVulkan.log + 编辑器 Log Panel 三处同收)。约定:消息开头
// 写 [脚本名] 前缀(面板过滤好定位):
// ScriptLog::Info("[WASDMove] jump");
//
#pragma once
#include <Engine/Core/Log.h>
#define SLOG_TRACE(...) ::spdlog::trace(__VA_ARGS__)
#define SLOG_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
#define SLOG_INFO(...) ::spdlog::info(__VA_ARGS__)
#define SLOG_WARN(...) ::spdlog::warn(__VA_ARGS__)
#define SLOG_ERROR(...) ::spdlog::error(__VA_ARGS__)
-120
View File
@@ -1,120 +0,0 @@
//
// 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 <Engine/Scripting/Script.h>
#include <Engine/Scripting/ScriptRuntime.h> // GetScripts() 跨脚本参数访问
#include <Engine/Core/Types.h>
#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 实时改色)。
// alpha 0.85(与靶体 BuildGlassCube 同步:0.45 时代碎片
// 在亮背景里几乎隐形)
m_Burst.look.baseColor = glm::vec4{m_Color, 0.85f};
m_Burst.look.transparent = true;
m_Burst.look.emissive = m_Color * 0.7f;
m_Burst.look.emissiveStrength = 3.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<u32>(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<bool>(&v.v); b && !*b) {
return; // 回合已结束(Director 收靶前的漏网命中)
}
}
i32 score = 0;
if (scripts.GetParam(director, "GameDirector", "Score", v)) {
if (const auto* s = std::get_if<i32>(&v.v)) {
score = *s;
}
}
scripts.SetParam(director, "GameDirector", "Score",
ParamValue{static_cast<i32>(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)
+20 -35
View File
@@ -1,16 +1,11 @@
//
// TestShoot.cpp:射击脚本(发射源 = 绑定实体位姿,挂谁从谁发)。
//
// 正确用法:挂到场景 primary 相机实体(CameraComponent.primary=true)
// 上——PIE/游戏形态渲染源就是它(SceneRenderer::BuildFrame 分派),
// 实体朝向即准星,实体位置 + MuzzleOffset 即枪口。配 PlayerController
// (FPS 转向/移动/鼠标捕捉)即完整第一人称射击。
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
// 转发的端到端链路。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂相机实体):
// 光标捕捉态下鼠标左键上升沿 → 从绑定实体沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入);
// 未捕捉(编辑器交互中)不射——防点 UI 误发
// TestShoot(挂任意实体,如相机实体):
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
@@ -18,11 +13,6 @@
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 历史注:早期版本从编辑器相机(EditorCamera)发射——那是视口 UI
// 状态,不是场景实体,PIE 渲染根本不用它(渲染源 = primary 相机
// 实体),子弹会从视口外飞出。已改为绑定实体位姿(Unity 组件语义:
// 挂谁从谁发)。
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
@@ -31,9 +21,8 @@
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:primary 相机实体挂 PlayerController + 本脚本 → Play →
// 鼠标转视角 → 左键射击:弹道点飞出 → 撞击瞬间原地绽放十字特效、
// 子弹消失。
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
//
#include <Engine/Scripting/Script.h>
@@ -41,6 +30,8 @@
#include <Engine/Core/Input.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
@@ -148,7 +139,7 @@ namespace {
f32 m_Age = 0.0f;
};
// ---- 枪口脚本(挂谁从谁发:位姿取绑定实体,正挂 primary 相机)----
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
class TestShoot final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "TestShoot"; }
@@ -167,18 +158,10 @@ namespace {
void OnUpdate(f32 dt) override {
m_Cooldown -= dt;
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
// 捕捉态门控:光标未捕捉 = 用户在跟编辑器 UI 交互,不射
// (m_FireWasDown 照常对账,重捕按下的那一下不误发)
if (!Input::IsCursorCaptured()) {
m_FireWasDown = down;
return;
}
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
const bool fire = down && !m_FireWasDown &&
m_Cooldown <= 0.0f;
m_FireWasDown = down;
@@ -190,18 +173,20 @@ namespace {
private:
void SpawnBullet() {
// 枪口 = 绑定实体视线前方(挂 primary 相机实体 = 第一人称
// 射击;PIE 渲染源就是这台相机,所见即所射)
const glm::quat rot = Transform().GetWorldRotationQuat();
const glm::vec3 fwd = rot * glm::vec3(0.0f, 0.0f, -1.0f);
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
// 与 WASDMove 的朝向来源一致)
const auto& cam =
App().GetRenderer().GetEditorCamera().GetTransform();
const glm::vec3 fwd =
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 muzzle =
Transform().GetWorldPosition() + fwd * m_MuzzleOffset;
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(rot);
tr.SetRotationQuat(cam.GetRotationQuat());
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
+188
View File
@@ -0,0 +1,188 @@
//
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
//
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
// 撞墙)避免画面抖动。
//
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h>
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <cmath>
using namespace Engine;
namespace {
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "ThirdPersonCamera"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Sensitivity", &m_Sensitivity, 1.0f);
pb.Field("Distance", &m_Distance, 0.0f); // 0 = 吸编辑器机位
pb.Field("HeightOffset", &m_HeightOffset, 1.2f);
pb.Field("PitchMinDeg", &m_PitchMin, -35.0f);
pb.Field("PitchMaxDeg", &m_PitchMax, 75.0f);
pb.Field("GamepadTurnSpeed", &m_PadTurnSpeed, 180.0f);
// 跟随目标按名锁定(默认 "player"):场景有多个角色控制器
// 实体(如跟随 NPC)时,按名解析保证相机锁主角而非随机命中
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 竖直跟随阻尼(1/s,指数逼近;0 = 硬跟=旧行为)。水平 XZ
// 恒硬跟(保证跑动时相机/角色不错位、无拖影——全轴阻尼会
// 让角色在高速移动时被"甩"出画面并抖动);仅 Y 阻尼吸收
// 角色脚底每物理步的微弹跳(踩碎块/撞墙),帧率无关
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
pb.Field("SnapDistance", &m_SnapDistance, 3.0f);
}
void OnStart() override {
auto& reg = App().GetScene().GetRegistry();
// ---- 找目标:优先按名(TargetName)命中主角 ----
auto byName = reg.view<Engine::ECS::NameComponent>();
for (auto e : byName) {
if (byName.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
break;
}
}
// 名称未命中(未命名/改名)→ 回退第一个角色控制器实体
if (m_Target == entt::null) {
auto view = reg.view<Engine::ECS::CharacterControllerComponent>();
if (view.empty()) {
ENGINE_WARN(
"[ThirdPersonCamera] 场景无 CharacterController 实体,相机静止");
return;
}
m_Target = *view.begin();
}
// ---- 从编辑器机位反推 yaw/pitch/dist(摆哪开场在哪)----
const auto& camT = Transform();
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 offset =
camT.GetPosition() -
(tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f));
const float dist = glm::length(offset);
if (dist > 1e-4f && m_Distance <= 0.0f) {
m_Distance = dist;
m_Pitch =
glm::degrees(std::asin(std::clamp(offset.y / dist,
-1.0f, 1.0f)));
m_Yaw = glm::degrees(std::atan2(offset.x, offset.z));
}
ENGINE_INFO(
"[ThirdPersonCamera] 目标已锁定,dist={:.2f} yaw={:.1f} pitch={:.1f}",
m_Distance, m_Yaw, m_Pitch);
Input::SetCursorCaptured(true);
}
void OnUpdate(float dt) override {
if (m_Target == entt::null) {
return;
}
// 光标管理:Esc 释放(Inspector 调参),左键重捕。
// 手柄玩家不碰鼠标——光标保持捕获态不影响摇杆输入
if (Input::IsKeyDown(Input::Key::Escape)) {
Input::SetCursorCaptured(false);
}
if (Input::IsMouseButtonDown(Input::MouseButton::Left)) {
Input::SetCursorCaptured(true);
}
// ---- 鼠标增量 + 右摇杆 → 球面角 ----
// 鼠标 = 相对增量(GetMouseDelta);右摇杆 = 持续量
//(Look 动作,满推 m_PadTurnSpeed 度/秒),两者叠加
float dx = 0.0f, dy = 0.0f;
Input::GetMouseDelta(dx, dy);
const glm::vec2 padLook = Input::ActionValue2("Look");
const float k = 0.12f * m_Sensitivity;
m_Yaw -= dx * k + padLook.x * m_PadTurnSpeed * dt;
m_Pitch += dy * k + padLook.y * m_PadTurnSpeed * dt;
m_Pitch = std::clamp(m_Pitch, m_PitchMin, m_PitchMax);
m_Yaw = std::fmod(m_Yaw, 360.0f);
// ---- 位置 = 目标枢轴 + 球面偏移 ----
// 水平 XZ 硬跟(与角色零滞后——跑动时相机/角色不错位、无
// 拖影),仅 Y 经阻尼逼近(FollowResponse):吸收目标脚底
// 每物理步的微弹跳(踩碎块/撞墙),相机竖直方向不再 1:1
// 抖动。首帧、瞬移(超 SnapDistance)或阻尼为 0 时直接吸附
// = 旧硬跟语义
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 rawPivot =
tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f);
if (!m_FollowInit || m_FollowResponse <= 0.0f ||
std::abs(rawPivot.y - m_FollowPivotY) > m_SnapDistance) {
m_FollowPivotY = rawPivot.y;
m_FollowInit = true;
} else {
m_FollowPivotY +=
(rawPivot.y - m_FollowPivotY) *
(1.0f - std::exp(-m_FollowResponse * dt));
}
glm::vec3 pivot = rawPivot; // XZ 硬跟
pivot.y = m_FollowPivotY; // Y 阻尼
const float yawR = glm::radians(m_Yaw);
const float pitchR = glm::radians(m_Pitch);
const glm::vec3 dir( // 枢轴 → 相机(yaw 绕 Y,pitch 抬升)
std::sin(yawR) * std::cos(pitchR),
std::sin(pitchR),
std::cos(yawR) * std::cos(pitchR));
auto& camT = Transform();
camT.SetPosition(pivot + dir * m_Distance);
// ---- 朝向锁定目标 ----
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
// 反解:pitch = asin(vd.y),yaw = atan2(-vd.x, -vd.z)
const glm::vec3 vd = glm::normalize(pivot - camT.GetPosition());
const float p = glm::degrees(std::asin(std::clamp(vd.y, -1.0f, 1.0f)));
const float y = glm::degrees(std::atan2(-vd.x, -vd.z));
camT.SetRotation(glm::vec3(p, y, 0.0f));
}
private:
float m_Sensitivity = 1.0f;
float m_Distance = 0.0f; // 0 = OnStart 吸编辑器机位
float m_HeightOffset = 1.2f; // 枢轴高度(瞄胸口)
float m_PitchMin = -35.0f;
float m_PitchMax = 75.0f;
float m_PadTurnSpeed = 180.0f; // 右摇杆满推角速度(度/秒)
std::string m_TargetName = "player"; // 目标实体名(NameComponent)
float m_FollowResponse = 12.0f; // 竖直跟随阻尼 1/s(0 = 硬跟)
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
bool m_FollowInit = false; // 首帧吸附基准
float m_Yaw = 0.0f;
float m_Pitch = 20.0f;
entt::entity m_Target = entt::null;
};
ENGINE_REGISTER_SCRIPT(ThirdPersonCamera)
} // namespace
+79 -31
View File
@@ -1,5 +1,9 @@
//
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
//
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
//
// 移动语义分派(按实体配置自动选择,优先级从上到下):
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
@@ -19,6 +23,7 @@
#include <Engine/Core/Input.h>
#include <Engine/Debug/DebugDraw.h>
#include "ScriptDebug.h"
#include "ScriptLog.h"
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
@@ -49,37 +54,68 @@ namespace {
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
SLOG_TRACE("hello");
SLOG_INFO("hello");
SLOG_DEBUG("hello");
SLOG_WARN("hello");
SLOG_ERROR("hello");
SLOG_INFO("[WASDMove] 就绪:走 {:.1f} m/s / 跑 {:.1f} m/s / 跳 {:.1f} m/s",
m_Speed, m_RunSpeed, m_JumpSpeed);
}
void OnUpdate(float dt) override {
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
// 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
// 键盘时门面静默。要手柄/运行时改键再换动作层
//(Input::ActionValue2("Move")——Unity6 新 Input System /
// UE5 Enhanced Input 对应物),本示例保持最简
const glm::quat cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
.GetRotationQuat();
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
// 手柄一套代码,将来改键/加绑定不进脚本
// 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它
// 的朝向——玩家看到的视角就是移动参考);无相机实体时
//(纯独立形态)回退编辑器相机
glm::quat cq;
bool refCam = false;
auto camView = App().GetScene()
.GetRegistry()
.view<Engine::ECS::CameraComponent>();
for (auto e : camView) {
const auto& cam =
camView.get<Engine::ECS::CameraComponent>(e);
if (cam.primary && cam.active) {
cq = App().GetScene()
.GetRegistry()
.get<Engine::ECS::TransformComponent>(e)
.GetRotationQuat();
refCam = true;
break;
}
}
if (!refCam) {
cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
.GetRotationQuat();
}
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
glm::vec2 mv(0.0f);
if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f;
if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f;
if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f;
if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f;
const glm::vec2 mvRaw = Input::ActionValue2("Move");
glm::vec2 mv = mvRaw;
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
move.y = 0.0f;
if (glm::length(move) > 1e-4f) {
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
if (glm::length(move) > 1.0f) {
move = glm::normalize(move);
}
// 走/跑
const bool moving = glm::length(mv) > 0.1f;
const bool running =
moving && Input::IsKeyDown(Input::Key::LeftShift);
// 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺
// 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0,
// 那样判会把走路误判成跑
const bool running = Input::ActionDown("Run");
const float speed = running ? m_RunSpeed : m_Speed;
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
@@ -95,6 +131,7 @@ namespace {
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
//(起跳后不宜急转弯)。指数逼近,帧率无关
const bool moving = glm::length(mvRaw) > 0.1f;
float response = moving ? m_AccelResponse : m_StopResponse;
if (!grounded) {
response *= 0.25f;
@@ -113,15 +150,14 @@ namespace {
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
// 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住
// 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防
// 空中连跳
const bool jumpDown = Input::IsKeyDown(Input::Key::Space);
if (jumpDown && !m_JumpWasDown &&
// 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘
// Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳
if (Input::ActionPressed("Jump") &&
phy.IsCharacterGrounded(EntityBits())) {
phy.Jump(EntityBits(), m_JumpSpeed);
SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)",
m_JumpSpeed);
}
m_JumpWasDown = jumpDown;
} else if (auto* rb =
TryGetComponent<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid &&
@@ -155,11 +191,24 @@ namespace {
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)----
// 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以
// 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无
// 条件 Play 会不停重置播放头,动画永远播不完
const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip);
// ---- 动画:按真实速度分档(迟滞防临界抖)----
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
// 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与
// 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞
//(进 0.9×RunSpeed / 退 0.7×RunSpeed):摇杆在阈值附近晃
// 不抖档。引擎 Play(名) 自带相位对齐 + crossfade,切档
// 突兀感只剩判据本身的问题
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
@@ -221,7 +270,6 @@ namespace {
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测)
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
};
+25 -19
View File
@@ -1,15 +1,19 @@
//
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
// DeclareParams 直绑 Fracture::Params(参数名/默认值与历史档位
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
// 逐一同名同值,旧场景不受影响)
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方 Dynamic
// 速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
//
// 外观默认 = 砖色不透明(原观感);玻璃/发光碎片换 Target.cpp 的
// 用法(Fire 前改 Burst::look)。
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
// 再用 baseColor/emissive 显式指定。
//
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
@@ -35,25 +39,27 @@ namespace {
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
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
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
// 独立 m_P)会让计数器/寿命等全部失效
pb.Field("Count", &m_Burst.p.count, 30);
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.9f); // 加密半径 m
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.12f); // 种子最小间距 m
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
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); // 触发阈值
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 触发条件在库内:对方须是活的 Dynamic 刚体且速度够快
//(过滤静止贴靠/角色轻碰——子弹/抛体必中)
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
m_Burst.TryFire(HV(), other, point, normal);
}
@@ -72,8 +78,8 @@ namespace {
[this](Engine::ECS::Entity e) { Destroy(e); }};
}
Engine::Fracture::Params m_P; // 参数(DeclareParams 直绑)
Engine::Fracture::Burst m_Burst; // 碎片簿记 + 生命周期(look 默认砖色)
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
Engine::Fracture::Burst m_Burst;
};
} // namespace