资讯动态

Unity3D太阳系模拟实战:自转公转与时间缩放控制

发布时间:2026/9/16 12:48:47 来源:尧图企业网站定制
简介面向Unity3D学习者和天文可视化爱好者的太阳系模拟交互项目演示八大行星围绕太阳的自转与公转运动。项目支持ASDW键调整视角、Q键加速旋转、E键减速旋转并配有操作说明按钮适合作为理解Unity3D中Transform组件、旋转速度控制、UI交互绑定的入门实践。压缩包约410.5MB文件清单暂未提供已有1009人学习/下载。通过该资源可掌握使用Transform.Rotate模拟自转、按开普勒定律变化公转速度的思路并了解如何在同一场景中组合3D模型、灯光与多个天体对象。对于希望用Unity制作动效演示或科普小工具的开发者这一案例提供了可直接运行参考的场景逻辑与关键代码视角能帮助快速上手天体运动的程序化表达。1. 为什么这个太阳系Demo值得拆开看用Unity3D做太阳系自转公转第一反应是往场景里拖八个球然后写一行transform.Rotate(Vector3.up * speed * Time.deltaTime)。这个做法确实能动但跑起来会发现要么水星转得比木星还快要么视角一晃就找不到地球要么按Q加速时所有行星一起抽风。这个项目值得拆解的地方在于它把“天体运动”拆成了三层独立逻辑——自转、公转、交互控制每层都可以单独调参而不互相干扰。对于刚接触Unity3D坐标变换和帧循环的开发者这是一个能看清Transform核心机制的完整案例对于已经有经验的从业者这篇博文会讲清楚轨道数据怎么组织、为什么不用物理引擎、以及开普勒第二定律在代码里能简化到什么程度。2. 场景搭建与轨道数据模型2.1 天体层级结构怎么摆才不会乱先想清楚层级关系。太阳作为场景根节点放在原点八颗行星作为太阳的子物体。这里有个容易踩的坑如果把行星放在太阳的子级下行星的Transform坐标是相对于太阳的公转只需要修改行星的局部位置或局部旋转如果所有天体都是平级公转就得每帧手动计算“太阳位置-行星位置”的方向向量多写代码不说Scalable性也差。我的做法是建立一个Sun空物体把太阳球体挂在其下然后创建八个子物体作为行星轨道挂点。每个行星轨道挂点只负责围绕Y轴旋转行星球体再作为轨道挂点的子物体负责自转。这样层级链路是Sun - OrbitalAnchor(水星) - Mercury(球体)公转旋转锚点自转旋转球体二者互不牵连。在Project窗口中创建Scripts文件夹先写一个数据类PlanetData.cs把每颗行星的轨道半径、公转周期、自转周期集中定义成静态数组。写死在场景里不是不行但后面想调参数要同时改八个物体维护成本高。集中管理后不管是用代码生成行星还是手动拖拽赋值数据来源都统一。// PlanetData.cs [System.Serializable] public class PlanetData { public string planetName; // 行星名称 public float orbitalRadius; // 轨道半径世界单位非真实比例 public float revolutionPeriod; // 公转周期秒 public float rotationPeriod; // 自转周期秒 } public static class SolarSystemData { public static PlanetData[] Planets new PlanetData[] { new PlanetData { planetName Mercury, orbitalRadius 10f, revolutionPeriod 88f, rotationPeriod 58f }, new PlanetData { planetName Venus, orbitalRadius 15f, revolutionPeriod 224f, rotationPeriod 243f }, new PlanetData { planetName Earth, orbitalRadius 20f, revolutionPeriod 365f, rotationPeriod 1f }, // ... 其余行星依此追加 }; }代码逻辑说明orbitalRadius是场景中轨道圈的半径数据采用“相对比例”而不是真实天文学尺度因为真实尺度下地球和太阳的距离在Unity场景中根本放不下。revolutionPeriod和rotationPeriod以秒为单位数值上取自真实天体的相对周期关系——地球自转周期设为1秒公转周期设为365秒这样模拟时水星公转88秒转完一圈观感上能体现周期差。真实公转周期365.25天在这里压缩为365秒时间尺度一致比例关系不扭曲。2.2 用代码生成行星球体手动在Hierarchy里创建八个Sphere再逐个赋值太慢写入生成脚本可以顺便规避一个常见坑Unity默认的Sphere是1单位直径直接放大比例会丢失尺寸可比性。我的建议是生成球体后按真实半径的比例关系设置localScale但注意不要拿轨道半径去缩放——轨道空间和行星体积是两套比例尺混用会让水星看起来和木星差不多大。// PlanetGenerator.cs using UnityEngine; public class PlanetGenerator : MonoBehaviour { public Material sunMaterial; public Material[] planetMaterials; // 按行星顺序赋值 void Start() { var sun GameObject.CreatePrimitive(PrimitiveType.Sphere); sun.name Sun; sun.transform.SetParent(transform); sun.transform.localScale Vector3.one * 5f; sun.GetComponentRenderer().material sunMaterial; for (int i 0; i SolarSystemData.Planets.Length; i) { PlanetData data SolarSystemData.Planets[i]; // 轨道锚点负责公转 GameObject anchor new GameObject($Anchor_{data.planetName}); anchor.transform.SetParent(transform); anchor.transform.localPosition Vector3.zero; // 行星球体负责自转 GameObject planet GameObject.CreatePrimitive(PrimitiveType.Sphere); planet.name data.planetName; planet.transform.SetParent(anchor.transform); float planetScale 0.4f i * 0.2f; // 简化比例按索引递增 planet.transform.localScale Vector3.one * planetScale; planet.transform.localPosition new Vector3(data.orbitalRadius, 0, 0); planet.GetComponentRenderer().material planetMaterials[i]; } } }这段代码把行星球体放在锚点的X轴正方向距离为orbitalRadius。公转时旋转锚点的Y轴行星就会以太阳为圆心在XZ平面画圆。为什么放在XZ平面而不是XY平面因为Unity的默认相机视角俯视时XZ平面正好是“俯视太阳系”的观感方向后期按ASDW调整视角时也不会出现行星跑到垂直方向的问题。planetScale这里简化成了按索引递增实际项目可以直接用行星直径数据做等比缩放。注意planetMaterials数组的长度必须与SolarSystemData.Planets一致否则会越界。一个更稳的写法是在Editor脚本里做长度校验这个后面排错章节一起说。3. 自转与公转的运动解耦3.1 自转Transform.Rotate的两种调用姿势自转是最直白的旋转运动行星绕自身Y轴旋转。Transform.Rotate有两种入参方式一种是传欧拉角增量另一种是传一个轴向量加角度。同样一行代码写起来不同性能差别不大但语义上第二种更清楚。// PlanetRotation.cs using UnityEngine; public class PlanetRotation : MonoBehaviour { public float rotationSpeed 360f; // 每秒旋转度数 void Update() { // 方式一相对世界轴旋转 transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World); // 方式二相对局部轴旋转 transform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime); } }逻辑说明两行代码效果几乎一样因为行星的自转轴本来就垂直于公转平面局部Y轴与世界Y轴重合。但写法上有语义差别——方式一指定了Space.World明确绕世界坐标系的Y轴旋转方式二没有指定默认是局部坐标系。如果哪天想把某颗行星的轴倾角做出来比如天王星倾角98度就必须用局部坐标系旋转这时只有方式二能正确工作。所以实际项目里用方式二参数rotationSpeed按表赋值地球设为360f代表每秒转一圈水星58f代表58秒转一圈。把自转周期对应到rotationSpeedrotationSpeed 360f / rotationPeriod。水星自转周期58秒speed约为6.2f视觉上几乎看不出在转这符合真实比例却会让演示效果显得“死板”。一个常见做法是把自转周期整体压缩比如全部除以10让每颗行星看起来都在动同时保持自转速度的相对快慢关系。这个压缩系数应该做成全局变量方便在Inspector中调试。3.2 公转为什么绕开物理引擎直接用角度插值公转按理说属于动力学问题应该交给Rigidbody加引力去模拟。但实际上这个项目里用物理引擎是自找麻烦引力模拟需要每帧对每一对天体求万有引力还要调质量、阻尼、初始速度稍微调错就是行星飞出去或者直接掉进太阳。对教学演示来说没有必要。要做的只是让行星“看起来在围绕太阳转”。所以公转采用运动学方案每帧给轨道锚点增加一个绕Y轴的旋转角度。角度变化量由公转周期决定// PlanetRevolution.cs using UnityEngine; public class PlanetRevolution : MonoBehaviour { public float revolutionPeriod 365f; // 公转周期秒 private float angle 0f; void Update() { float angularSpeed 360f / revolutionPeriod; angle angularSpeed * Time.deltaTime; angle % 360f; transform.localRotation Quaternion.Euler(0, angle, 0); } }逻辑说明angle变量累加每帧转过的角度后取模避免浮点数无限增大导致精度损失。用localRotation赋值而不是Rotate累加好处是角度可读、可断点检查。localRotation是基于父物体的旋转锚点的父物体是太阳原点所以绕Y轴转就是绕太阳转。这里说清楚一个选型理由用Rigidbody模拟公转每帧要算合力、速度、位置性能开销大且不稳定用Transform角度插值每个行星只多一个浮点累加和一个四元数赋值性能开销几乎为零。而且公转周期直接映射为可调参数谁来了都能改。前提是你不需要处理行星之间的引力摄动——演示太阳系运动多体摄动本来就该忽略。3.3 自转与公转的帧率相关性注意点Update是逐帧调用的帧率越高每帧Time.deltaTime越小累加结果一致。但如果在FixedUpdate里做同样的事Time.deltaTime应替换为Time.fixedDeltaTime否则帧率波动时运动速度会漂移。有一类经典Bug是把自转放在FixedUpdate里、公转放在Update里两个循环的调用频率不同帧率不稳定时行星会“打滑”。统一放在Update即可不需要物理步进。把两个脚本挂到对应物体上PlanetRotation挂在行星球体上PlanetRevolution挂在轨道锚点上。层级和组件的对应关系在排错时极其重要——如果发现行星自转变成了公转八成是脚本挂错了物体。4. 交互控制视角切换与加减速的实现细节4.1 键盘输入为什么用Input.GetAxis而不是GetKey原项目描述了ASDW调整视角、Q加速、E减速的交互。视角控制的标准做法是用Input.GetAxis(Horizontal)和Input.GetAxis(Vertical)这两个映射默认对应键盘方向键和WASD同时支持手柄摇杆输入扩展性更好。如果你直接判断Input.GetKey(KeyCode.W)手柄用户就没法玩了。// CameraController.cs using UnityEngine; public class CameraController : MonoBehaviour { public float moveSpeed 30f; void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move (Vector3.right * h Vector3.forward * v) * moveSpeed * Time.deltaTime; transform.Translate(move, Space.World); float scroll Input.GetAxis(Mouse ScrollWheel); transform.position transform.forward * scroll * 50f * Time.deltaTime; } }逻辑说明GetAxis返回的是-1到1之间的平滑值直接乘以速度就是移动距离。Vector3.right是X轴正方向Vector3.forward是Z轴正方向但注意Translate的第二个参数如果传Space.World移动方向不受相机旋转影响——这正是俯视太阳系想要的按W永远是往Z正方向走而不是“朝屏幕里面走”。如果希望按W走向相机面向的方向就把参数改成Space.Self。滚轮缩放是后加的没有也可以。transform.forward是相机当前朝向乘滚轮值缩放距离实现对场景的推进拉远。这个交互对观察内行星和外行星的轨道密度很有用。4.2 Q/E加速减速的三种实现方案加减速看起来简单但实现方式决定了代码的扩展性。原项目说“修改Transform.Rotate的速度”实际操作上有三种方案方案一修改每颗行星公转脚本里的revolutionPeriod。按下Q就把所有周期除以1.05按下E就乘以1.05。方案简单但行星各自保存周期要写循环遍历所有行星容易漏。方案二做一个全局时间缩放器把Time.timeScale整体放大。一行代码解决但Time.timeScale影响所有使用deltaTime的逻辑包括相机移动和UI动画副作用明显。方案三定义一个全局速度倍率public static float GlobalSpeedMultiplier行星公转脚本里每个角度增量乘以这个倍率。这是这个项目里最合理的做法因为只影响天体运动不碰UI和相机逻辑倍率可以做成[Range(0.1f, 10f)]在Inspector里拖拽调试后续想加“暂停”功能把倍率设为0即可而Time.timeScale0会把UI动画也冻住。// SpeedController.cs using UnityEngine; public class SpeedController : MonoBehaviour { public float speedMultiplier 1f; void Update() { if (Input.GetKey(KeyCode.Q)) { speedMultiplier Mathf.Clamp(speedMultiplier * 1.02f, 0.1f, 20f); } if (Input.GetKey(KeyCode.E)) { speedMultiplier Mathf.Clamp(speedMultiplier / 1.02f, 0.1f, 20f); } } }对应的PlanetRevolution改造void Update() { float angularSpeed 360f / revolutionPeriod; angle angularSpeed * Time.deltaTime * SpeedController.Instance.speedMultiplier; transform.localRotation Quaternion.Euler(0, angle, 0); }逻辑说明Mathf.Clamp限制倍率范围在0.1到20防止玩家把速度加到行星直接“瞬移”。倍率增减用乘除法而不是加减法是因为乘除法是等比变化——从1倍到2倍和从2倍到4倍的手感一致加减法会导致越快越感觉不到变化。SpeedController.Instance是单例访问实现方式在类内加public static SpeedController Instance;并在Awake里赋值即可。这里不直接用Time.timeScale理由如前述。4.3 UI操作说明面板的绑定方式操作说明按钮挂在Canvas下。Unity UGUI的标准流程是创建Canvas → 创建Button作为“操作说明”入口 → 创建Panel默认隐藏作为弹窗 → 在Panel里放Text描述操作 → 给Button的onClick绑定脚本方法。// UIManager.cs using UnityEngine; using UnityEngine.UI; public class UIManager : MonoBehaviour { public GameObject helpPanel; public Text speedText; void Awake() { helpPanel.SetActive(false); } public void ToggleHelp() { helpPanel.SetActive(!helpPanel.activeSelf); } void Update() { if (speedText ! null) { float multiplier SpeedController.Instance.speedMultiplier; speedText.text $Speed x{multiplier:F1}; } } }常见坑Canvas默认的渲染模式是Screen Space - OverlayUI会永远显示在最上层所以帮助面板不需要考虑被行星遮挡。Text组件的Rich Text默认开启如果想在文本里显示“Q/E”加粗效果可以直接写bQ/b不需要额外处理。4.4 开普勒第二定律到底要不要做进代码里回到天体物理本身。原项目提到“离太阳近时速度快、离太阳远时速度慢”这是开普勒第二定律的内容。真实模拟椭圆轨道需要每个行星定义轨道离心率、半长轴、真近点角等多个参数代码量翻一倍不止。我的建议是第一版做成圆形轨道匀速周向运动因为公转的视觉主体是“绕转”而不是“椭圆率”等基础运动没问题了再往椭圆轨道上加变速逻辑。很多天体模拟项目翻车就是一开始追求精确把轨道参数配错了行星直接穿出屏幕。验证椭圆轨道是否合理的简易办法让地球轨道orbitalRadius在近地点和远地点差值不超过20%肉眼几乎看不出区别超过50%轨道形状在俯视视角下变得明显但行星运行到远日点的速度如果没同步降低看起来会很假。公式上可以简化为angularSpeed baseAngularSpeed * (1 eccentricity * Mathf.Cos(angle))在近地点转速提高、远地点降低虽然不严格满足面积速度相等但视觉上已经“像那么回事”。5. 用轨道参数验证运动逻辑与常见调试技巧5.1 一套不依赖Debug.Log的验证流程跑起来之后怎么确认自转和公转的速度值是对的用Debug.Log每帧输出角度会刷屏我的做法是写一个简单的验证脚本按空格键时把当前各行星的轨道角度和自转角度打印成一行方便核对。// OrbitDebugger.cs using UnityEngine; using System.Text; public class OrbitDebugger : MonoBehaviour { void Update() { if (Input.GetKeyDown(KeyCode.Space)) { StringBuilder sb new StringBuilder(); PlanetRevolution[] revolutions FindObjectsOfTypePlanetRevolution(); foreach (var rev in revolutions) { sb.Append(rev.name).Append( angle).Append(rev.GetAngle().ToString(F1)).Append(; ); } Debug.Log(sb.ToString()); } } }注意GetAngle()需要在PlanetRevolution里加一个公开的只读属性返回angle。按空格后观察输出如果水星的angle增速大约是地球的四倍说明周期参数正确如果所有行星的angle都一样说明revolutionPeriod没有正确赋值大概率是数据类序列化失败或者Inspector里的值被覆盖了。5.2 三个最容易踩的坑坑一行星公转方向和角度初始值设定。Quaternion.Euler(0, angle, 0)里角度0对应行星在X轴正方向如果所有行星初始角度一样从俯视角看会排成一条直线。一般按索引错开初始角度比如angle i * 45f让行星分散在轨道上观感更好。坑二材质和光源。太阳应该发亮但Unity默认的场景光源是平行光球体太阳不会自发光。正确做法是给太阳加Point Light组件再把太阳材质设为Emission模式或勾选Unlit。否则你会得到一个“黑色太阳”演示时非常尴尬。坑三对象缩放导致父子层级视觉突变。行星放在锚点下面锚点旋转时行星的位置是“锚点位置加局部偏移”但如果中途手动把锚点拖到了别处行星会瞬间飞走。在Inspector里改轨道半径应该改行星的localPosition不是锚点的worldPosition。这个用文字不好表述实际操作一次就会记住。为保险起见PlanetGenerator里应该用anchor.transform.localPosition Vector3.zero把锚点固定在父物体原点所有轨道半径只体现在行星的localPosition上。5.3 时间缩放与倍率控制的前端体验细节速度倍率做到20倍后水星公转一圈只需要约4秒看起来“嗖嗖的”此时Time.deltaTime每帧也许只有0.016秒角度增量依然平滑。但如果倍率继续往上加角速度可能超过每帧90度Quaternion.Euler不会插值会直接跳到最终角度画面出现“闪烁”或“跳变”的视觉残影。这不是程序Bug是角度采样率高于旋转率的必然结果。解决方案是把倍率上限从20降到10或者改用Quaternion.Slerp配合累计角度做插帧后者对教学项目来说过度设计了。验证手段可以这样把speedMultiplier固定为10打开Game视图观察水星连续转5圈耗时是否约为88秒除以10等于8.8秒。计时可以用手机秒表如果偏差超过10%检查是否在Update里用了Time.fixedDeltaTime或者帧率被V-Sync限制导致deltaTime偏大。这类误差在编辑器里很常见Build出来反而正常因为编辑器自身会消耗额外帧时间。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价