1. 项目概述与核心思路最近在重温一些经典物理游戏比如《愤怒的小鸟》发现里面那个清晰、流畅的抛物线轨迹提示对于游戏手感和策略判断的帮助太大了。很多刚接触Unity物理交互开发的朋友可能都想过自己实现一个类似的轨迹预测系统。今天我就来拆解一下如何用Unity内置的LineRenderer组件结合基础的物理运动学公式打造一个高还原度、可高度定制的抛物线轨迹预测器。这个方案不依赖复杂的物理模拟计算开销小非常适合移动端或需要实时显示大量轨迹的场景。简单来说我们要实现的功能是给定一个初始发射点、一个初始速度包括大小和方向然后实时计算并绘制出物体在此后一段时间内的运动轨迹。这听起来像是物理引擎的活儿但用LineRenderer来画线用我们自己写的公式来算点能获得更高的可控性和性能。无论是做弹弓类游戏、炮弹轨迹预测还是任何需要抛物线预览的功能这套思路都能直接套用。2. 核心原理从物理公式到屏幕坐标在动手写代码之前我们必须把背后的物理原理吃透。这里我们忽略空气阻力只考虑匀加速运动最典型的就是重力作用下的抛体运动。2.1 抛物线运动分解一个物体的抛物线运动可以分解为水平和垂直两个方向上的独立运动水平方向 (X轴)匀速直线运动。因为理想情况下忽略空气阻力水平方向不受力速度保持不变。公式x v0x * tv0x是初始速度的水平分量。垂直方向 (Y轴)匀加速直线运动。主要受重力加速度影响。公式y v0y * t 0.5 * a * t * tv0y是初始速度的垂直分量。a是加速度在这里通常是重力加速度g方向向下所以通常取负值如 -9.8。初始速度分解是关键一步。我们通常获得的初始速度是一个向量initialVelocity它包含了大小和方向例如从鼠标拖拽力度和方向计算得出。我们需要把它分解v0x initialVelocity.xv0y initialVelocity.y在3D空间中如果发射平面是X-Z平面那么轨迹计算主要在X和Y轴上进行Z轴可以视为另一个水平方向处理方式同X轴。2.2 LineRenderer 的作用与参数理解LineRenderer是Unity用来绘制3D空间中的连续线段的神器。你可以把它想象成一支虚拟的画笔我们只需要告诉它一系列空间中的点positions它就会把这些点按顺序用线连接起来。几个关键参数决定了轨迹的“外观”Positions (Vector3[])这是核心数据就是我们计算出的抛物线上一系列采样点的世界坐标。Width线的宽度。可以设置起始宽度和结束宽度用来实现轨迹头粗尾细的效果增强视觉引导。Material线的材质。用一个自发光、半透明的材质可以让轨迹看起来更醒目、更有“能量感”。Color Gradient颜色渐变。可以设置轨迹从起点到终点的颜色变化例如从明亮的红色高能渐变为淡蓝色或透明能量衰减。Num Cap Vertices / Num Corner Vertices控制线端点和拐角处的圆滑度值越大越圆滑但顶点数越多。我们的工作流就是根据物理公式按固定时间间隔计算出未来一系列时间点的位置坐标然后将这个坐标数组赋值给LineRenderer的positions属性。3. 完整代码实现与逐行解析理论清晰后我们开始实战。我会创建一个名为TrajectoryPredictor的C#脚本并附上详细注释。using UnityEngine; public class TrajectoryPredictor : MonoBehaviour { // 公开变量方便在Inspector中调试 [Header(轨迹渲染设置)] public LineRenderer lineRenderer; // 拖拽赋值 [Range(10, 100)] public int resolution 50; // 轨迹采样点数量影响平滑度 public float simulationTime 2f; // 预测的总时间秒 [Header(物理参数)] public Vector3 startPosition; // 发射起始点 public Vector3 initialVelocity; // 初始速度向量 public Vector3 gravity new Vector3(0, -9.81f, 0); // 重力加速度默认为地球重力 void Start() { // 如果未手动赋值LineRenderer尝试从当前物体上获取 if (lineRenderer null) { lineRenderer GetComponentLineRenderer(); } // 如果还没有就动态创建一个适用于快速原型 if (lineRenderer null) { GameObject lineObj new GameObject(TrajectoryLine); lineObj.transform.SetParent(this.transform); lineRenderer lineObj.AddComponentLineRenderer(); // 给一个默认的简单材质白色 lineRenderer.material new Material(Shader.Find(Sprites/Default)); lineRenderer.startColor Color.red; lineRenderer.endColor new Color(1, 0, 0, 0.3f); lineRenderer.startWidth 0.2f; lineRenderer.endWidth 0.05f; } } void Update() { // 每一帧都根据当前参数更新轨迹 UpdateTrajectory(); } /// summary /// 核心方法根据当前参数计算并更新LineRenderer的轨迹点 /// /summary public void UpdateTrajectory() { if (lineRenderer null) return; // 1. 创建存储位置点的数组 Vector3[] positions new Vector3[resolution]; // 2. 计算每个采样点的位置 for (int i 0; i resolution; i) { // 计算当前采样点对应的时间从0到simulationTime均匀分布 float t (float)i / (resolution - 1) * simulationTime; // 应用抛物线运动公式S V0 * t 0.5 * a * t^2 // 位移 初始速度 * 时间 0.5 * 加速度 * 时间的平方 Vector3 displacement initialVelocity * t 0.5f * gravity * t * t; // 最终位置 起始位置 位移 positions[i] startPosition displacement; } // 3. 将计算好的点数组设置给LineRenderer lineRenderer.positionCount resolution; // 关键必须先设置点的数量 lineRenderer.SetPositions(positions); } /// summary /// 外部调用更新发射参数例如在拖动弹弓时调用 /// /summary /// param namenewStartPos新的起始位置/param /// param namenewVelocity新的初始速度/param public void UpdateTrajectoryParameters(Vector3 newStartPos, Vector3 newVelocity) { startPosition newStartPos; initialVelocity newVelocity; // UpdateTrajectory()会在下一帧由Update自动调用 } /// summary /// 显示或隐藏轨迹线 /// /summary public void ToggleTrajectory(bool isVisible) { if (lineRenderer ! null) { lineRenderer.enabled isVisible; } } }代码关键点解析resolution分辨率这个值决定了轨迹由多少个线段连接而成。值越大曲线越平滑但计算量也轻微增加。对于手机游戏50-70个点通常能在效果和性能间取得良好平衡。simulationTime模拟时间这决定了我们“预测”多远的距离。设置得太短轨迹可能还没落地就消失了设置得太长轨迹可能会无限延伸直到穿出地面。在实际游戏中这个值可能需要动态计算比如预测到与地面的碰撞点为止。位移计算displacement这是整个计算的核心。initialVelocity * t计算了匀速运动部分0.5f * gravity * t * t计算了匀加速运动部分。注意gravity是向量通常为(0, -9.81, 0)表示向下的加速度。lineRenderer.positionCount在给LineRenderer赋值新的点数组positions之前必须先设置positionCount属性告诉它这次要渲染多少个点。这是一个非常常见的遗漏点忘记设置会导致轨迹不显示或显示不全。UpdateTrajectoryParameters方法这是为游戏逻辑准备的接口。比如在你的弹弓脚本中当玩家拖动小鸟时实时计算出一个力度和方向向量作为newVelocity然后调用这个方法轨迹就会实时更新。4. 在《愤怒的小鸟》式场景中的实战集成现在我们有了轨迹预测器如何把它集成到一个具体的发射场景中呢我们来模拟一个经典的弹弓发射逻辑。4.1 搭建场景与发射逻辑场景搭建创建一个平面作为地面一个球体作为“小鸟”一个用两个柱子和一个弹性带可以用胶囊体或弹簧关节模拟组成的弹弓。创建发射控制器新建一个BirdLauncher脚本挂在小鸟或一个空物体上。using UnityEngine; public class BirdLauncher : MonoBehaviour { public GameObject bird; // 小鸟物体 public TrajectoryPredictor trajectoryPredictor; // 轨迹预测器脚本 public float maxDragDistance 2.0f; // 最大拖拽距离控制力度上限 public float launchPowerMultiplier 10.0f; // 力度乘数 private Vector3 _birdStartPos; private bool _isDragging false; private Camera _mainCamera; void Start() { _birdStartPos bird.transform.position; _mainCamera Camera.main; if (trajectoryPredictor ! null) { trajectoryPredictor.startPosition _birdStartPos; trajectoryPredictor.ToggleTrajectory(false); // 初始隐藏轨迹 } } void Update() { if (Input.GetMouseButtonDown(0)) { // 简单射线检测判断是否点中了小鸟 Ray ray _mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit) hit.collider.gameObject bird) { _isDragging true; if (trajectoryPredictor ! null) { trajectoryPredictor.ToggleTrajectory(true); } } } if (_isDragging Input.GetMouseButton(0)) { // 1. 将鼠标屏幕坐标转换为世界坐标在弹弓所在的平面上 Vector3 mousePos Input.mousePosition; mousePos.z _mainCamera.transform.position.y - _birdStartPos.y; // 粗略计算深度 Vector3 worldMousePos _mainCamera.ScreenToWorldPoint(mousePos); worldMousePos.y _birdStartPos.y; // 锁定Y轴在2D平面内拖拽 // 2. 计算拖拽偏移向量并限制最大距离 Vector3 dragOffset _birdStartPos - worldMousePos; dragOffset Vector3.ClampMagnitude(dragOffset, maxDragDistance); // 3. 更新小鸟的临时位置向后拉 bird.transform.position _birdStartPos - dragOffset; // 4. 计算并更新轨迹预测 // 发射速度与拖拽方向相反向后拉得越远向前发射越快大小与拖拽距离成正比 if (trajectoryPredictor ! null) { Vector3 launchVelocity dragOffset * launchPowerMultiplier; trajectoryPredictor.UpdateTrajectoryParameters(_birdStartPos, launchVelocity); } } if (_isDragging Input.GetMouseButtonUp(0)) { _isDragging false; // 实际发射小鸟 LaunchBird(); if (trajectoryPredictor ! null) { trajectoryPredictor.ToggleTrajectory(false); // 发射后隐藏轨迹 } } } void LaunchBird() { // 给小鸟添加刚体并施加力/速度 Rigidbody rb bird.GetComponentRigidbody(); if (rb null) rb bird.AddComponentRigidbody(); rb.isKinematic false; // 启用物理模拟 Vector3 finalDragOffset _birdStartPos - bird.transform.position; Vector3 launchVelocity finalDragOffset * launchPowerMultiplier; rb.velocity launchVelocity; // 可以在这里添加发射音效、粒子效果等 Debug.Log(发射速度 launchVelocity); } }4.2 轨迹视觉美化技巧基础的白色线条太朴素了。要让轨迹看起来像《愤怒的小鸟》里那样有质感需要对LineRenderer进行深度配置。创建专用材质在Assets中创建MaterialShader可以选择Particles/Standard Unlit或Mobile/Particles/Alpha Blended。这样更容易控制颜色和透明度。配置颜色渐变在LineRenderer组件的Color区域使用渐变编辑器。起点颜色设置为不透明度高的亮色如白色或亮黄色RGB(255, 255, 200)。中间点可以添加一个关键点颜色稍暗。终点颜色设置为同色系但透明度为0的颜色实现淡出效果。调整宽度曲线在Width区域使用曲线编辑器。将起点的宽度值设得高一些如0.25。将终点的宽度值设得很低如0.02。这样轨迹就会呈现出“头粗尾细”的生动效果仿佛能量在衰减。添加纹理进阶可以给材质赋一个横向渐变的纹理图并设置Texture Mode为Tile让纹理沿着轨迹线分布可以做出更复杂的能量流动效果。注意LineRenderer的宽度受透视影响。在正交相机2D游戏常用下看起来正常在透视相机下远离相机的部分会显得更细。如果追求绝对均匀的视觉宽度可能需要将LineRenderer的useWorldSpace设置为false并在本地空间进行计算或者使用Billboard技术但这会复杂很多。对于大多数情况默认的透视效果是可以接受的。5. 性能优化与高级功能扩展基础功能完成后我们得考虑它在真实项目中的表现和可能性。5.1 性能优化要点控制分辨率与频率resolution是性能的关键。在移动设备上可以尝试动态调整当玩家拖拽时使用高分辨率如50点获得平滑预览发射后或轨迹静止时降低分辨率如20点。另外不需要每帧都更新轨迹。可以在BirdLauncher的拖拽逻辑中只有当拖拽偏移量变化超过某个阈值时才调用UpdateTrajectoryParameters。对象池管理如果你的游戏需要同时显示多条轨迹比如多人对战或连发武器频繁创建和销毁GameObject和LineRenderer会产生GC垃圾回收压力。应该使用对象池来管理TrajectoryPredictor对象。简化计算我们的公式已经非常简洁。如果项目对性能极端敏感可以预先计算0.5f * gravity这个常量在循环中使用避免重复乘法。5.2 高级功能扩展思路碰撞检测与轨迹截断目前的轨迹是“无限”的。更真实的做法是进行射线检测预测轨迹与场景中碰撞体的交点并在交点处停止绘制。可以在UpdateTrajectory方法的循环中每计算一个点就从上一点到当前点做一次Raycast或SphereCast考虑物体体积如果检测到碰撞就设置该点为终点并终止循环。动态重力与风力我们的公式假设重力恒定且方向向下。你可以扩展它引入一个externalForce向量在位移公式中叠加0.5f * (gravity externalForce) * t * t。这样就能模拟风、磁力等效果。externalForce甚至可以随时间变化实现更复杂的物理场。轨迹样式切换通过脚本动态切换LineRenderer的材质、颜色渐变或宽度曲线可以表示不同的发射状态。例如正常状态用蓝色虚线蓄力最大时用红色实线碰到障碍物可能变为闪烁的警告色。贝塞尔曲线平滑对于需要更艺术化、非纯粹物理轨迹的情况比如魔法飞弹可以用计算出的物理轨迹点作为控制点再用贝塞尔曲线算法生成更平滑的路径然后交给LineRenderer渲染。这能让轨迹运动看起来更优美、更有弹性。6. 常见问题与调试技巧实录在实际开发中你肯定会遇到轨迹显示不正常的情况。这里记录几个我踩过的坑和解决方法。问题1轨迹线完全不显示。检查清单LineRenderer组件是否启用检查Inspector面板或确认lineRenderer.enabled为true。positionCount设置了吗这是最容易被忘记的一步必须在SetPositions之前设置lineRenderer.positionCount positions.Length;。材质/Shader兼容吗如果材质丢失或Shader不支持线会是粉色。确保材质球有效并且其Shader适合LineRenderer如Particles/Standard Unlit。坐标值是否异常打印几个计算出的positions坐标看看是不是有NaN非数字或者数值巨大例如因为initialVelocity或simulationTime设置得过大导致轨迹画在了视野之外。问题2轨迹线显示但位置不对或者方向反了。原因这几乎总是初始速度向量initialVelocity的方向或大小出了问题。调试在Scene视图中用Debug.DrawRay(startPosition, initialVelocity, Color.red)画一下这个速度向量。看看它的方向和长度是否符合你的预期比如拖拽弹弓时速度方向应该是向前发射而不是向后。检查你的拖拽逻辑中dragOffset的计算_birdStartPos - worldMousePos是否正确。鼠标位置到起始点的向量反向才是发射方向。检查launchPowerMultiplier的值是否合理。可以先设为一个较小的值如5观察轨迹长度再慢慢调整。问题3轨迹线看起来是折线不光滑。原因resolution采样点数量设置得太低。解决增加resolution的值比如从20提高到50。但要注意性能平衡。如果提高后仍不光滑可能是LineRenderer的Num Corner Vertices值太低尝试将其从默认的0提高到5或10。问题4在透视相机下轨迹线宽度不均匀远处很细。原因这是LineRenderer在透视投影下的正常现象宽度是按世界空间单位定义的。解决方法A简单调整LineRenderer的Width Multiplier曲线让起始宽度更大或者接受这种透视效果。方法B复杂但精确将LineRenderer的useWorldSpace设为false然后在计算positions时将世界坐标转换为该LineRenderer物体本地坐标系下的坐标。这通常需要将轨迹预测器脚本挂在一个始终面向相机的画布物体下。问题5轨迹更新卡顿。原因UpdateTrajectory方法每帧计算50个点虽然计算量不大但如果同时有大量物体需要预测或者在低端设备上仍可能造成卡顿。优化如5.1节所述降低更新频率。不要每帧调用而是在拖拽参数变化时调用。降低分辨率在移动平台上尝试用30个点。使用协程进行分帧计算但这对实时拖拽预览不友好。对于固定轨迹如关卡设计时摆放的炮台可以预计算并缓存位置数组无需每帧重算。最后我个人在项目中的体会是这个抛物线轨迹系统是一个“性价比”极高的功能。它用相对简单的代码极大地提升了游戏的交互反馈和策略深度。最关键的是理解初始速度向量和重力加速度向量这两个核心参数它们直接决定了轨迹的形态。多花时间在Scene视图里用Gizmos或Debug绘图可视化这些向量比在代码里埋头苦想有效得多。当你看到那条优美的弧线随着你的鼠标拖拽而实时变化时那种成就感就是游戏开发最纯粹的乐趣之一。