简介:这是一套基于Unity 3D搭建的太阳系VR模型工程,面向Unity开发初学者、VR交互学习者以及天文科普展示场景,重点解决三维太阳系场景搭建、天体运动控制与头显交互的参考实现问题。压缩包共2000个文件,整体约70.58MB,以220个C#脚本、65个png纹理、50个mat材质、27个asset配置、3个prefab预设和2个unity场景为主,并包含meta、json、dll等工程与依赖文件,覆盖场景布局、脚本逻辑、模型材质和资源管理。已有1145人学习下载,说明其在Unity VR入门与课程演示中具有一定参考价值。工程内可见太阳与八大行星的独立对象组织,脚本可用于轨道运动、视角切换和行星信息查看,VR SDK接入与控制器交互也留有实现线索,头显沉浸查看与抓取、旋转、缩放等操作可据此复现。对于想拆解太阳系比例、材质渲染、预制体复用和基础性能优化的读者,这份工程能提供较完整的目录结构与可运行参考。
1. 第一次戴头显看太阳系模型,我伸手去抓土星环却抓了个空
那会儿我以为,把八颗球按比例摆出来就是太阳系 VR 模型了。真正戴上头显才发现问题:行星在眼前抖成重影,土星环离远了只剩一条细线,伸手穿过行星毫无反馈,帧率掉到四十多。基于 Unity 3D 的太阳系 VR 模型,难点从来不是建模,而是尺度压缩、性能预算、交互反馈这三件事同时成立。这份资源提供了一套能跑起来的完整场景:太阳与八大行星、主要卫星、可开关的轨道线、时间加速与倒放、射线选中的信息卡、传送移动,还有真实比例和观感比例两套切换方案。它适合想给课堂做天文演示的人,也适合拿一个结构完整的 XR 工程练手的 Unity 开发者。
2. 技术底座与尺度方案:为什么这个太阳系场景不能用真实比例直接摆
2.1 这份资源实际能做出什么效果
打开主场景,人站在一片深空背景里,脚下没有地面,只有一层淡淡的网格。太阳在原点发光,八颗行星沿各自的轨道缓慢公转,土星环带透明贴图,木星表面有云带纹理。手柄射线指向任何一颗行星,会弹出一张世界空间的信息卡,写着半径、公转周期、自转周期、卫星数量这类参数。左手柄摇杆做瞬移,右手柄扳机做射线选择,握住 grip 键可以拉近拉远整个场景尺度。
时间控制是这个场景里最实用的部分。控制面板上有几个按钮:暂停、1 天/秒、10 天/秒、100 天/秒、倒放。把速度调到 100 天/秒,水星几乎在飞,海王星才挪了一小段,行星之间的速度差一眼就能看出来,这正是课堂上最难用静态图讲清楚的东西。
场景还提供了两种显示模式。真实模式按天文数据等比缩放,行星小得几乎看不见;观感模式把半径和轨道距离分别做幂函数压缩,牺牲比例精度换取可看性。两种模式共用同一套数据和同一条更新链路,切换时只换缩放系数,不重建场景。
适用人群可以分两类。一类是教育方向的开发者,需要一套能直接投屏、能戴头显、能讲解的演示工具,改动主要集中在文案和配色。另一类是 XR 入门者,想要一个不算太大、但五脏俱全的工程去理解 XR Interaction Toolkit 的组织方式,这个场景的模块划分刚好够用又不会劝退。
2.2 技术选型:Unity LTS + OpenXR + XR Interaction Toolkit + URP
工程基于 Unity 2021.3 LTS 或 2022.3 LTS 都能开,渲染管线选 URP。选 URP 不是为了画面多漂亮,而是因为它在移动端头显上有成熟的单通道立体渲染路径,双眼只提交一次 draw call,对太阳系这种「很多小物体 + 一个天空盒」的场景收益明显。内置管线也能跑,但需要手动处理立体渲染和后期,反而更麻烦。
XR 底座用 OpenXR 插件加 XR Interaction Toolkit。OpenXR 的好处是一次构建可以在多个头显上跑,不用为每台设备单独接 SDK。XR Interaction Toolkit 提供现成的射线、抓取、传送组件,省掉大量自己写输入映射的时间。输入系统用新版 Input System,XR Interaction Toolkit 2.x 之后的版本已经默认依赖它,两套输入系统混用会出问题,这点后面避坑章节会细说。
依赖装法不复杂,可以在 Package Manager 里逐个添加,也可以直接改Packages/manifest.json:
{ "dependencies": { "com.unity.render-pipelines.universal": "14.0.11", "com.unity.xr.openxr": "1.10.0", "com.unity.xr.interaction.toolkit": "2.5.4", "com.unity.inputsystem": "1.7.0", "com.unity.textmeshpro": "3.0.6" } }版本号不是死的,2022.3 对应的 URP 是 14.x,2021.3 对应 12.x,装之前先看自己编辑器的版本。OpenXR 装完还要在 Project Settings 的 XR Plug-in Management 里勾选目标平台,安卓端勾 OpenXR 后把交互配置(Interaction Profile)加上,否则手柄按键全是死的。
2.3 尺度策略:真实数据压缩到场景单位的两条曲线
这是整个项目最先要定下来的事。太阳半径约 696000 公里,地球半径约 6371 公里,日地距离约 1.496 亿公里。如果按 1 单位 = 1000 公里摆,地球半径是 6.37 单位,日地距离是 149600 单位,场景里最远的海王星在 45 万单位外。相机的远裁剪面拉到这么大,深度缓冲精度直接崩,行星表面会出现 Z-fighting 闪烁。
所以必须压缩,而且半径和距离要用不同的压缩强度。半径压缩得轻一点,保留木星和地球的视觉差异;距离压缩得重一点,把最外层行星拉回可视范围。常见做法是幂函数 r' = k · r^p,p 取 0.4 左右,因为指数小于 1 时大数值被压得更多,小数值几乎不变,正好符合「内圈别挤在一起,外圈别跑太远」的需求。
public static class ScaleUtil { // 半径压缩:p=0.4 时,木星(69911km)与地球(6371km)的比值从 11 倍压到约 2.7 倍 public static float RadiusToUnit(float radiusKm, float k = 0.0009f, float p = 0.4f) { return k * Mathf.Pow(radiusKm, p); } // 距离压缩:p=0.75,海王星(30.07AU)从 30 个单位压到约 12.8 个单位 public static float DistanceToUnit(float au, float k = 6f, float p = 0.75f) { return k * Mathf.Pow(au, p); } }k是整体缩放系数,调大调小就是整体放大缩小;p是压缩强度,越接近 1 越接近真实比例,越小压缩越狠。我一般在工程里放一个SolarSystemConfig资源,把这两个参数暴露成可调的滑条,运行时改,看完效果再写死。
| 天体 | 真实半径(km) | 显示半径(单位) | 轨道半长轴(AU) | 显示轨道半径 | 公转周期(天) | 自转周期(小时) | 轴倾角 |
|---|---|---|---|---|---|---|---|
| 水星 | 2440 | 0.021 | 0.387 | 2.83 | 87.97 | 1407.6 | 0.03 |
| 地球 | 6371 | 0.031 | 1.000 | 6.00 | 365.26 | 23.93 | 23.44 |
| 火星 | 3390 | 0.024 | 1.524 | 8.24 | 686.98 | 24.62 | 25.19 |
| 木星 | 69911 | 0.081 | 5.203 | 21.30 | 4332.59 | 9.93 | 3.13 |
| 土星 | 58232 | 0.075 | 9.537 | 34.39 | 10759.2 | 10.66 | 26.73 |
| 海王星 | 24622 | 0.052 | 30.07 | 78.34 | 60182 | 16.11 | 28.32 |
表里的显示轨道半径用的 k=6、p=0.75,算出来海王星在 78 单位,相机远裁剪面设 500 就够,深度精度完全没问题。注意自转周期里金星是负数(逆行),数据表里我把 -5832.5 填进去,代码里取符号自动处理方向,不用单独写 if。
3. 工程结构与数据驱动:天体参数为什么不该写在代码里
3.1 目录组织与关键文件分布
工程目录按职责切,不按类型切。很多人习惯把几百个材质全丢进Materials/,结果找一个土星环材质要滚半天。这套结构是按「这个文件属于哪个模块」来分的:
Assets/ Art/ Models/Planets/ # 各行星 fbx,统一朝 +Z 方向建模 Materials/Planets/ # 每颗行星一个材质 Textures/Planets/ # 2K 以内的漫反射贴图 Skybox/ # 单张 cubemap 或 6 面图 Prefabs/ Celestial/ # 太阳、行星、卫星、轨道线预制体 UI/ # 世界空间信息卡、控制面板 Scenes/ SolarSystem_Main.unity # 完整效果,含轨道线与后期 SolarSystem_Lightweight.unity # 移动端精简版,去后处理 Scripts/ Core/ # 模拟时钟、天体驱动、系统控制器 Data/ # ScriptableObject 定义与资源 Interaction/ # 射线选中、面板跟随、传送 Rendering/ # 轨道线、星空、辉光 Settings/ URP/ # 管线资源、渲染器 XR/ # OpenXR 交互配置Scripts/Data/里放的是数据资产,Assets/Data/Bodies/下每颗天体一个.asset文件,命名跟天体同名。这样美术要改贴图换模型不用碰代码,策划要改文案直接点开资源改。
3.2 用 ScriptableObject 描述一颗天体
数据资产是这套工程的核心。把所有参数塞进 MonoBehaviour 也能跑,但一旦有二十个天体,调一个公转周期就要在场景里翻半天。用 ScriptableObject 的好处是参数集中、可复用、可以脱离场景编辑,还能在编辑器脚本里批量生成。
using UnityEngine; [CreateAssetMenu(menuName = "SolarSystem/BodyData", fileName = "NewBodyData")] public class BodyData : ScriptableObject { [Header("身份")] public string displayName; // 显示名,信息卡用 [TextArea(3, 8)] public string desc; // 一段科普文案 [Header("物理参数(真实值,未压缩)")] public float radiusKm; // 赤道半径,公里 public float semiMajorAxisAU; // 轨道半长轴,天文单位 public float orbitPeriodDays; // 公转周期,天 public float rotationPeriodHours; // 自转周期,小时,负值表示逆行 public float eccentricity; // 偏心率,0 表示正圆 public float axialTiltDeg; // 轴倾角,度 public float orbitInclinationDeg; // 轨道倾角,度 [Header("表现")] public Material surfaceMaterial; // 表面材质,换贴图改这里 public Color orbitLineColor = Color.white; public float orbitLineWidth = 0.02f; public BodyData parent; // 卫星指向它的行星,行星留空 }字段分成三块是刻意的:身份信息给 UI,物理参数给模拟逻辑,表现参数给渲染。真实值不做任何加工存进去,压缩放在运行时算,这样切到真实比例模式时直接换一个系数就行,不用维护两套数据。parent字段解决了卫星的问题——月球的天体资产里 parent 指向地球,初始化时把月球挂成地球的子节点,公转半径取日地距离的差值,不用单独写一套卫星轨道代码。
rotationPeriodHours允许负值是金星这类逆行行星的表示法,代码里算角速度时保留符号,转起来的自然就是反方向。
3.3 预制体层级:位移、倾斜、自转必须分三层
这是踩过坑才定的结构。天体预制体的层级是:
Planet_Base (根节点,挂 CelestialBody 脚本) └── TiltRoot (只负责绕 Z 轴倾斜 axialTiltDeg) └── SpinRoot (只负责绕 Y 轴自转) └── Visual (模型或球体,挂 Collider) └── RingRoot (土星环专用,可空)为什么要拆三层?如果倾斜和自转都写在一个 transform 上,Quaternion.Euler(tilt, spin, 0)这种写法会让倾斜轴跟着自转一起转,土星环会像陀螺一样摇。TiltRoot 固定不动,SpinRoot 只转 Y 轴,两个旋转互相不干扰,模型看起来才是「歪着身子自转」。
Collider 挂在 Visual 上而不是根节点,是因为根节点每一帧都在改位置,Collider 跟着移动的开销比静止物体大。Visual 相对根节点是静止的,物理引擎能把它归到静态批处理里。土星环单独挂RingRoot,用一个扁圆柱 Collider 近似,射线选中时优先命中环。
4. 核心逻辑拆解:从模拟时钟到射线选中,一条链路走完
4.1 天体驱动脚本:公转、椭圆修正与自转
每颗天体每帧要做的事其实只有三件:算轨道角、按椭圆公式算半径、把自转累加上去。核心代码不到三十行,但每一行的顺序不能乱。
using UnityEngine; public class CelestialBody : MonoBehaviour { public BodyData data; public Transform spinRoot; // 只转 Y 轴的那层 public Transform orbitCenter; // 公转中心,行星是太阳,卫星是行星 private float _orbitAngleDeg; // 当前轨道角,度 private float _spinDeg; // 当前自转角,度 private float _a; // 压缩后的轨道半长轴 private float _e; // 偏心率 void Start() { var cfg = SolarSystemConfig.Instance; _a = ScaleUtil.DistanceToUnit(data.semiMajorAxisAU, cfg.distK, cfg.distP); _e = data.eccentricity; _orbitAngleDeg = Random.Range(0f, 360f); // 初始相位打散,避免所有行星排成一条线 spinRoot.localRotation = Quaternion.Euler(0f, 0f, data.axialTiltDeg); } void Update() { float days = SimulationClock.Instance.DaysPerFrame(Time.deltaTime); // 1) 轨道角推进:周期越短转得越快,倒放时 days 为负 _orbitAngleDeg += 360f / data.orbitPeriodDays * days; // 2) 椭圆修正:r = a(1-e²)/(1+e·cosθ),e 很小时可近似为正圆 float rad = _orbitAngleDeg * Mathf.Deg2Rad; float r = _a * (1f - _e * _e) / (1f + _e * Mathf.Cos(rad)); // 3) 轨道平面内定位,Z 轴取负保证从上方看是逆时针 Vector3 localPos = new Vector3(Mathf.Cos(rad) * r, 0f, -Mathf.Sin(rad) * r); transform.position = orbitCenter.position + Quaternion.Euler(data.orbitInclinationDeg, 0f, 0f) * localPos; // 4) 自转:负周期自动反向 _spinDeg += 360f / (data.rotationPeriodHours / 24f) * days; spinRoot.localRotation = Quaternion.Euler(0f, _spinDeg, 0f) * Quaternion.Euler(0f, 0f, data.axialTiltDeg); } }逐段说。第一段算days,这是把真实时间换算成「模拟过了多少天」,由时钟统一发,天体自己不关心速度倍率。第二段是标准的椭圆极坐标公式,_e为 0 时退化成正圆,公式里的(1-e²)/(1+e·cosθ)直接等于 1,不影响性能。第三段乘了一个绕 X 轴的倾角四元数,让水星、冥王星这类轨道有倾角的天体偏离黄道面,视觉上更有层次。第四段两个四元数相乘的顺序有讲究,先转自转再转倾斜,等价于在倾斜后的坐标系里自转,跟现实一致。
Random.Range打散初始相位那行别删。第一次搭场景时我没写,所有行星从 θ=0 出发,整整齐齐排成一条直线,截图发出去被吐槽像「串糖葫芦」。
4.2 模拟时钟:一个全局倍率控制所有天体
时钟是单例,全场景只有一个,所有天体和轨道线都从它取时间增量。这样暂停、倒放、变速只要改一个值。
using UnityEngine; public class SimulationClock : MonoBehaviour { public static SimulationClock Instance { get; private set; } [Range(-500f, 500f)] public float daysPerSecond = 1f; public bool paused; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } // 返回这一帧模拟了多少天,倒放时返回负值 public float DaysPerFrame(float deltaTime) { if (paused) return 0f; return daysPerSecond * deltaTime; } public void SetSpeed(float v) => daysPerSecond = v; public void TogglePause() => paused = !paused; }daysPerSecond允许负值是倒放的关键,UI 上的「倒放」按钮只是调SetSpeed(-10f),不用额外写一套反向逻辑,天体代码里也没有任何 if 判断方向。默认 1 天/秒,地球一年转完需要六分钟,合适;课堂上讲内行星差异时调到 30 天/秒。
这个类有个小细节:Awake里做了重复实例销毁。场景里如果有两个时钟,Instance会指向后创建的那个,先创建的天体拿到的是旧引用,速度调节失效。加了判重之后,至少不会静默出错。
4.3 VR 交互:射线选中与信息面板跟随
交互部分基于 XR Interaction Toolkit。行星的 Collider 上挂一个继承自XRSimpleInteractable的组件,选中时把数据交给面板。
using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; public class PlanetSelectable : XRSimpleInteractable { public BodyData data; public InfoPanelController panel; protected override void OnSelectEntered(SelectEnterEventArgs args) { base.OnSelectEntered(args); // 把被选中的天体 transform 传进去,面板每帧跟着它 panel.Show(data, transform); } }XRSimpleInteractable不处理抓取物理,只发选中事件,正好符合「点一下看信息」的需求。如果换成XRGrabInteractable,行星会被握住拖动,整个轨道系统就乱了。这里用OnSelectEntered而不是OnHoverEntered,是因为悬停事件在快速扫过时会疯狂触发,面板会闪。
面板跟随时不要挂在行星下面当子物体。跟着行星转会让人晕,正确做法是面板始终在用户前方固定距离,朝用户,只是内容换掉:
using UnityEngine; public class InfoPanelFollower : MonoBehaviour { public Transform head; // 主相机 public float distance = 1.4f; // 面板到眼睛的距离,1.2~1.6 之间比较舒服 public float heightOffset = -0.15f; // 略微下移,视线不用抬太高 public float followSpeed = 6f; // 跟随速度,太快会跟手抖,太慢会拖影 void LateUpdate() { Vector3 target = head.position + head.forward * distance + Vector3.up * heightOffset; transform.position = Vector3.Lerp(transform.position, target, Time.deltaTime * followSpeed); // 只做水平朝向,避免面板仰俯跟着头部倾角乱翻 Vector3 dir = transform.position - head.position; dir.y = 0f; if (dir.sqrMagnitude > 0.001f) transform.rotation = Quaternion.LookRotation(dir); } }放在LateUpdate里很关键,Update阶段头显位置可能还没更新完,面板会出现一帧的错位。heightOffset设成负值让面板略低于视线中心,看文字时眼球不用频繁上下调焦。dir.y = 0那两行保证面板只在水平面上朝向用户,不会因为用户低头而仰起来。
4.4 性能预算:90 帧下每帧只有 11 毫秒
移动端头显讲 90 帧,一帧 11.1 毫秒,扣掉系统开销和合成时间,留给渲染的大概 8 毫秒。太阳系场景看着简单,实际很容易超:八颗行星带卫星十几个体,每个都有独立材质,天空盒一张大 cubemap,轨道线是 LineRenderer,信息卡是世界空间 Canvas 带文字。
几条硬性设置。URP 渲染器里开 Single Pass Instanced,双眼一次提交;MSAA 开 4x 就够,再高收益递减;实时阴影全关,太阳系不需要,行星之间的互相遮挡用光照贴图或者干脆不投影;后处理只保留一点 Bloom 给太阳,其他全关;贴图上限 2K,行星表面用 1K 就足够,没人会贴到表面看纹理。
天空盒是大头。6 面 4K 的 cubemap 在移动端能吃满带宽,换成 6 面 2K 或者一张 Equirectangular 转到天空盒,帧率能回来三到五帧。轨道线用 LineRenderer 时把positionCount控制在 128 左右,段数太多反而看不出圆滑,还增加顶点数。
5. 避坑与排查:这几个问题我调了两天才定位
5.1 行星表面闪烁、边缘有锯齿状抖动
现象:场景跑起来,尤其是切换到真实比例模式后,行星边缘出现闪烁的黑边,转动视角时表面贴图有细密的网格状抖动,越远越明显。
原因基本是深度缓冲精度不够。相机的近裁剪面设了 0.01,远裁剪面设了 20000,两者比例达到两百万比一,24 位深度缓冲在这样的范围内根本无法区分远处的相邻像素。VR 双眼渲染还会进一步放大这个问题,因为左右眼的投影矩阵略有差异。
解决方法是先把近裁剪面提到 0.05,别小看这 0.04,比例直接降了五倍。然后压缩场景尺度,ScaleUtil.DistanceToUnit的 k 从 6 调到 3,海王星从 78 单位缩到 39,远裁剪面设 500 就够。实在需要大场景,就分两层渲染,近处行星用一个相机,远处星空用一个只画天空盒的相机,两层各自设置裁剪面。VR 里手离脸很近,近裁剪面不能超过 0.1,否则手柄模型会被裁掉。
5.2 手柄射线能指到行星,但按扳机没反应
现象:射线有高亮,准星也变颜色了,扣扳机什么都没发生,信息卡不弹。
先看 Collider。行星的 Collider 挂在Visual节点上,但PlanetSelectable脚本挂在根节点,XR Interaction Toolkit 找的是「射线命中的 Collider 所在物体及其父级」上的 Interactable,理论上能找到。真正出问题的是 Layer:XR Ray Interactor 的Raycast Mask默认只勾了 Default 层,而美术把行星模型导进来时 Layer 全设成了 Ignore Raycast,射线根本打不到。
第二个常见原因是交互层配置。XR Interaction Manager 里的 Interaction Layer Mask 和 Interactable 的 Layer Mask 必须匹配,两个都设成 Everything 时能通,但只要有一边被改过,选中就静默失败,控制台一个字都不报。我现在的习惯是新建一个Interactable层,行星、按钮、控制面板全放这层,射线和 Interactable 两边都只勾这一层,出问题一眼就能看出来。
还有一种情况是脚本没生效:PlanetSelectable继承自XRSimpleInteractable,如果工程里同时装了旧版 XR Toolkit 的包,命名空间会冲突,编译能过但运行时组件识别不到。检查 Package Manager 里只有一个版本的 XR Interaction Toolkit。
5.3 信息卡文字糊成一团,靠近才清楚
现象:面板拉到眼前看,字是虚的,边缘有彩边,稍微偏一点角度更明显。
世界空间 Canvas 的缩放是最容易设错的地方。Canvas 的Scale设成 1 时,一个 800×600 的 Canvas 在场景里就是 800 米宽,字被放大到几层楼高,摄像机贴到 1.4 米看,当然糊。正确做法是把 Canvas 的 RectTransform 缩到 0.001 左右(即 1 单位 = 1 毫米),字体 Size 设 32 到 48,这样面板实际宽度 0.8 米,文字大小合理。
TextMeshPro 的字体图集分辨率也要跟上。默认图集是 512×512,中文字符集一多,每个字的位图被压得很小,放大后就是糊的。把图集改成 1024×1024,字符集选「常用汉字 3500」,重新生成。另外关掉材质的Softness,世界空间文字不需要抗锯齿柔化,开了反而发灰。
还有一点,别用 Canvas 的World Space加Screen Space - Camera混着来。信息卡必须纯粹的世界空间,混用会导致排序错乱,面板被行星挡住或者穿透显示。
5.4 编辑器里 90 帧,打包到头显只有 45 帧
现象:Editor 的 Game 视图跑得很顺,Build 出来装到头显,帧率腰斩,转动头部有明显的拖影和延迟感。
第一个要查的是图形 API。安卓端如果同时勾了 Vulkan 和 OpenGL ES 3,Unity 会优先用 Vulkan,某些机型上 Vulkan 驱动对单通道立体渲染支持不好,帧率反而低。我一般的做法是先只留 OpenGL ES 3 跑一遍对比,哪个稳用哪个,不要想当然认为 Vulkan 一定快。
第二个是渲染分辨率。URP 的 XR 设置里有个Render Scale,默认 1.0,但有些头显的默认分辨率是超采样的,实际渲染分辨率比屏幕高不少。把 Render Scale 调到 0.9,画质肉眼看不出差别,帧率能回来十帧。
第三个是后处理和阴影。编辑器里勾着实时阴影看不出来,打包后每帧多一次 shadow map 渲染,二十多个天体每个都投射,开销直接翻倍。太阳系场景把Shadows全关,太阳的辉光用一个 Billboard 面片加加法混合材质伪造,比真光源便宜得多。
5.5 真机数据变了但场景没变,改了资产不生效
现象:把某颗行星的公转周期从 365 改成 687,重新运行,行星转得跟原来一模一样。
这是 ScriptableObject 的经典坑。BodyData是资产,场景里的CelestialBody通过 Inspector 引用它。如果改了资产里的值,但场景里的组件在Start里已经把数值缓存到私有字段(比如_a和_e),改资产不会触发重新初始化。我遇到过更隐蔽的情况:场景里存的是资产的副本而不是引用,改资产完全不影响场景。
排查方式是运行时选中天体,看 Inspector 里data字段是不是 None 或者指向了别的资源。如果是 None,说明序列化引用丢了,通常发生在重命名或移动资产文件之后,需要重新拖一次。
彻底的做法是在OnValidate里加一段重建逻辑,改资产时立即刷新场景:
#if UNITY_EDITOR void OnValidate() { if (data == null || spinRoot == null) return; _a = ScaleUtil.DistanceToUnit(data.semiMajorAxisAU, SolarSystemConfig.Instance.distK, SolarSystemConfig.Instance.distP); _e = data.eccentricity; spinRoot.localRotation = Quaternion.Euler(0f, 0f, data.axialTiltDeg); } #endifOnValidate只在编辑器里跑,不影响打包体积。注意里面不能调Start里的逻辑,SolarSystemConfig.Instance在编辑器里可能还没初始化,要加空判断,否则改资产时控制台刷一堆空引用。
6. 二次开发:加一颗行星、换一套坐标、做一个课堂模式
想加一颗天体,流程不复杂。先在Assets/Data/Bodies/右键创建 BodyData,填真实参数;然后把Planet_Base预制体拖进场景,把资源和spinRoot、orbitCenter拖到组件上;最后把轨道线的材质颜色改掉,轨道线组件会读orbitLineColor。整个过程不用改一行代码,这也是当初把数据抽出来的原因。
如果要加的是卫星,注意parent字段。卫星的orbitCenter不是太阳而是母行星的 transform,轨道半径不能直接用semiMajorAxisAU的绝对值,因为月球的 0.00257 AU 压缩后只有 0.04 单位,几乎贴在行星表面。我一般的处理是给卫星单独一个orbitRadiusUnit字段,直接填场景单位,绕开压缩公式。
换一套坐标是最容易翻车的地方。有人想把 Y 轴向上改成 Z 轴向上,直接改localPos的轴顺序,结果所有轨道倾角全乱。正确做法是保留内部计算在 XZ 平面,在根节点加一层空物体做整体旋转,这样倾角、偏心率、自转全都不用动。我知道这个是因为第一次改的时候把Mathf.Cos(rad) * r塞进了 Y 轴,行星全飞成竖直的圈,调了半小时才反应过来。
课堂模式是我觉得最值得加的东西。核心是三个按钮:预设视角(俯视黄道面、侧视、跟随某颗行星)、时间预设(1 天/秒、30 天/秒、暂停)、一键复位。实现上不用改天体逻辑,只写一个ClassroomModeController,调用SimulationClock.SetSpeed和相机的transform.SetPositionAndRotation就行。要注意相机在 XR 里不能直接设位置,得移动 XR Origin 的父节点,直接改 Camera 的 transform 会被头显追踪覆盖,画面纹丝不动,这个坑我踩过不止一次。
验证改动的习惯是:先跑SolarSystem_Lightweight场景,用 45 帧的目标看会不会掉帧,再切到主场景看效果。轻量场景里如果都跑不动,主场景肯定更不行。从那以后我每次改完天体参数,都会先在这个精简场景里跑一遍 100 天/秒的高速模拟,看有没有行星穿模或者轨道线错位,再打包上真机。希望这些能帮你少走两步弯路。
本文还有配套的精品资源,点击获取