Unity物理引擎实现日月地系统模拟:从万有引力到轨道可视化
2026/7/27 5:00:09 网站建设 项目流程

1. 项目概述与核心价值

最近在捣鼓Unity的物理引擎,想找个有点意思又不太复杂的项目练手,就想到了模拟日月地系统。这玩意儿听起来挺唬人,又是太阳又是月亮,感觉是天文模拟级别的。但实际做下来,你会发现,用Unity内置的物理引擎,配合一些简单的脚本和数学知识,就能搭出一个有模有样的动态系统。这不仅仅是做个动画,而是让三个球体(太阳、地球、月亮)在万有引力的作用下,按照我们熟知的规律动起来。对于想深入理解Unity物理引擎(特别是刚体、力、坐标系统)和基础天体力学模拟的开发者来说,这是个绝佳的练手项目。它能帮你把书本上的牛顿定律,变成屏幕上看得见、摸得着的交互式体验。

这个项目的核心,说白了就是“用游戏引擎的物理模块,去模拟现实世界的物理规律”。Unity的物理引擎(PhysX)默认是为游戏场景服务的,处理碰撞、刚体运动这些很在行,但它并没有内置“万有引力”这种特定公式。我们的任务,就是自己写脚本,为每个天体计算并施加符合万有引力定律的力。最终,你会得到一个地球绕着太阳转,月亮又绕着地球转的简化版太阳系模型。虽然极度简化(忽略了行星、轨道倾角、相对论效应等等),但它抓住了天体运行最核心的动力学原理。

2. 核心思路与物理模型设计

2.1 为什么选择Unity物理引擎而非纯数学模拟?

刚接触这个想法时,你可能会想:我直接用数学公式计算每个时刻天体的位置,然后用Transform.position去更新不就行了?为什么非要引入物理引擎和刚体(Rigidbody)?

这里的关键区别在于“过程”与“结果”。纯数学模拟(开普勒轨道方程)是直接给出结果——在某个时间点,天体应该在哪里。而基于物理引擎的模拟,是定义规则(力),然后让引擎去积分、计算每一步的加速度、速度和位移,最终“演化”出结果。后者有两大优势:

  1. 更强的扩展性和交互性:一旦系统建立,你可以轻易地加入第四个、第五个天体(比如火星、木星),或者用鼠标给地球一个推力,观察其轨道如何被扰动。物理引擎会自动处理这些多体相互作用和外部干扰,而纯数学模拟要修改核心公式,复杂得多。
  2. 更符合学习直觉:通过编写“施加力”的脚本,你能更直观地理解F=ma和万有引力定律是如何一步步驱动物体运动的。这对于理解物理引擎的工作机制(FixedUpdate循环、力的累积、积分运算)有巨大帮助。

所以,我们选择基于物理引擎的方案,不是为了追求天文级的精度,而是为了搭建一个可交互、易扩展的物理学习沙盒

2.2 简化物理模型的关键假设

真实的日月地系统极其复杂。为了让项目可控且重点突出,我们必须做出简化:

  1. 质点模型:将太阳、地球、月亮视为只有质量、没有大小的质点。这样我们就可以忽略它们的自转和形状对引力的影响,只关心质心之间的作用力。在Unity中,我们用球体(Sphere)来视觉化表示。
  2. 二体问题分解:真正的三体问题没有解析解。我们采用一个巧妙的近似:将地月系统视为一个整体,与太阳构成一个二体问题;同时,地月之间再构成另一个独立的二体问题。在计算力时,太阳对地球和月亮分别施加引力,地球也对月亮施加引力(反之亦然,但月亮对地球的引力影响较小,有时可忽略以简化)。这种“层级化”处理在精度要求不高时非常有效。
  3. 固定太阳:为了进一步简化,并让地球的轨道更稳定,我们通常将太阳设置为位置固定(Rigidbody.isKinematic = true)且质量极大。这样,太阳几乎不受地球和月亮引力的影响而移动,成为了系统的引力中心。这类似于在物理学中选取了太阳参考系。
  4. 理想化轨道:忽略所有轨道偏心率(设为圆形轨道)、轨道倾角(所有运动在同一平面)。这会让运动看起来是完美的同心圆,但足以阐明原理。

