本文档旨在构建一个通用、高性能、高扩展性的投射物系统理论框架。 在 Project Vampirefall 中,投射物(Projectiles)是塔防与战斗的核心交互载体。与 Hitscan(射线判定)不同,投射物拥有飞行时间 (Travel Time)、独立轨迹和物理交互。
投射物不应该是一个简单的 MonoBehaviour,它应包含两个分离的层:
ProjectileManager 统一驱动(ECS 或 Struct-based)。| 类型 | 描述 | 适用场景 | 数学模型 | | :— | :— | :— | :— | | 直射 (Linear) | 沿直线匀速/变速飞行。 | 箭矢、子弹、激光束。 | $P = P_0 + V \cdot t$ | | 抛射 (Lobbed) | 受重力影响,呈抛物线。 | 迫击炮、手雷、投石车。 | $y = v_{0y}t - \frac{1}{2}gt^2$ | | 追踪 (Homing) | 动态调整速度向量指向目标。 | 魔法飞弹、制导导弹。 | Steering Behavior (操纵行为) | | 贝塞尔 (Bezier) | 沿预计算的曲线飞行,无物理模拟。 | 华丽的技能弹道、回旋镖。 | Bezier Curve Interpolation | | 环绕 (Orbital) | 围绕宿主或定点旋转。 | 护盾球、环绕法球。 | Polar Coordinates (极坐标) | | 垂直发射 (Javelin) | 先垂直升空,再转为追踪。 | 标枪导弹 (Javelin)、天降正义。 | State Machine (Ascend -> Lock -> Homing) |
给定起点 $S$、终点 $E$ 和飞行时间 $T$(或高度 $H$),如何计算初速度 $V_0$?
公式推导 (基于时间 $T$):
代码片段:
public Vector3 CalculateLobVelocity(Vector3 start, Vector3 end, float time)
{
Vector3 distance = end - start;
Vector3 distanceXZ = distance;
distanceXZ.y = 0;
float sY = distance.y;
float sXZ = distanceXZ.magnitude;
float Vxz = sXZ / time;
float Vy = (sY / time) + (0.5f * Mathf.Abs(Physics.gravity.y) * time);
Vector3 result = distanceXZ.normalized;
result *= Vxz;
result.y = Vy;
return result;
}
简单的 LookAt 会导致导弹像苍蝇一样抽搐。优秀的追踪需要转弯速度限制 (Turn Rate Limit)。
实现步骤:
DesiredVelocity = (TargetPos - CurrentPos).normalized * SpeedSteering = DesiredVelocity - CurrentVelocitySteering = Vector3.ClampMagnitude(Steering, TurnRate * dt)CurrentVelocity += Steering为了让导弹平滑转向目标,我们应使用四元数 (Quaternion)。
Quaternion.Slerp(currentRot, targetRot, turnSpeed * dt)。适合平滑插值,但在大角度转向时可能不够直接。Quaternion.RotateTowards(currentRot, targetRot, maxDegreesDelta)。更精确地控制每帧最大转角,适合模拟导弹的机械限制。预瞄准理论 (Lead Aiming):
| 这转化为解方程:$ | (P_T - P_B) + V_T \cdot t | ^2 = (S_B \cdot t)^2$。 |
迭代求解 (Iterative Solver): 当目标不是匀速直线运动(例如在做圆周运动或变速运动),解析解变得极其复杂。此时应使用迭代法:
为了追求更真实或特殊的手感,我们需要引入空气动力学。
真空中的抛物线是完美的对称图形,但在游戏中这看起来可能很“飘”。
Pos += Vel * dt; Vel += Acc * dt; 简单但误差大,不推荐用于长距离高精度弹道。Pos += Vel * dt + 0.5 * Acc * dt * dt;
NewAcc = ComputeForces(Pos, Vel);
Vel += 0.5 * (Acc + NewAcc) * dt;
当投射物检测到碰撞后,不仅是造成伤害,还可以触发复杂的后续行为。
PierceCount 并继续飞行。List<int> hitInstanceIDs。击中时检查 ID,若已存在则跳过。OverlapSphere 寻找最近的、未被击中过的敌人作为下一个目标方向。这是一种两阶段弹道,常用于“攻顶”导弹。
逻辑流程:
Vector3.up * LaunchForce。TurnRate(或者直接 Slerp 旋转),让导弹头朝下对准目标。在高性能要求下(同屏 1000+ 弹幕),不能给每个子弹挂 Rigidbody + Collider。
Physics.Raycast(prevPos, (currPos - prevPos).normalized, dist)Physics.SphereCast。Physics.CapsuleCast。相比射线,它有一个“粗细”;相比球体,它能更好地模拟长条物体的体积,避免“箭身”穿过敌人却没判定的情况。虽然本项目偏单机,但若涉及联机,需理解:
不要让每个 Bullet 都有 Update()。
public struct ProjectileData {
public Vector3 position;
public Vector3 velocity;
public float gravityScale;
public int pierceCount;
public int targetMask;
// ...
}
public class ProjectileManager : MonoBehaviour {
private ProjectileData[] _projectiles; // 或 NativeArray
private int _activeCount;
void Update() {
// 1. 批量更新位置 (Simulate Physics)
for (int i = 0; i < _activeCount; i++) {
UpdateProjectile(ref _projectiles[i], Time.deltaTime);
}
// 2. 批量处理碰撞 (Collision Query)
// 3. 批量更新渲染实例 (GPU Instancing / Matrix List)
}
}
ProjectileData)GameObject 或 VFX)Clear())。OnTriggerEnter。