阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 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:
@@ -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
@@ -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 形状)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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__)
|
||||
@@ -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
@@ -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>();
|
||||
|
||||
@@ -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
@@ -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,调试图)
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user