基于这些假设,我们的核心任务就清晰了:为地球和月亮这两个非固定天体添加刚体组件,并编写脚本,在每一物理帧中,根据万有引力公式计算它们受到的合力,并以力的形式施加给刚体。

2.3 场景与对象基础设置

在动手写代码前,需要在Unity编辑器中搭建好场景框架:

  1. 创建天体:创建三个球体(GameObject -> 3D Object -> Sphere),分别重命名为SunEarthMoon。调整它们的Scale以区分大小,例如Sun(5,5,5), Earth(1,1,1), Moon(0.3,0.3,0.3)。
  2. 设置材质与颜色:为它们赋予不同的材质和颜色以便区分。太阳可以用自发光的Emission材质,地球用蓝色,月亮用灰色。
  3. 布置初始位置:将太阳放在场景中心(0,0,0)。将地球放在太阳右侧一定距离,例如(10, 0, 0)。将月亮放在地球右侧更近的距离,例如(12, 0, 0)。这样它们一开始就在一条直线上。
  4. 创建空物体作为轨道平面:建议创建一个空的GameObject(如命名为SolarSystem),将三个天体都拖拽为其子物体。这样做有利于整体移动、旋转或管理。

注意:初始位置的设置会影响天体所需的初始速度。如果只给引力而不给初速度,天体将直接坠向太阳。我们后续需要通过脚本或估算来赋予一个合适的切向初速度。

3. 核心组件与脚本实现详解

3.1 刚体组件配置:运动的基础

EarthMoon对象添加Rigidbody组件。这是物理引擎接管其运动的前提。

  • Mass(质量):这是关键参数。我们需要设定一个符合相对比例的质量。例如,设太阳质量为单位1,地球质量约为0.000003(这是一个非常小的值,以确保太阳几乎不动)。在刚体属性中,我们可以直接设置mass。例如,Earth.mass = 1.0fMoon.mass = 0.012f(约为地球质量的1.2%)。太阳的刚体是运动学(Kinematic)的,其mass属性不影响动力学计算,但我们的引力计算脚本会用到它的质量值。
  • Drag(阻力)必须设置为0。太空是真空,没有空气阻力。任何非零的Drag值都会导致轨道能量衰减,最终使行星螺旋坠入太阳。
  • Use Gravity必须取消勾选。我们要禁用Unity默认的全局向下(Y轴)的重力,完全由我们的自定义万有引力脚本接管。
  • Is Kinematic:对于Sun,我们需要勾选此项,使其位置和旋转不受物理力影响,由我们完全控制(我们将其固定在原点)。对于EarthMoon,保持不勾选。

3.2 万有引力脚本的核心算法

创建一个C#脚本,命名为CelestialBodyGravity。这个脚本将挂载到每一个需要受引力影响的天体上(即EarthMoon)。

脚本的核心逻辑在FixedUpdate()中执行,因为物理计算需要固定的时间步长。

