Unity3D太阳系模拟实战:自转公转与时间缩放控制
2026/9/16 12:48:32 网站建设 项目流程

简介:面向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场景中根本放不下。revolutionPeriodrotationPeriod以秒为单位,数值上取自真实天体的相对周期关系——地球自转周期设为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.GetComponent<Renderer>().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.GetComponent<Renderer>().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.timeScale=0会把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 - Overlay,UI会永远显示在最上层,所以帮助面板不需要考虑被行星遮挡。Text组件的Rich Text默认开启,如果想在文本里显示“Q/E”加粗效果,可以直接写<b>Q</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 = FindObjectsOfType<PlanetRevolution>(); 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出来反而正常,因为编辑器自身会消耗额外帧时间。

本文还有配套的精品资源,点击获取

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

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

立即咨询