简介:这是2022年Unity课程实践完成的模拟太阳系小游戏完整工程,面向需要提交Unity课设的高校学生,也适合刚入门Unity开发、希望了解完整项目结构的新手。项目构建了开放可探索的太阳系场景,玩家可驾驶飞船穿行于各星球之间,靠近后查看对应星球介绍;飞船支持真实受力驾驶与灵巧穿梭两种模式,太阳效果基于Unity官方UBR渲染管线制作。资源共2000个文件,压缩包约246.69MB,主要包含2766个C#脚本(游戏逻辑)、1483张PNG贴图(星球与UI素材)、Shader与ShaderGraph渲染文件(太阳特效)、FBX模型、Prefab场景配置及大量Markdown说明文档,覆盖从场景搭建、脚本控制、UI交互到渲染表现的完整链路。目前已有506人学习下载。获取后可得到整套Unity工程与项目说明,对照源码理解两种移动模式、星球信息面板交互以及UBR管线太阳效果的具体实现,便于直接用于课设提交或在此基础上二次扩展开发。
1. 模拟太阳系课设:看起来“超简单”,翻车点却全在细节上
如果你在 2022 年那一批交过 Unity 课设,应该对“模拟太阳系”这个题目不陌生:不碰物理引擎、不做碰撞、不用寻路,只要让几个球绕着一个大球转,听起来比打飞机、跑酷简单一个量级。可到了验收现场,很多人的作品长一个样——八个颜色不同的球在黑色背景上做匀速平移,老师看一眼就说是“会动的图”,连答辩问题都懒得问。
这个项目真正值钱的地方,不是把行星模型做得像照片,而是让“公转、自转、轨道、信息展示”这套逻辑闭环跑得顺。它适合两类人:一类是 Unity 基础薄弱、想在几天内交出一份及格课设的新手;另一类是已经能跑通小 Demo、但不知道课设怎么做才像“作品”而不是作业的同学。下面这套方案,是我按课设验收标准拆过的落地做法:场景搭建用代码生成,公转自转用参数驱动,最后补上倍速、信息面板和相机切换,让演示从“能转”变成“能讲”。
2. 在 Unity 里让太阳系转起来:场景搭建与两个核心脚本
2.1 用代码生成八大行星:不手拖球体,避免重复挂脚本
很多课设第一步就选错了姿势:先手拖八个球,再一个一个调整大小和位置,最后手动挂公转脚本,光挂脚本就挂了四十分钟。更麻烦的是,如果半径参数不合适,要回到场景里一个球一个球地改,改完一个忘了另一个,运行出来轨道全乱。
我一般会写一个生成器脚本,放在空物体上,让 Start 方法负责创建所有行星。这样做的好处是:行星的位置、大小、速度全部集中在同一份配置里,改数字重进 Play 就生效,不用在 Inspector 里到处翻。
using UnityEngine; public class CreateSolarSystem : MonoBehaviour { public GameObject planetPrefab; // 通用球体预设,带 Sphere Collider public Transform sun; // 太阳的 Transform,公转中心 public Material[] planetMaterials; // 按顺序给每颗行星上色/贴图 [System.Serializable] public class PlanetData { public string planetName; public float orbitRadius; // 轨道半径,单位:Unity 米 public float planetScale; // 显示用缩放倍率 public float orbitSpeed; // 公转角速度,单位:度/秒 public float selfSpeed; // 自转角速度,单位:度/秒 } public PlanetData[] planets; // 在 Inspector 里填 8 组数据 void Start() { for (int i = 0; i < planets.Length; i++) { PlanetData data = planets[i]; // 随机初始角度,避免所有行星一开始在一条直线上 float angle = Random.Range(0f, 360f); Vector3 pos = new Vector3( Mathf.Cos(angle * Mathf.Deg2Rad), 0f, Mathf.Sin(angle * Mathf.Deg2Rad)) * data.orbitRadius; GameObject p = Instantiate(planetPrefab, pos, Quaternion.identity); p.name = data.planetName; p.transform.localScale = Vector3.one * data.planetScale; // 挂公转和自转脚本,参数直接写入 Orbit orbit = p.AddComponent<Orbit>(); orbit.center = sun; orbit.orbitRadius = data.orbitRadius; orbit.orbitSpeed = data.orbitSpeed; SelfRotate rotate = p.AddComponent<SelfRotate>(); rotate.selfSpeed = data.selfSpeed; // 给球体换材质,区分行星 if (i < planetMaterials.Length) { p.GetComponent<Renderer>().sharedMaterial = planetMaterials[i]; } } } }这段脚本的核心是“数据驱动”:把轨道半径、缩放倍率、公转速度、自转速度四组参数写成一个可序列化的结构体数组,在 Inspector 里直接配。Instantiate时一次性把 Orbit 和 SelfRotate 挂好,避免了手工重复操作。
参数说明:planetScale我建议用 0.2~2.5 这个范围,水星最小、木星最大,但不要用真实比例,否则土星会挡掉后面所有行星;orbitRadius控制在 5~35 之间,配合主相机视野 60 度左右,刚好能看全太阳系;orbitSpeed是角度制(度/秒),不是弧度制,方便你心算一圈要多少秒,比如 18 度/秒就是 20 秒转一圈,这个速度在演示时刚好等得起。
2.2 公转与自转脚本:绕太阳转动的最小实现与参数说明
公转脚本本身只有二十行左右,关键在“怎么让行星的初始角度和生成位置保持一致”。如果脚本里直接让角度从 0 开始算,生成时明明放在 30 度方向,播放瞬间就会跳回 0 度位置,看起来像“闪了一下”。我习惯在 Start 里根据当前位置反推角度。
using UnityEngine; public class Orbit : MonoBehaviour { public Transform center; // 绕谁转,这里指向太阳 public float orbitRadius = 10f; // 轨道半径 public float orbitSpeed = 20f; // 角速度,度/秒 private float currentAngle; void Start() { // 从当前位置反推初始角度,防止生成后第一帧跳位置 Vector3 offset = transform.position - center.position; currentAngle = Mathf.Atan2(offset.z, offset.x) * Mathf.Rad2Deg; } void Update() { currentAngle += orbitSpeed * Time.deltaTime; Vector3 offset = new Vector3( Mathf.Cos(currentAngle * Mathf.Deg2Rad), 0f, Mathf.Sin(currentAngle * Mathf.Deg2Rad)) * orbitRadius; transform.position = center.position + offset; } }using UnityEngine; public class SelfRotate : MonoBehaviour { public float selfSpeed = 30f; // 自转速度,度/秒 void Update() { // Space.Self 表示绕自身 Y 轴转,不受父物体旋转影响 transform.Rotate(Vector3.up, selfSpeed * Time.deltaTime, Space.Self); } }这里要解释两个选择。第一,为什么不用transform.RotateAround(center.position, Vector3.up, speed)?因为它内部会在每一帧叠加旋转,如果你在代码生成时设置了位置,再用 RotateAround 会把位置和旋转搅在一起,遇到需要手动调整行星初始角度时会很别扭。用“角度 + 半径算位置”的方式,行星位置是绝对可控的。第二,为什么公转写在 Update 而不是 FixedUpdate?因为课设里的运动只需要视觉正确,不需要物理同步,Update 的帧率波动在视觉上完全看不出来。真要写 FixedUpdate,反而要注意物理步长变化,多一层麻烦。
自转脚本里的Space.Self是重点。如果漏掉第三个参数写成transform.Rotate(Vector3.up, speed),默认是 Space.World,球体挂在太阳系根节点下时确实也没区别;但只要以后你想给轨道加倾斜角度,世界坐标自转就会歪掉。现在先用 Self,后面加轨道倾角不用改代码。
层级结构也要注意:行星不要做成太阳的子物体。如果把行星拖到太阳下面,太阳一旦有缩放或旋转,子物体会跟着产生位移偏移,容易把轨道半径带乱。正确做法是太阳、行星、轨道环全部平级放在根节点下,根节点只负责挂生成脚本。
3. 从“八个球”变成“太阳系”:轨道参数、贴图与轨道环
3.1 轨道参数怎么定:为什么必须用显示比例而不是真实数据
第一次做这个项目的人,最容易陷入“真实数据崇拜”:地球到太阳 1.5 亿公里,轨道半径就填 150000000;土星公转 29.4 年,周期就填 9.3 亿秒。运行结果不用猜,画面里只剩一个太阳,相机被夹在行星轨道之间,随便一动就穿模。Unity 的 Transform 坐标在毫米级精度下没问题,但跨了好几个数量级之后,浮点精度会导致行星在远处抖动,更不用说相机裁剪面得调到离谱才能看见全部轨道。
课设要的是“演示效果”,不是“天文模拟器”。我的做法是:轨道半径用线性压缩,让水星在 5 个单位、海王星在 30 个单位左右;公转周期用等比例缩放,保证相对顺序正确——水星转得最快,海王星最慢,但整体缩短到几十秒以内。下面是我常用的一组参数,可以直接抄进 Inspector。
| 行星 | 轨道半径(Unity 单位) | 显示大小倍率 | 公转速度(度/秒) | 自转速度(度/秒) |
|---|---|---|---|---|
| 水星 | 5.0 | 0.2 | 40 | 15 |
| 金星 | 7.5 | 0.3 | 28 | 10 |
| 地球 | 10.0 | 0.35 | 20 | 20 |
| 火星 | 13.0 | 0.3 | 16 | 18 |
| 木星 | 17.0 | 1.8 | 9 | 30 |
| 土星 | 21.0 | 1.5 | 7 | 25 |
| 天王星 | 26.0 | 0.8 | 5 | 20 |
| 海王星 | 31.0 | 0.8 | 4 | 18 |
这套参数的关键在于:行星大小和轨道半径不要成正比。木星真实直径是地球的 11 倍,但轨道半径按比例放大后它应该放在 110 个单位处,这会超出相机视野。课设里行星是“图标”,不是“模型”,放大到能辨认即可,顺序对、颜色对、周期对,就是合格的太阳系模拟。
公转速度的调整原则是“外慢内快”,但差距不要超过 10 倍。如果水星 40 度/秒、海王星只有 1 度/秒,演示时海王星看起来像静止,老师会问是不是脚本失效。把比例压缩到 4:1 到 10:1 之间,既体现规律,又不让外围行星显得死寂。
3.2 贴图材质与轨道环:用 LineRenderer 画轨道并让行星看得清
八个纯色球在黑色背景下非常像“幼儿园课件”,但课设时间有限,不值得为每颗行星去美术网站找高精贴图。折中方案是:给行星用渐变色材质,在太阳位置放一个点光源,再用 LineRenderer 画出轨道线。一套下来,画面立刻从“占位符”变成“演示作品”。
轨道环不需要美术资源,一个脚本就能画:
using UnityEngine; [RequireComponent(typeof(LineRenderer))] public class OrbitRing : MonoBehaviour { public float radius = 10f; // 轨道半径,和对应行星保持一致 public int segments = 128; // 分段数,越高曲线越圆 public float lineWidth = 0.03f; // 线条宽度 void Start() { LineRenderer lr = GetComponent<LineRenderer>(); lr.positionCount = segments + 1; lr.startWidth = lineWidth; lr.endWidth = lineWidth; for (int i = 0; i <= segments; i++) { float angle = i * 360f / segments * Mathf.Deg2Rad; float x = Mathf.Cos(angle) * radius; float z = Mathf.Sin(angle) * radius; lr.SetPosition(i, new Vector3(x, 0f, z)); } } }参数说明:segments = 128是视觉平滑和性能的平衡点,太小会看到折线,太大在小半径轨道上没必要;轨道环的 Y 坐标固定在 0,如果行星轨道不在水平面,这个脚本要加一个inclination角度旋转整个 LineRenderer;lineWidth按轨道半径调整,外围轨道可以稍微加粗到 0.05,内圈保持 0.03,视觉层次会更清楚。
材质方面,不要在 Unity 默认材质里直接选纯色。我一般会在工程里建两个 Shader 预设:一个是不透明标准材质,给岩石行星用,调整基础色和粗糙度;另一个给太阳用,把自发光(Emission)开到 2 以上,让它看起来像光源。太阳位置放一个Point Light,范围盖住最外层轨道,强度调到能照亮行星背阳面,这样行星不会只有靠近太阳那一半是亮的。这一步常常被忽略,但实际效果提升非常明显。
4. 答辩演示能力:时间倍速、行星信息面板与相机切换
4.1 时间倍速与暂停:一个 TimeController 脚本统一管理
演示时最尴尬的是木星公转一圈要 40 秒,老师在台下看完水星转两圈就想走。只加速不动代码,就得不停改 orbitSpeed,改完又得重置。正确做法是做一个全局时间倍速控制器,让所有公转、自转脚本统一读取它。
using UnityEngine; public class TimeController : MonoBehaviour { [Range(0.1f, 10f)] public float timeScale = 1f; // 当前速度倍率 public bool paused; public static TimeController Instance; // 简单位置的单例 void Awake() { Instance = this; } void Update() { if (Input.GetKeyDown(KeyCode.Space)) { paused = !paused; } if (Input.GetKeyDown(KeyCode.UpArrow)) { timeScale = Mathf.Min(10f, timeScale * 2f); } if (Input.GetKeyDown(KeyCode.DownArrow)) { timeScale = Mathf.Max(0.1f, timeScale / 2f); } } }然后在 Orbit 和 SelfRotate 脚本里,把Time.deltaTime换成Time.deltaTime * TimeController.Instance.timeScale,暂停时直接把 timeScale 当成 0 处理,或者加一个 if 判断。
这里有一个很关键的差别:为什么不直接改Time.timeScale?因为 Unity 的Time.timeScale会同时影响相机动画、UI 动画、粒子系统,结果就是切倍速时整个界面都跟着变速,尤其是 UI 提示闪烁频率会变得很快,答辩现场很掉价。用自定义倍速只作用于行星运动脚本,相机的阻尼和 UI 都保持正常帧率,观感完全不同。
4.2 点击行星弹出信息面板:射线检测与 UI 的最小实现
演示时老师经常问“这是哪颗星?离太阳多远?”之类的问题,如果行星上没有信息,就得靠嘴硬答。做一个点击显示信息的脚本,属于课设里性价比最高的加分项。核心逻辑就是:鼠标点击位置发射一条射线,命中带 Collider 的行星后,从它的数据组件里读取参数并显示到 UI 文本。
using UnityEngine; using UnityEngine.UI; public class PlanetInfo : MonoBehaviour { public Camera mainCamera; public Text nameText; // 场景中 Canvas 的标题 Text public Text detailText; // 场景中 Canvas 的详情 Text void Update() { if (!Input.GetMouseButtonDown(0)) return; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 200f)) { PlanetDataHolder data = hit.collider.GetComponent<PlanetDataHolder>(); if (data != null) { nameText.text = data.planetName; detailText.text = data.GetInfo(); } } } }using UnityEngine; public class PlanetDataHolder : MonoBehaviour { public string planetName; public float orbitRadius; // 显示用半径,不是真实值 public float periodSeconds; // 演示中一圈的秒数 public string GetInfo() { return string.Format( "公转半径:{0} 单位\n演示周期:{1} 秒", orbitRadius, periodSeconds); } }这段脚本要注意两个坑。第一,UI 文本的引用必须手动拖到 Inspector,如果场景里没有 Canvas,Text 组件不存在,脚本会空引用;第二,射线检测要求行星球体上有 Collider,Unity 的 Sphere Collider 默认就有,但如果你用了自己的网格模型,记得手动加。这块也顺带说明:Raycast的out RaycastHit hit是 Unity 5.0 之后推荐的写法规避内存分配,课设里写Physics.Raycast(ray, out hit)就够,不要在每帧 Update 里反复 new RaycastHit 数组。
4.3 相机视角切换:自由观察与跟随行星的两种模式
默认透视相机固定在一个位置,老师想看看海王星轨道,就得手动拖视角。如果演示时间三分钟,视角操作不顺会扣印象分。我在这里做了一个双模式相机:自由模式和跟随模式,按 F 键切换。
using UnityEngine; public class CameraController : MonoBehaviour { public Transform target; // 跟随目标,运行时动态赋值 public float rotateSpeed = 100f; // 右键旋转速度 public float zoomSpeed = 5f; // 滚轮缩放速度 public float minDistance = 2f; public float maxDistance = 80f; private bool isFollowing; private float distance = 20f; void Update() { // 右键拖拽旋转 if (Input.GetMouseButton(1)) { float x = Input.GetAxis("Mouse X") * rotateSpeed * Time.deltaTime; float y = Input.GetAxis("Mouse Y") * rotateSpeed * Time.deltaTime; transform.Rotate(Vector3.up, x, Space.World); transform.Rotate(Vector3.right, -y, Space.Self); } // 滚轮缩放 float scroll = Input.GetAxis("Mouse ScrollWheel"); distance = Mathf.Clamp(distance - scroll * zoomSpeed, minDistance, maxDistance); transform.position = transform.position + transform.forward * (scroll * zoomSpeed); // F 键切换跟随 if (Input.GetKeyDown(KeyCode.F) && target != null) { isFollowing = !isFollowing; } if (isFollowing) { Vector3 offset = new Vector3(0f, 3f, -4f); transform.position = target.position + offset; transform.LookAt(target); } } }参数说明:rotateSpeed = 100是鼠标灵敏度,太高容易转晕,太低拖半天看不到目标;zoomSpeed = 5适合 5~80 的范围,如果你把轨道半径拉到 50,这里得同步加到 8 左右。跟随模式里的 offset 写死(0, 3, -4),是一个从斜上方看的经典角度,行星不会挡视线,也能看清自转。注意自由模式下不要每帧给 transform.position 赋固定值,否则滚轮缩放会和旋转冲突。
5. Unity 课设避坑指南:5 个经典翻车点与对应修复方案
5.1 太阳没有赋值,一运行就报空引用
现象:点击 Play 后一帧就报NullReferenceException,所有行星原地不动。
原因:Orbit 脚本里的center字段是公开的,但程序生成时拿不到太阳的 Transform。如果在 Inspector 里忘记把 sun 拖到 CreateSolarSystem 的sun槽上,代码里orbit.center = sun;就会写入 null。
解决:生成器里加一层判空保护。在Start开头写:
if (sun == null) { GameObject sunObj = GameObject.Find("Sun"); if (sunObj != null) sun = sunObj.transform; }同时把 Orbit 脚本里也做保护:
void Update() { if (center == null) return; // 原有移动逻辑 }这样即使漏配置,至少不会白屏报错,行星停在原地,你还能在 Hierarchy 里找到问题。
5.2 轨道半径和相机设置不匹配,画面一片黑
现象:运行以后什么也看不到,或者只能看到几个发亮的小点,连行星都分不清。
原因:轨道半径填了 500,相机远裁剪面Far Clip Plane还是默认 1000(这看起来没问题),但相机位置在原点附近,视角 60 度,行星全部挤在边缘;更常见的是太阳缩放太大,把行星全部包在太阳模型内部,相机进到太阳网格里被包围成黑屏。
解决:轨道半径先按 5~31 设置,确认运行正常后,再看太阳缩放。太阳的缩放不要超过 3,否则水星轨道会被太阳模型挤掉一半。相机初始位置放在(0, 20, -45),看向原点,远裁剪面设 500,这组数值对上面参数表是通用的。如果画面还是偏暗,检查太阳位置是否有点光源,光照强度是否大于 1。
5.3 倍速拉高后行星“穿越”轨道
现象:倍速调到 10 倍后,行星不是沿着轨道连续移动,而是一闪一闪地跳位置,甚至从轨道一边瞬间跳到另一边。
原因:公转脚本用Update累加角度,倍速变大后每帧角度增量变大,比如一度速 18 度/秒、10 倍就是每帧增加 3 度,在 30 FPS 下看起来还勉强;如果帧率掉到 15 FPS,单帧角度增量 12 度,行星在画面里就变成瞬移。
解决:把倍速上限压到 10 倍以内,同时把公转计算改成线性插值。最简单的做法是在 TimeController 里限制timeScale最大值为 10,并在 Orbit 脚本中改成:
float realSpeed = orbitSpeed * TimeController.Instance.timeScale; currentAngle += realSpeed * Time.deltaTime;如果倍速用到 20 倍以上,就得把角度计算挪到FixedUpdate并插值更新位置,否则任何方案都会有跳变。课设一般 10 倍封顶足够,没有必要做那么快。
5.4 行星贴图显示不出来或纹理朝向不对
现象:贴图资源拖到 Material 上后,球体显示成纯色,或者纹理被拉伸成几条色带,完全看不出像行星。
原因:一是贴图的导入设置里 Texture Type 不是 Default,被 Unity 按 Sprite 模式处理了;二是球体模型的 UV 展开方式与贴图不匹配,常见于从外部下载的模型;三是球体缩放不均匀,导致纹理采样密度不一致。
解决:在 Project 窗口选中贴图,把Texture Type改成Default,Wrap Mode设成Repeat,然后点 Apply。如果贴图还是撕裂,换成 Unity 自带的 Sphere 模型(GameObject > 3D Object > Sphere),再做材质。对于土星,直接在SelfRotate脚本里加一个Vector3 rotateAxis字段来控制轴方向,想让土星环看得清,就把轴设成(0, 0.2, 1)左右,稍微倾斜一点看起来更自然。
5.5 所有行星都从同一个方向出发,拍屏像静态图
现象:运行第一帧所有行星挤在一条直线上,水星、金星、地球互相重叠,看不出来在转。
原因:生成脚本里每个行星的初始角度写死成同一个值,运行后虽然会分散,但答辩时老师刚点开 Play 看到的就是一条线,印象极差。
解决:就是你见到的代码里那行Random.Range(0f, 360f)。注意用Random而不是写死角度。另一个更可控的做法是:在 Inspector 给每个行星手动填startAngle字段,分布 45 度错开,这样每次打开演示画面都是散开的。随机的好处是每次跑都不一样,手动的好处是能看到所有行星轨道,我倾向于在课设里用手动分配,方便截图存档。
6. 交付前最后一小时:用验证清单和演示习惯降低翻车概率
把功能做完到真正递交之间,还有一个最容易出事的环节:场景没保存、脚本编译错误没发现、导出后没有音效或画面变暗。我习惯在交付前做一张验证清单,按顺序过一遍,通过才导出。
验证清单分三块。运动块:运行场景,按空格确认暂停生效;按上键把倍速拉到 4 倍,确认水星明显比海王星转得快;点击任意行星,确认 UI 面板显示的信息和行星对应。视觉块:把相机拖到最外层轨道外,确认最外圈轨道完整可见;切换到跟随模式跟随木星,确认相机不会穿进行星模型内部。导出块:从 Build Settings 导出 Windows 可执行文件,在导出前确认场景在 Build 列表里,不要把空场景打进去;导出后运行一次 exe,确认不是只在编辑器里能跑。
演示时最好固定一条“开场动线”:先展示整体轨道,同时按上键加速,让老师看到“外慢内快”的运动关系;然后切换到水星跟随视角,说明自转方向;最后点一下地球,把信息面板展示给老师。整个过程一分钟左右,节奏紧凑。我会把这条动线写在一个 txt 文件里放在工程根目录,名字就叫“演示步骤.txt”,答辩前对着练一遍,跑急了也不丢关键环节。
还有一个容易忽略的点:导出 exe 之前,把编辑器里的 Game 视图分辨率设置成 16:9,比如 1280×720。很多同学在编辑器里用 Game 视图默认分辨率测试没问题,导出后在老师 16:10 的笔记本上画面被拉伸,UI 文字模糊。提前锁定分辨率,至少保证大部分屏幕下显示正常。
太阳系课设做到这个程度,本质上就是一个“参数工程”:轨道数据合理、运动逻辑清晰、演示工具顺手。哪一届课设里都有因为参数没调好被说“像挂图”的作品,但真正按上面这套流程做下来的,几乎没有不过的道理。你现在手头如果正卡在某个报错上,先别急着改代码,按第 5 章的五个坑对一遍,多半能找到原因。希望帮到你。
本文还有配套的精品资源,点击获取