using UnityEngine; public class CelestialBodyGravity : MonoBehaviour { private Rigidbody rb; // 引力常数G。在真实世界中是6.674e-11,但在我们的模拟尺度下,需要放大很多倍才能看到明显效果。 public float gravitationalConstant = 100f; void Start() { rb = GetComponent<Rigidbody>(); if (rb == null) { Debug.LogError("Rigidbody component not found on " + gameObject.name); } } void FixedUpdate() { ApplyGravity(); } void ApplyGravity() { // 找到场景中所有带有Rigidbody的天体(包括自己) CelestialBodyGravity[] allBodies = FindObjectsOfType<CelestialBodyGravity>(); foreach (var otherBody in allBodies) { // 排除自身对自己的引力 if (otherBody == this) continue; Rigidbody otherRb = otherBody.GetComponent<Rigidbody>(); if (otherRb == null) continue; // 计算方向向量:从他物体指向本物体 Vector3 direction = otherRb.position - rb.position; float distance = direction.magnitude; // 避免距离过近导致力趋于无穷大,引发物理引擎不稳定 if (distance < 0.1f) continue; // 计算引力大小:F = G * (m1 * m2) / r^2 float forceMagnitude = gravitationalConstant * (rb.mass * otherRb.mass) / Mathf.Pow(distance, 2); // 将大小与方向结合,得到力矢量 Vector3 force = direction.normalized * forceMagnitude; // 将力施加到本物体的刚体上 rb.AddForce(force); } } }

代码关键点解析:

  1. FindObjectsOfType的使用:在FixedUpdate中每帧查找所有天体在性能上并非最佳,但对于只有3-5个对象的学习项目完全可接受。在更复杂的模拟中,应使用对象池或管理器来传递引用。
  2. 距离检查if (distance < 0.1f) continue;这行代码至关重要。当两个物体因速度过快或初始位置设置不当而非常接近时,根据公式,引力会变得异常巨大,导致刚体获得极大的速度并被“弹飞”,模拟立刻崩溃。这个最小距离阈值是一个安全阀。
  3. 引力常数G:真实世界的G值太小,在游戏尺度下(距离单位是米,质量单位是千克)产生的力微乎其微。我们必须将其放大很多个数量级(例如100, 1000,甚至更高),才能看到明显的轨道运动。这个值需要与你的天体质量、初始距离配合调试。
  4. 力的施加:使用AddForce,这是最符合物理直觉的方式。物理引擎会在当前帧累积所有力,然后在物理更新中计算加速度和速度变化。

实操心得:gravitationalConstant、天体mass和初始距离是三个联动的“调参旋钮”。如果行星飞走了,可能是G太大或初速度太小;如果转得太慢,可能是G太小或初速度太大。调试时,最好从一个天体(如地月系统)开始,稳定后再加入太阳。

3.3 赋予初始轨道速度:让天体“转起来”

只有引力,天体会被直线吸向中心天体。要形成轨道,必须赋予一个适当的切向初速度。对于理想的圆轨道,这个速度可以通过向心力公式推导:F引力 = F向心=>G * M * m / r^2 = m * v^2 / r,简化得v = sqrt(G * M / r)。其中M是中心天体质量,r是轨道半径。

我们可以写一个简单的脚本,在Start()中为地球和月亮赋予这个计算出的初速度。

void Start() { rb = GetComponent<Rigidbody>(); // 假设太阳是中心天体,其质量为 sunMass GameObject sun = GameObject.Find("Sun"); if (sun != null) { Rigidbody sunRb = sun.GetComponent<Rigidbody>(); // 计算从本物体指向太阳的方向 Vector3 directionToSun = sunRb.position - rb.position; // 计算轨道半径 float orbitRadius = directionToSun.magnitude; // 计算所需的圆轨道速度大小 // 注意:这里假设太阳质量极大且固定,其刚体可能为Kinematic,mass属性可能为1或其他值。 // 我们需要一个公开变量来设置太阳的“引力质量”,它可能不同于刚体的物理质量。 float sunGravitationalMass = 10000f; // 这是一个示例值,需要调试 float orbitSpeed = Mathf.Sqrt(gravitationalConstant * sunGravitationalMass / orbitRadius); // 速度方向垂直于指向太阳的半径方向,即切向方向。 // 在二维平面(XZ平面)上,可以通过叉乘得到垂直向量。 Vector3 tangentDirection = Vector3.Cross(directionToSun.normalized, Vector3.up).normalized; // 设置刚体的初始速度 rb.velocity = tangentDirection * orbitSpeed; } }

为月亮设置速度的复杂性:月亮的运动是围绕地球的。上述脚本假设中心是太阳,这不适用于月亮。更准确的做法是,月亮的初速度应该基于地球(作为其中心天体)来计算。这意味着你需要为脚本指定它的“主星”(Primary Body)。我们可以修改脚本,增加一个public Transform primaryBody;字段,在Inspector中手动将地球拖拽赋值给月亮的这个字段。然后在计算速度时,使用primaryBody的位置和质量。

注意事项:这种赋予完美圆轨道初速度的方法,在只有两个天体且不受其他扰动时,理论上可以形成稳定圆轨道。但在三体系统中,由于相互引力扰动,轨道仍然会慢慢演化,可能变得不稳定或椭圆化,这反而是模拟有趣的一部分。

4. 系统调试、优化与可视化增强

4.1 参数调试:让系统稳定运行

将脚本挂载好,参数设好,点击运行,很可能看到的不是优美的轨道,而是地球像炮弹一样被甩出太阳系,或者笔直地撞向太阳。别慌,这是调试环节的开始。

  1. 调整引力常数G和质量:这是最关键的步骤。建议的调试流程:
    • 先屏蔽太阳对月亮的引力(可以在脚本中加个判断,或者先不把脚本挂给月亮),只做地月系统模拟。将地球设为Kinematic(固定),只让月亮动。调整地球的“引力质量”和G值,直到月亮能大致绕地球做圆周运动。
    • 稳定地月系统后,激活太阳。将地球的脚本也启用。此时地球会受到太阳引力。你需要调整太阳的“引力质量”(一个用于计算的变量,可能远大于其刚体mass值)和G值,使地球能绕太阳旋转。
    • 同时考虑地月系统:最后,解除地球的Kinematic,让地月系统作为一个整体在太阳引力下运动,同时月亮还受地球引力影响。这是最复杂的阶段,可能需要微调所有参数。
  2. 使用Time Scale:在Game视图,可以调整Time.timeScale(如设为0.1, 0.5),让运动变慢,方便观察轨道形状和调试。
  3. 绘制调试轨迹:在Update()中使用Debug.DrawLine绘制上一帧和这一帧的位置连线,可以直观看到运动轨迹。但这只是临时调试,线条不会持久。

4.2 轨道可视化:让运动轨迹清晰可见

调试用的Debug.DrawLine不够美观。我们可以创建更美观的永久性轨迹。

方法一:使用LineRenderer组件

  1. 为每个运动天体(地球、月亮)创建一个子空物体,命名为Trail
  2. Trail添加LineRenderer组件。
  3. 编写脚本,在LateUpdate()中,将天体过去若干帧(例如500帧)的位置记录到一个列表(List<Vector3>)中,并实时更新LineRendererpositionCountpositions
  4. 可以设置LineRenderer的材质、颜色、宽度,让轨迹看起来像发光的弧线。

方法二:使用粒子系统(ParticleSystem)创建一个非常简单的粒子系统,附着在天体上:

  • 关闭所有速度、力模块。
  • 开启Emission,但速率设为0。
  • Renderer模块,使用一个小的点状材质。
  • 编写脚本,在Update()中,每隔几帧(如每0.1秒)通过Emit方法在当前天体位置发射一个粒子。粒子寿命(startLifetime)设置得较长(如30秒),这样就会形成一条由点组成的轨迹。调整粒子大小和颜色,效果非常科幻。

实操心得:轨迹点列表需要定期清理,否则列表会无限增长,导致性能下降。可以设置一个最大点数,当超过时移除最旧的点。使用LineRenderer时,更新大量顶点对性能有影响,500-1000个点通常是平衡点。

4.3 性能考量与脚本优化

当前的简单实现有优化空间:

  1. 避免每帧FindObjectsOfType:在Start()中一次性找到所有天体引用并存入数组,在FixedUpdate中直接遍历这个数组。
  2. 距离计算的平方优化:万有引力公式中需要计算距离的平方。我们已经用了Mathf.Pow(distance, 2)。另一种更高效的方法是先计算方向向量direction,然后使用Vector3.sqrMagnitude获得距离平方,避免了一次开方运算。但注意,归一化direction.normalized需要实际距离,所以这里需要权衡。如果为了极致的性能,可以自己用sqrMagnitude计算距离平方用于力大小计算,同时用sqrt(sqrMagnitude)得到实际距离用于归一化(当距离很小时仍需避免)。
  3. 为固定天体(太阳)做特殊处理:在引力计算循环中,如果检测到对方天体是Kinematic的(太阳),可以跳过对其施加力的计算(因为太阳不受力),但太阳的质量仍然要用于计算它对其他天体的引力。
// 优化示例片段 void ApplyGravityOptimized(CelestialBodyGravity[] allBodies) { foreach (var otherBody in allBodies) { if (otherBody == this) continue; Rigidbody otherRb = otherBody.rb; // 假设在Start中已经缓存了其他天体的rb // 如果对方是运动学刚体,它不受力,但我们仍要计算它对我们的引力 // 如果对方也是运动学刚体,且我们也是,则完全跳过(两个固定天体之间无动力学) if (otherRb.isKinematic && rb.isKinematic) continue; Vector3 direction = otherRb.position - rb.position; float distanceSqr = direction.sqrMagnitude; if (distanceSqr < 0.01f) continue; // 最小距离平方阈值 float distance = Mathf.Sqrt(distanceSqr); float forceMagnitude = gravitationalConstant * (rb.mass * otherRb.mass) / distanceSqr; // 使用平方 Vector3 force = (direction / distance) * forceMagnitude; // 用 direction/distance 代替 normalized // 只对非运动学刚体施加力 if (!rb.isKinematic) { rb.AddForce(force); } } }

5. 常见问题、排查技巧与扩展思路

5.1 典型问题速查表

问题现象可能原因排查与解决思路
天体直接坠向中心,没有轨道未赋予初始切向速度。检查赋予初速度的脚本是否执行,速度方向是否正确(应垂直于指向中心天体的半径)。
天体沿直线飞速离开,不回转初始速度过大,或引力常数G太小/中心质量太小。降低初始速度,或增大G值/中心天体的“引力质量”。
轨道不稳定,逐渐螺旋式坠入或飞出1. 物理步长(Fixed Timestep)不合适。
2. 能量不守恒(如存在非零Drag)。
3. 数值精度误差累积。
1. 尝试减小Edit -> Project Settings -> Time -> Fixed Timestep(如从0.02改为0.01)。
2. 确保所有运动天体的DragAngular Drag为0。
3. 使用Rigidbody.interpolation可能改善视觉平滑度,但对物理精度无帮助。对于长期模拟,数值误差不可避免。
多个天体时,运动极其混乱、爆炸1. 初始位置太近,导致引力过大。
2. 引力常数G设置得过大。
3. 未设置最小距离保护。
1. 增大初始天体间距。
2. 大幅调小G值,缓慢增加直到运动可见。
3. 在引力计算中加入最小距离判断(如if(distance < minDistance) continue;)。
月亮不绕地球转,或很快被太阳吸走月亮受到的太阳引力远大于地球引力。增大地球相对于太阳的“引力质量”比例。在现实中,太阳质量是地球的33万倍,但在我们的简化模拟中,可能需要大幅缩小这个比例(比如太阳质量是地球的100或1000倍),才能让地月系统保持稳定。或者,减小地月距离。
轨迹可视化(LineRenderer)不连续或闪烁点在Update中记录,但LineRendererLateUpdate中更新,可能不同步。或列表更新逻辑有误。确保轨迹点列表的添加和LineRenderer的位置更新在同一个方法周期内(如同在LateUpdate中)。检查是否在清空列表时误操作了。

5.2 扩展项目的高级思路

当基础系统运行稳定后,可以尝试以下扩展,让模拟更丰富:

  1. 椭圆轨道与开普勒定律:不赋予完美的圆轨道速度,而是给一个略大或略小的切向速度。天体将在引力作用下运行椭圆轨道。你可以尝试测量“相同时问内扫过的面积”来验证开普勒第二定律。
  2. 加入更多行星:轻松加入火星、金星等。只需复制地球的预制体,修改初始位置、质量、初速度。观察多体引力下的复杂相互作用(虽然不精确,但很有趣)。
  3. 用户交互
    • 引力弹弓:允许用户用鼠标拖动一个天体并赋予一个初速度,然后观察它如何被大行星的引力场加速或偏转。
    • 动态修改质量:在运行时通过UI滑块实时修改太阳或地球的质量,观察轨道瞬间变化的动态效果。
  4. 摄像机与视觉特效
    • 智能跟随摄像机:写一个脚本让摄像机平滑跟随地球,并始终将太阳保持在视野某处。
    • 星空背景:使用粒子系统生成静态或缓慢旋转的星空背景。
    • 光晕与镜头耀斑:为太阳添加Lens FlareBloom后处理效果,增强视觉表现力。
  5. 从3D简化为2D:如果你的目标是专注于物理逻辑,可以将所有运动限制在XY平面(设置刚体的constraints冻结Z轴位置和旋转),并使用正交摄像机。这样调试轨道形状会更直观。

这个基于物理引擎的日月地系统实现,就像搭积木。核心的引力脚本和刚体设置是地基,参数调试是让积木站稳的微调,而轨迹可视化和扩展功能则是让作品变得生动有趣的装饰。整个过程下来,你对Unity物理引擎的RigidbodyAddForceFixedUpdate的理解,会比单纯看文档深刻得多。下次当你再遇到需要模拟自然力(如磁力、粒子间作用力)的场景时,这套“计算力-施加力”的思维模式就能直接派上用场了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询