尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity模拟太阳系小游戏开发:公转自转、相机动画与答辩避坑全攻略

Unity模拟太阳系小游戏开发:公转自转、相机动画与答辩避坑全攻略 简介这是一份2022年基于Unity课程实践开发的模拟太阳系小游戏项目面向Unity入门学习者、游戏开发课程学生以及需要完成课设或做技术预研的开发者。项目以飞船探索太阳系为主线实现了两种驾驶模式一种采用真实受力模拟移动手感更贴近物理另一种为通用灵活移动方式便于在太空中穿梭自如靠近行星时还可查看介绍信息太阳光效运用了Unity官方渲染管线制作。压缩包为zip格式共包含2000个文件以C#脚本、Prefab预制体、FBX模型、PNG贴图、材质与着色器、Markdown文档为主整个资源包约246.69MB项目结构与场景已完整搭建解压后即可在Unity中打开查看。目前已有506人学习下载其中不仅包含可直接运行体验的完整工程还有说明文档辅助理解。可帮助读者掌握Unity场景管理、物体运动控制、UI信息展示、渲染管线应用等知识点适合作为课设参考模板也适合在此基础上继续扩展行星数量或增加交互玩法。1. 一个“超简单”的太阳系课设为什么有人拿了高分有人被答辩追问标题里有“2022”“Unity”“模拟太阳系小游戏”三个关键词这题目年年都有人选做出来的东西差异极大有人三天交一版“九颗球绕太阳转”就算完工有人后面还加了轨道线、速度调节、相机跟随和启动提示答辩时三分钟就能把功能演示完。它不涉及物理引擎、不涉及复杂AI拼的是场景组织、旋转逻辑和光照设置这三件事。适合第一次做Unity课设的学生也适合想在周末补一个完成度较高Demo的开发者我会按“先搭结构、再写逻辑、后调效果、最后排坑”的顺序把整个实现路径讲清楚。2. 先把层级结构搭对太阳系课设的Hierarchy决定了后续八成的工作量2.1 用空物体当轨道枢纽别把月球直接挂到地球下面很多Unity新手拿到这个课设第一反应是建几个Sphere然后把月亮拖到地球下面再把地球拖到太阳下面。这个父子关系在“静止模型”里没问题但在“公转加自转”的动效模型里会埋雷子物体继承父物体的位移和旋转当地球用Space.Self自转时月球会跟着地球翻面当地球公转时月球的公转半径会被父物体位移叠加。更麻烦的是你很难一眼看清“月球到底绕谁转”。我一般会把场景挂成下面这样的结构SolarSystem空物体 ├── Sun │ └── SphereScale3挂Unlit材质 ├── Planet_Earth挂Orbit和Spin ├── Moon_Pivot空物体挂Orbitcenter指向Planet_Earth │ └── Moon挂Spin ├── Planet_Mars挂Orbit和Spin ├── Planet_Jupiter挂Orbit和Spin └── CameraRig挂CameraController这个结构里最关键的是Moon_Pivot它是空物体不挂任何Mesh月球公转脚本挂在它身上月球自转脚本挂在Moon自己身上。月球本体和地球本体之间没有父子关系位置全由脚本在运行时计算。这样任何时刻公转和自转的职责都清清楚楚不用靠Transform层级去猜“这个旋转到底是谁带出来的”。这种设计的另一个好处是排错方便。答辩前检查时只要看Inspector里的组件列表就能说出每颗星“绕谁转、转多快、倾角多少”而不是在一堆父子节点里翻来翻去。课设评分时能被评委当面问的问题大多集中在“这个月球的轨道怎么实现的”空物体枢纽方案可以直接讲透。2.2 自转和公转拆成两个脚本一个组件只干一件事拆分还有个实际原因Unity的Transform同时只能保留一个旋转状态。如果你在同一个Update里先调transform.RotateAround再调transform.Rotate两个旋转会依次叠加速度一快就出现“行星边转边抖”。拆成两个组件后Orbit挂在星球上控制公转Spin挂在星球上控制自转两个类各写各的Update互不干扰。这是我在课设里最常用的两个最小类using UnityEngine; public class Orbit : MonoBehaviour { public Transform center; // 绕谁公转通常拖太阳或月球枢纽 public float radius 10f; // 轨道半径场景单位 public float angleSpeed 12f; // 公转速度度/秒 public float axisTilt 0f; // 轨道面倾角单位度 private Vector3 orbitAxis; // 轨道法线方向由axisTilt决定 void Start() { if (center null) { Debug.LogError(name 的 Orbit 组件没有指定 center); return; } orbitAxis Quaternion.Euler(axisTilt, 0f, 0f) * Vector3.up; Vector3 fromCenter transform.position - center.position; fromCenter Vector3.ProjectOnPlane(fromCenter, orbitAxis).normalized * radius; transform.position center.position fromCenter; } void Update() { if (center ! null) { transform.RotateAround(center.position, orbitAxis, angleSpeed * Time.deltaTime); } } }using UnityEngine; public class Spin : MonoBehaviour { public float speed 20f; // 自转速度度/秒 public Vector3 selfAxis Vector3.up; // 自转轴默认让南北极朝上 void Update() { transform.Rotate(selfAxis, speed * Time.deltaTime, Space.Self); } }逻辑说明Orbit.Start里按radius重摆位置这一步很多人会删掉但留着能保证星球永远从半径一致的轨道起点开始转不会出现“上一帧拖歪了轨道线对不上”的问题。Vector3.ProjectOnPlane把当前位置投影到轨道平面上防止center和星球初始Y轴有偏差时轨道整体倾斜。Spin用Space.Self转自转轴始终跟随天体本地坐标而不是世界坐标的Y轴。如果你把第三个参数改成Space.World地球转半圈后自转轴会乱指这是自转翻车的头号原因。参数说明angleSpeed按“一圈360度”来试地球30秒一圈就填12水星12秒一圈就填30速度越快越适合答辩演示自转速度建议比公转快一倍以上否则看不出自转效果。axisTilt在0到10度之间微调即可课设评委几乎不会拿量角器量轨道倾角但0度和10度在视觉上的差异很明显。2.3 轨道可视化用LineRenderer画一圈线把“转”变成“看得见的转”公转代码写好之后如果场景里没有轨道线看起来只是一颗球在空转。这是Unity里最常见的“逻辑对了但演示效果为零”的环节。给OrbitLine挂到各轨道所在的枢纽上用LineRenderer在轨道平面上画一圈圆几十行代码就能让画面立刻像太阳系。using UnityEngine; [RequireComponent(typeof(LineRenderer))] public class OrbitLine : MonoBehaviour { public Transform center; public float radius 10f; void Start() { LineRenderer lr GetComponentLineRenderer(); int segments 72; // 72段肉眼已经够圆 lr.positionCount segments 1; // 顶点比段数多1保证闭合 lr.startWidth 0.05f; lr.endWidth 0.05f; for (int i 0; i segments; i) { float angle i / (float)segments * Mathf.PI * 2f; float x Mathf.Cos(angle) * radius; float z Mathf.Sin(angle) * radius; lr.SetPosition(i, new Vector3(x, 0f, z)); } } }逻辑说明LineRenderer挂在空物体上Start里直接喂顶点不依赖外部计算。注意这里画的是纯平面圆形轨道如果Orbit里设置了axisTilt轨道线就要和它保持同一个倾斜方向否则会出现“球在斜轨道上转线在水平面上画”的分裂感。解决方案是把OrbitLine也挂到带倾角的枢纽下或者在算顶点时整体乘一个Quaternion.Euler(tilt, 0, 0)再赋值。参数说明radius必须与Orbit里填的radius一致segments设72就够了再高对肉眼没有区别。LineRenderer的材质换成Unlit/Color透明度调到0.5左右轨道线不会抢走星球本身的视觉重点。3. 公转自转调参转速、半径与轨道倾角怎么设才像“模拟太阳系”3.1 先搞清楚一件事正角度到底往哪转Unity的Transform.RotateAround以Vector3.up为旋转轴时从Top视角从正Y向下看看到的正角度旋转是顺时针方向。这句话非常反直觉因为很多教材里默认“正角度逆时针”那是右手坐标系的结论。Unity用左手坐标系Y轴向上的时候正角度从顶视图看就是顺时针。我不建议去背这个结论因为一紧张就会记反。更稳的做法是先给angleSpeed填一个正数在Scene窗口切到Top视角按播放键看行星往哪走想要逆时针就把angleSpeed取负或者把orbitAxis取反。轨道线在这个环节很关键——没有轨道线时你得盯着一颗球判断方向有了轨道线一眼就能看出它是在线上走还是在打转。3.2 每颗星的推荐参数表把真实数据按比例压缩到“肉眼可辨”真实太阳系里水星公转周期88天海王星165年如果按真实比例做海王星一整节课都一动不动课设答辩现场会非常尴尬。所以参数的第一原则是把周期压缩到10到120秒区间让每个评委肉眼都能看出速度差。行星轨道半径场景单位公转一圈秒数自转一圈秒数备注水星3.5104最内圈速度最快金星520-10自转方向相反负号表示反转地球7308同时作为月球的公转中心火星94512半径差拉开后肉眼可见木星12606自转比地球快木纹可旋转土星15807加一个Ring环更出效果天王星18100-8躺转selfAxis改为(1,0,0)海王星2212015最外圈慢而稳填进代码时angleSpeed直接填360除以“公转一圈秒数”地球30秒一圈就填12水星10秒一圈就填36。自转也一样把360除以秒数。表格里的负号不是数学上的负数是为了提醒你金星和天王星的自转方向特殊写代码时直接给speed填负值就能反转。这个表不是精确的天文数值是“演示合理值”。我见过有同学拿真实比例做运行半小时火星才走了几度答辩时评委第一句就问“它是不是卡住了”。课设评分看的是轨道模型是否正确、交互是否清楚不是天文精度。3.3 用空物体做“月球绕地球”父节点方案的翻车原因如果让月球成为地球的子物体再给月球写一个公转代码实际上会发生两件坏事第一地球自转时月球会跟着地球“翻面”因为子物体继承了父物体的旋转第二月球的轨道半径是相对于地球的但地球自身也在公转两个位置叠加会让月球轨迹变成奇怪的螺旋线。所以月球绝对不能是地球的子物体。正确的结构在2.1节里已经给了Moon_Pivot平级放在SolarSystem下月球公转脚本挂在Moon_Pivot上把center指向Planet_Earth。这里给一个不依赖RotateAround的写法某些场景下更直观using UnityEngine; public class MoonOrbit : MonoBehaviour { public Transform earth; public float radius 2f; public float period 10f; void Update() { if (earth null) return; float angle 360f / period * Time.time; Vector3 offset new Vector3( Mathf.Cos(angle * Mathf.Deg2Rad), 0f, Mathf.Sin(angle * Mathf.Deg2Rad) ) * radius; transform.position earth.position offset; } }逻辑说明用Time.time作为相位累计period10表示10秒绕地球转一圈。每帧把月球位置设成“地球当前位置加一个半径偏移”偏移方向由角度决定所以月球永远围绕实时移动的地球转且不会继承地球的自转姿态。参数说明radius填2表示月球离地球2个场景单位period填8到12都行太快了看不清月球形状。这个脚本挂在Moon_Pivot空物体上Moon本体继续挂Spin做自转。注意使用这个方案时不要再在Moon_Pivot上挂Orbit脚本两套公转逻辑同时跑会打架。3.4 土星环唯一适合用父子结构的地方土星环和土星的关系与月球不同环和行星本体是同步自转的所以它适合挂成子物体。做法是给土星加一个Torus圆环MeshScale沿X和Z方向拉大放到土星球体的赤道位置环的材质用半透明Unlit颜色偏灰黄环就出来了。把Torus拖到Planet_Saturn下面土星自转时会带着环一起转效果自然。这里要注意的是Torus的默认朝向可能不是水平方向需要把它的Rotation的X调到90度让环面与土星赤道面对齐。环的半径不要超过土星球体Scale的1.5倍否则视觉上像个飞碟。4. 光照材质相机三关让太阳亮起来别让行星黑掉或镜头走丢4.1 平行光和点光源的正确分工默认场景里的Directional Light是全局方向光它会照亮场景里所有物体。很多教程新手往太阳位置再放一盏Point Light结果整个场景都被点光源照亮行星变成“到处都是亮的”太阳系失去了层次感。正确分工是平行光负责整体照亮太阳自身用自发光材质把“发光体”表现出来星空用天空盒或粒子不需要额外加第四盏灯。如果你确实想做“离太阳近就更亮”的效果可以加一盏Intensity为0.5、Range为12左右的暖黄色Point Light。但注意它的Range不要覆盖到木星轨道否则远处行星也被照得曝光效果反而失真。我一般会关掉Point Light的阴影投射只保留亮度衰减让水星金星区域微微泛黄。场景光源参数可以参考这个组合光类型Intensity颜色用途Directional Light1.0白色偏暖照亮所有天体Point Light可选0.5橙黄模拟太阳近处的热辐射太阳自身无亮黄白用Unlit材质表现发光4.2 Unlit材质 vs Standard材质太阳为什么不能黑脸把太阳材质Shader换成Unlit/Color颜色设为亮黄白比如#FFF3C4这是最稳的做法。为什么不建议用Standard材质加Emission因为Standard材质仍然会受平行光影响Emission数值调低时背光面发灰调高了又容易过曝而且Standard材质在打包后shader被裁剪掉的风险比Unlit更高后面5.4会提到。Unlit材质完全忽略光照颜色即最终像素。对“太阳”来说它就是自己发光的不受平行光影响才是对的。太阳的Scale建议3左右相机Far Clip Plane至少要调到200否则太阳运行到远处时会被相机裁剪面直接切掉出现“太阳消失了”的灵异现象——这不是代码问题是Far太小。4.3 相机跟随与LookAt让镜头永远锁定目标没有相机脚本的时候行星一旦开始公转镜头里就找不到东西了。最简单的方式是让相机始终看向一个目标再用鼠标滚轮控制观察距离using UnityEngine; public class CameraController : MonoBehaviour { public Transform target; // 要跟随的星球 public float distance 12f; public float height 5f; public float rotateSpeed 80f; public float scrollSpeed 5f; private float currentAngle 0f; void Update() { if (target null) return; if (Input.GetKey(KeyCode.Q)) currentAngle rotateSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.E)) currentAngle - rotateSpeed * Time.deltaTime; distance - Input.mouseScrollDelta.y * scrollSpeed; distance Mathf.Clamp(distance, 3f, 60f); Vector3 pos target.position; pos.y height; transform.position pos Quaternion.Euler(0f, currentAngle, 0f) * (Vector3.forward * distance); transform.LookAt(target); } }逻辑说明字母Q和E绕Y轴旋转观察角度鼠标滚轮控制缩放。每次先算好相机应处的位置再用LookAt把镜头的正前方对准目标。LookAt省去了手动计算欧拉角的麻烦这是它在这里不可替代的价值。参数说明height设置成目标Scale的一半到一倍比较合适目标Scale为1时height填1.5距离下限设3防止镜头穿进行星内部。target在Inspector里拖入Planet_Earth就能一直跟拍地球拖入Sun则看全场景。答辩演示时可以直接用这个脚本巡视每一个天体不需要切Scene窗口。4.4 星空背景粒子星星和天空盒的取舍全黑场景做星空最简单的方案是给Camera的Clear Flags选Skybox再用Unity内置的Procedural Skybox调一个深色天空。想更出效果可以加一个Particle System做星星参数上注意三点Start Lifetime不要设InfinityMax Particles设100到200Emission Rate设0然后在脚本里手动Emit一次。这三点直接关系到5.1节要讲的粒子内存泄露问题这里先留个伏笔。星星粒子的大小调到0.05到0.1之间材质用Additive混合模式颜色选偏蓝白。粒子挂在空物体上不要挂在相机下否则相机一晃星星也跟着晃答辩时会暴露“这个星空是贴上去的”这个事实。5. 太阳系课设避坑清单五处查完就能安心答辩的隐患5.1 粒子星空越跑越卡Start LifetimeInfinity是内存泄露的元凶现象星空加完后前两分钟流畅三分钟后帧率从60掉到25任务管理器里Unity的内存一直往上涨场面逐渐卡顿。原因星空粒子用了Particle System的默认参数Start Lifetime被设成Infinity意味着粒子永远不会自然消亡系统要为每颗粒子保留完整生命周期数据。配合Emission模块持续发射新粒子内存只进不出这就是Unity里典型的粒子特效内存泄露场景。课设项目小但粒子星空是每帧都活跃的组件跑十分钟后压力会非常明显。解决把Start Lifetime从Infinity改成9999Max Particles设200Emission Rate改为0。然后在场景入场时手动Emit一次让星星固定存在不再持续生成。如果已经出现卡顿直接调用ParticleSystem.Clear强制回收ParticleSystem ps GetComponentParticleSystem(); ps.Clear(true);逻辑说明Clear会立即清空所有存活粒子并把内存标记为可复用在切场景、切视角时调用一次就够了。参数说明Max Particles200对于星空足够100也行Emission Rate0是关掉持续发射的关键否则Clear后又会慢慢涨回来。5.2 行星转反了、轴也歪了坐标系的直觉在这里不靠谱现象所有行星从顶视图看都是顺时针和教程里的“逆时针”相反或者地球转着转着自转轴逐渐指向太阳变成躺着转。原因第一个现象是因为Unity左手坐标系下正角度从Top视角看就是顺时针。第二个现象是因为Spin脚本里用了Space.World而不是Space.Self自转轴被固定在世界坐标Y轴方向当地球公转到太阳另一侧时本地南北极方向已经变了但自转轴没跟着变视觉上就是“翻转”。解决方向问题不看理论看轨道线。把angleSpeed填正数跑一帧方向不对就在数值前加负号这是最省脑子的做法。自转轴问题更简单Spin脚本的transform.Rotate固定用Space.Self自转轴始终保持本地方向无论公转到哪里北极始终朝本地Y轴。5.3 按暂停后星球停了、UI倒计时还在走现象用Time.timeScale0实现暂停后行星公转和自转都停了但屏幕上的倒计时或者星球介绍文本还在正常跳字。原因UI数字滚轮的刷新逻辑用的是Time.deltaTime但UI动画在Animator组件里没处理timeScale。Time.timeScale0会让所有依赖Time.deltaTime的逻辑停住这只对3D场景生效UI部分如果用的是Animator的Unscaled Time模式就会照常动画。解决两个方案任选。一是UI动画帧率模式改成Unscaled Time路径在Animator组件的Update Mode下拉框里默认是Normal改成Unscaled Time就不会受暂停影响。二是手动加一行暂停判断代码里更直白if (Mathf.Approximately(Time.timeScale, 0f)) return; // 暂停时跳过UI计时逻辑说明Mathf.Approximately比较浮点数近似相等避免直接写Time.timeScale 0f出现精度问题。参数说明这段代码放在UI计时逻辑的Update开头比改Animator模式更可控因为你明确知道“暂停时UI不该走”。5.4 打包后太阳黑了编辑器里却一切正常现象在编辑器里点Play一切正常Build出exe后整个场景黑得只剩自发光物体或者太阳和轨道线全变成紫色。原因两处坑最常见。第一Build Settings的Scenes列表里没把当前场景加进去Unity打包成了一个空场景自然一片黑。第二Shader Stripping把Unlit/Color当成“没用到”的Shader裁剪掉了打包后Unlit材质无法渲染显示成紫色或黑色。解决Build Settings里确认Scenes列表包含游戏场景在Player Settings的Graphics设置里把Always Included Shaders加入Unlit/Color。或者更省心一点Player Settings的Graphics下别开Auto Graphics API保持默认IL2CPP不激进裁剪。打包前先把这两处检查一遍能省掉大半的“打包翻车”现场。分辨率设置也建议在这里统一Game视图分辨率和打包后的Player Settings里的分辨率保持一致避免UI文字在窗口比例变化时错位。5.5 运行时所有星球叠在太阳上的一团center没拖造成的全灭现象按下Play的瞬间所有行星挤到太阳位置附近轨道线正常但星球全堆在原点那一圈。原因Orbit.Start里用到center.position如果center没有在Inspector里拖入太阳center是null但代码里没判空就会走到后面的运算。更隐蔽的是如果center字段被误拖成某个行星行星的公转中心就变成了那颗星整个太阳系结构瞬间崩坏。解决给Orbit加空引用保护并且把错误直接打印出来别让它悄悄跑void Start() { if (center null) { Debug.LogError(name 的 Orbit 组件没有指定 center请把太阳拖入槽位); return; } }逻辑说明加return之后该星球不会公转但不会崩日志会直接指出哪颗星有问题省去逐个检查的时间。如果你有八颗行星要拖八次center建议在SolarSystem上挂一个批量初始化脚本把center统一赋值public Orbit[] orbits; public Transform sun; void Start() { foreach (Orbit o in orbits) { if (o ! null) o.center sun; } }参数说明orbits数组在Inspector里拖满所有行星的Orbit组件sun拖入太阳。这个脚本只跑一次比手动拖字段防漏。6. 答辩收尾技巧一键切换观察目标、提示文字消失与轨道验证6.1 Tab键循环切换观察目标答辩时最尴尬的时刻是评委问“看一下木星”而你还得在Inspector里手动把相机的target拖到木星上。提前做一个目标数组用Tab键循环切换public Transform[] targets; public CameraController cam; private int index 0; void Update() { if (Input.GetKeyDown(KeyCode.Tab)) { index (index 1) % targets.Length; cam.target targets[index]; } }把太阳和八颗行星拖进targets数组cam引用拖入相机对象。介绍到哪颗星就按一下Tab镜头会直接飞过去并自动LookAt整个演示流程不用碰编辑器。6.2 用协程做“启动提示文字逐渐消失”答辩节奏更顺刚打开游戏时给评委一句提示比如“按Tab切换观察目标滚轮缩放视角”三秒后文字自然消失。用协程控制文字透明度逐渐降为零using System.Collections; using UnityEngine; using UnityEngine.UI; public class IntroText : MonoBehaviour { public Text tipText; IEnumerator Start() { tipText.text 按Tab切换观察目标滚轮缩放; Color c tipText.color; c.a 1f; tipText.color c; yield return new WaitForSeconds(2f); while (c.a 0f) { c.a - Time.unscaledDeltaTime * 0.8f; tipText.color c; yield return null; } tipText.gameObject.SetActive(false); } }逻辑说明先等2秒让评委读完提示然后每帧按unscaledDeltaTime减少透明度。这里刻意用unscaledDeltaTime而不是deltaTime是为了呼应5.3节的暂停问题——即使答辩中有人按下暂停键提示文字的淡出也不会被卡住。参数说明0.8f是淡出速度觉得太快就改0.52秒停留时间按演示语速调整太长会掩盖星球运动。6.3 把OrbitLine的radius和Orbit.radius绑定别让轨道线“脱钩”答辩中如果你现场演示“把某颗星半径从7改成10”轨道线却还停留在原来的7评委一眼就能看出轨道和运动不一致。根因是两个脚本里的radius是独立字段只改一处另一处不变。解决方式是让OrbitLine自动从父物体读取Orbit的半径Orbit o GetComponentInParentOrbit(); if (o ! null) { radius o.radius; // 从Orbit脚本同步改一处即可 }放在OrbitLine的Start里加载时自动同步。这样答辩时只改Orbit的radius轨道线会跟着变不会出现“线是线、球是球”的分裂画面。答辩前我还习惯做一次完整验证把地球的period临时调到5秒让它快速转几圈确认轨道线闭合、速度均匀、起点终点重合再把半径拉到20确认轨道线跟随。验证完调回原值。这个动作五分钟能做完但能把“演示到一半发现星球脱轨”的事故消灭在答辩之前。我当年做太阳系课设最亏的是把三天时间花在给星球找贴图上没先花一小时把转速参数调成肉眼可辨直到答辩前一晚才发现地球自转也看不清。后来养成的习惯是先调运动再打磨外观希望这些思路能帮你在同一个坑前省下同样的时间。本文还有配套的精品资源点击获取
返回列表