简介:一个基于Unity的模拟太阳系小游戏,来自作者课程实践项目。玩家可操控飞船在太空中穿梭,自由探索太阳系中的星球。游戏设计了两种驾驶模式:一种偏向真实物理,通过不同方向施加力来驱动飞船,操作较具挑战;另一种是常见灵活移动模式,飞船在太空行动更加轻巧自如。靠近星球后能查看对应介绍,太阳特效采用Unity官方URP渲染管线制作,视觉表现更佳。压缩包约246.69MB,含约2000个文件,主要包含C#脚本、FBX模型、材质贴图、Shader着色器以及UI配置文件等,目录结构完整,便于直接打开和参考。目前已有506人学习,适合Unity初中级学习者和课程设计者,可从中学习URP渲染、飞船控制、行星场景搭建及交互界面设计等实用技巧。
1. Unity模拟太阳系:一个课设如何把双驾驶和URP太阳都装进去
做课设最怕的不是没思路,而是方案太多,最后调出来的东西自己都不想开第二遍。模拟太阳系这个题目看着简单——摆几个球体转起来就行,但真正把它做得像样的,得同时解决两件事:行星公转自转的节奏感,以及飞船在太空里飞行的操控手感。这个unity3d课程设计项目把这两条线都做出来了:飞船有真实物理和简易操控两种驾驶模式,太阳用的是Unity官方渲染管线,带自发光和光晕效果。代码量不大,但天体运动、飞行手感、UI提示全串在了一起,适合直接当课设模板,也适合想做行星模拟但不想从零开始的从业者。
2. 天体系统搭建:公转自转脚本、轨道可视化与参数比例
2.1 场景层级与Transform管理:先搭结构再写逻辑
打开Unity的第一步不是急着写脚本,先把Hierarchy的整体结构搭出来。我做这个项目的时候习惯这样分:一个SolarSystem空物体当总容器,下面挂Sun、Planets、Spaceship三个子节点。Sun节点上再挂一个平行光作为整个场景的主光源,Planets下挂Earth、Mars等行星,月球挂在Earth的子节点下。
为什么这么分?核心原因是RotateAround这类旋转API操作的是世界坐标,公转脚本本身不依赖父子关系,但父节点一旦被移动或者缩放,子节点的世界坐标会跟着变。把行星直接挂成独立节点,飞船单独放一组,后面做驾驶模式时就不会出现飞船被容器拖着走的尴尬——这个坑我在第一次搭场景时踩过,飞船飞了半天,发现整个场景容器在动。
搭完层级还需要确认Scale。太阳系场景里最容易犯的错误是直接按真实比例缩放星球尺寸:木星半径是地球的11倍,真要按这个比例摆,地球在场景里只有一个小点,视角切过去根本看不清。课设阶段就用视觉比例,太阳半径5左右,地球0.5,木星2,距离再拉开,这样摄像机在近处能看到细节,拉远能看到轨道全貌。
2.2 公转与自转脚本:参数化实现与单位说明
天体运动的脚本本质就是一句话:绕中心旋转加自身旋转。下面这个CelestialBody类是我在项目里用的版本,同一个脚本挂给太阳以外的所有行星、月球:
using UnityEngine; public class CelestialBody : MonoBehaviour { [Header("运动参数")] public Transform center; // 公转中心,一般是太阳 public float orbitSpeed = 10f; // 公转角速度,单位:度/秒 public float rotateSpeed = 5f; // 自转角速度,单位:度/秒 public Vector3 orbitAxis = Vector3.up; // 轨道轴,想加倾角时改这里 void Update() { if (center != null) { // 绕center的世界坐标旋转,时间乘速度保证帧率无关 transform.RotateAround(center.position, orbitAxis, orbitSpeed * Time.deltaTime); } // 自转用本地Y轴 transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime); } }注意orbitSpeed的单位是「度/秒」,不是弧度也不是转数。假设想让地球在60秒完成一圈公转,orbitSpeed填6——因为360度除以60秒等于6。真实太阳系里地球一年转360度,但模拟项目如果按真实比例,观众要等一年才看到地球转完一圈,完全没法看,所以一般会把速度放大几十倍。这里我习惯把公转和自转写进同一个脚本,因为它们的生命周期完全一致,拆成两个脚本反而要管理两份组件。
我实际调参数时用的是下面这组数值,视觉优先,不追求真实物理比例:
| 天体 | 轨道半径(Unity单位) | 公转速度(度/秒) | 自转速度(度/秒) |
|---|---|---|---|
| 水星 | 90 | 20 | 8 |
| 金星 | 120 | 15 | 5 |
| 地球 | 150 | 10 | 5 |
| 火星 | 180 | 8 | 6 |
| 木星 | 230 | 5 | 12 |
| 土星 | 280 | 4 | 10 |
| 月球(绕地球) | 15 | 30 | 1 |
月球这个特殊情况要单独说:月球挂在Earth的子节点下,但Revolution脚本的center字段填的是Earth本身,而不是太阳。这样月球跟着地球绕太阳走,同时自己又绕地球转,轨迹会形成一条摆线,视觉上完全正确。如果center误填成太阳,月球会被地球拖住,绕行半径变成15+150,看起来就像月球在绕一个假想的中心点转圈。
2.3 轨道倾角:看起来真实,做起来麻烦
全部行星都用Vector3.up当轨道轴,意味着所有轨道都在同一水平面上,这确实不够还原。真实太阳系里水星轨道倾角7度,金星3.4度,冥王星17度,但把这个做进课设会带来两个麻烦。
首先轨道会交叉。行星在各自轨道上不同速度运行,倾角不同会导致某些时刻两行星几乎重叠,甚至互相穿模。其次,后续用LineRenderer画轨道线时,倾角需要额外的三维坐标变换,不再是简单画一个水平圆。我的建议很直接:课设阶段不做倾角,把orbitAxis保持默认。等飞船驾驶、星球信息、URP太阳全部做完,还有余力再说加倾角的事。想试的话把orbitAxis改成(new Vector3(0.1f, 1f, 0)).normalized这种形式,观察行星轨道如何从平面变成空间曲线,但别指望它好看。
2.4 轨道可视化:LineRenderer画圆,别挂在行星节点下
轨道线的目的是让玩家在远处一眼看出行星运行范围,做得太细反而抢戏。用LineRenderer画一个水平圆就够了,但有一个关键点:轨道绘制组件必须挂在一个独立的空物体上,不要挂在行星子节点下面。
using UnityEngine; [RequireComponent(typeof(LineRenderer))] public class OrbitDrawer : MonoBehaviour { public Transform sunTransform; public Transform planetTransform; public int segments = 128; public float heightOffset = 0f; void Start() { LineRenderer line = GetComponent<LineRenderer>(); line.positionCount = segments + 1; line.loop = false; line.startWidth = 0.02f; line.endWidth = 0.02f; float radius = Vector3.Distance(sunTransform.position, planetTransform.position); for (int i = 0; i <= segments; i++) { float angle = (360f / segments) * i * Mathf.Deg2Rad; Vector3 pos = new Vector3(Mathf.Cos(angle) * radius, heightOffset, Mathf.Sin(angle) * radius); pos += sunTransform.position; line.SetPosition(i, pos); } } }这里用Start而不是Update,是因为行星的轨道半径和位置在场景初始化后基本固定,每帧重新生成128个顶点纯属浪费。如果一定要做动态轨道——比如玩家可以把行星拖走——那也要只在行星位置变化时才重算,不能无脑每帧刷。
heightOffset设成0,让轨道线和行星在同一平面。注意pos在赋值前加上了sunTransform.position,把圆的圆心从世界原点移到太阳的位置。如果太阳在场景里的位置不是原点(比如你把它摆到了(50, 0, -30)),这行代码是必要的。
另外一个常见误区是LineRenderer挂在行星子节点下,然后行星一公转,轨道线也跟着转,玩家看到的轨道就成了螺旋线。轨道线应该存在于世界空间,独立于行星运动,所以挂在场景层级最外层最安全。
3. 飞船双驾驶模式:真实物理与简易操控的切换实现
3.1 模式A:Rigidbody力驱动——有惯性,像开船
先看物理模式。这个模式的出发点是用Rigidbody和AddForce模拟真实的太空惯性:给一个力,飞船沿受力方向加速,松开按键后继续滑行,想要变向得朝反方向给推力。手感上很像在没有空气阻力的空间里操作一艘船,滑行距离长、转向迟钝是正常现象,也正是这个模式的区分度所在。
using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class ShipPhysics : MonoBehaviour { [Header("推力参数")] public float thrustPower = 30f; // 推力大小,决定加速度 public float yawSpeed = 60f; // 鼠标转向速度,单位:度/秒 [Header("质量与阻尼")] public float mass = 1f; public float drag = 0.5f; // 阻力,太空里没有,但为了手感可以加 private new Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); rb.mass = mass; rb.drag = drag; rb.useGravity = false; rb.interpolation = RigidbodyInterpolation.Interpolate; // 减少卡顿感 } void Update() { float mouseX = Input.GetAxis("Mouse X") * yawSpeed * Time.deltaTime; float mouseY = Input.GetAxis("Mouse Y") * yawSpeed * Time.deltaTime; transform.Rotate(new Vector3(-mouseY, mouseX, 0f), Space.Self); } void FixedUpdate() { Vector3 input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if (input.magnitude < 0.1f) return; Vector3 force = transform.TransformDirection(input) * thrustPower * Time.fixedDeltaTime; rb.AddForce(force, ForceMode.Acceleration); } }三个细节需要重点说明。第一,输入读取和力施加放在FixedUpdate,而不是Update。FixedUpdate的调用频率是固定的(默认每秒50次),物理引擎只有在FixedUpdate里施加力才能真正作用于Rigidbody,在Update里AddForce会出现间歇性的跳帧感,有时按一下键飞船抖一下,就是这个原因。
第二,鼠标转向放在Update里处理,因为它不涉及物理计算,直接旋转Transform每帧响应更顺滑。有人习惯把所有控制都塞进FixedUpdate,结果鼠标转动速度在CPU负载高的时候变慢,这就是把渲染逻辑和物理逻辑混在一起的结果。
第三,ForceMode.Acceleration表示不受质量影响,质量不管设多少,加速度都等于thrustPower。如果想模拟不同质量飞船的不同响应,应该用ForceMode.Force,这时飞船越重,同等推力下加速度越小。我一般把质量固定在1,用thrustPower调手感,简单直接。
drag参数是给这个模式找平衡用的。真实太空里没有阻力,但drag设为0时飞船会永远匀速直线飞下去,玩家稍微偏一个角度就再也回不来。drag调到0.5,松开按键后飞船逐渐减速,既保留了惯性感又不至于失控。
3.2 模式B:Transform位移驱动——身轻如燕,指哪打哪
简易模式走的是另一条思路:不碰物理引擎,直接在Update里改Transform位置。玩家按前就往前飞,不按就停,没有惯性,转向立即生效。手感上接近街机飞行游戏,好处是任何人都能上手。
using UnityEngine; public class ShipSimple : MonoBehaviour { [Header("平移速度")] public float moveSpeed = 30f; public float boostMultiplier = 2f; [Header("转向")] public float yawSpeed = 90f; public float pitchSpeed = 60f; void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float boost = Input.GetKey(KeyCode.LeftShift) ? boostMultiplier : 1f; // 沿飞船本地坐标系平移,速度乘以deltaTime保证帧率无关 Vector3 move = (transform.forward * v + transform.right * h) * moveSpeed * boost * Time.deltaTime; transform.Translate(move, Space.Self); // 鼠标控制俯仰和偏航,注意Y轴要反向 float mouseX = Input.GetAxis("Mouse X"); float mouseY = Input.GetAxis("Mouse Y"); transform.Rotate(Vector3.up, mouseX * yawSpeed * Time.deltaTime, Space.Self); transform.Rotate(Vector3.right, -mouseY * pitchSpeed * Time.deltaTime, Space.Self); } }这里用Translate的Space.Self参数,意味着前后移动方向是飞船自己的forward和right,而不是世界坐标轴。如果把Space.Self换成Space.World,飞船朝一个方向飞,按下右键时却往世界坐标的X轴方向平移,玩家会立刻晕。很多新手在这个地方翻车,检查了半天发现只是少了一个参数。
左Shift加速是个细节但很实用。在简易模式里,加速键让moveSpeed从30变成60,降低了“找目标行星太远、飞半天”的烦躁感。两个速度都能在Inspector里调,不用改代码。
一个补充技巧:在简易模式下强制清零Z轴旋转。鼠标乱晃之后飞船容易出现绕Z轴的翻滚,画面开始倾斜,玩家会误以为视角坏了。在Update末尾加几行矫正逻辑:
Vector3 euler = transform.eulerAngles; euler.z = 0f; transform.eulerAngles = euler;这样飞船永远保持水平,操作逻辑更接近“无人机”而不是“战斗机”。
3.3 切换逻辑与手感补偿:不互相拖累
两种模式做成两个独立脚本同时挂在飞船上,用Tab键切换启用状态,这是最干净的做法。不要在同一个脚本里写两个if分支再禁用变量,时间一长自己都容易搞混。
using UnityEngine; public class ShipModeSwitcher : MonoBehaviour { public ShipPhysics physicsMode; public ShipSimple simpleMode; void Update() { if (Input.GetKeyDown(KeyCode.Tab)) { bool isPhysics = physicsMode.enabled; physicsMode.enabled = !isPhysics; simpleMode.enabled = isPhysics; } } }切换的瞬间注意一件事:从简易模式切换到物理模式时,Rigidbody的velocity默认是0,飞机会突然“刹车”。保证手感的做法是在切过去之前,把简易模式当前的速度换算成Rigidbody的初始速度,或者干脆在切回物理模式的同时保留原有velocity。我一般直接在ShipPhysics的OnEnable里保留当前rb.velocity历史值。
如果你把两种模式都跑过一遍,会发现手感差异的核心在“加速度曲线”。物理模式的响应是曲线——起步慢、后期快,简易模式是直线——输入多少就走多少。为了让物理模式更亲民,可以在FixedUpdate里加上限速逻辑,避免飞船被连续按键越推越快:
float maxSpeed = 50f; Vector3 vel = rb.velocity; vel = Vector3.ClampMagnitude(vel, maxSpeed); rb.velocity = vel;这段代码破坏了纯物理的真实性,但课设演示时观众体验更好——飞船不会因为推了片刻锂电就停不下来了。想追求真实物理的朋友可以去掉,演示效果优先的话建议保留。
4. 靠近星球看信息:公告板、UI面板与数据挂载
4.1 行星数据挂载:直接用MonoBehaviour,别上ScriptableObject
星球介绍信息是这个项目的互动峰值——玩家靠近某颗星,屏幕上弹出它的名字和简介。数据本身很简单:名字、类型、一句话描述、几个关键数字。用ScriptableObject是过度设计,数据不需要跨场景复用,也不需要外部持久化,一个挂在行星上的普通MonoBehaviour就够。
using UnityEngine; public class PlanetInfo : MonoBehaviour { [Header("基础信息")] public string planetName; [TextArea(3, 6)] public string shortDescription; // 简短介绍,UI上展示 [Header("物理数据")] public string massText; // 例如 "5.97 × 10^24 kg" public string diameterText; // 例如 "12,742 km" public float distanceFromSun; // 仅用于排序或调试显示 [Header("更多内容")] [TextArea(5, 10)] public string detailDescription; // 后续想展开聊的完整介绍 }planName和shortDescription直接手填在Inspector,比写配置文件再读取更直观。TextArea属性让Inspector里的输入框变大,多行文本不用挤在单行里编辑。massText和diameterText用字符串而不是float,因为科学计数法的数字在实数和显示格式之间来回转容易出错,直接按展示格式存字符串最省事。
detailDescription字段留给想做扩展的人,比如把面板做成可展开的详情页,一段完整的行星背景介绍放在这里,后面代码里读取填充。
4.2 公告板名字与靠近触发:射线检测不如碰撞触发稳定
行星头顶要显示名字,用3D Text或者世界空间Canvas都行,但都需要一个Billboard脚本来让文本始终面向相机。注意LookAt有个镜像问题,直接用transform.LookAt(cameraTransform)时文字会反过来,正确做法是让它朝相反方向看:
using UnityEngine; public class Billboard : MonoBehaviour { private Transform mainCam; void Start() { mainCam = Camera.main.transform; } void LateUpdate() { Vector3 direction = transform.position - mainCam.position; transform.rotation = Quaternion.LookRotation(direction); } }LateUpdate在整个帧的动画和旋转计算完成后执行,这样可以避免Billboard和相机同时移动时出现的抖动。
“靠近星球后查看信息”的触发,推荐用SphereCollider加Trigger,而不是每帧去检测玩家与行星的距离并把结果写给UI。原因很简单:Unity的碰撞系统本身就在做距离检测,你不需要再写一遍。给每颗行星挂一个SphereCollider,isTrigger为true,半径覆盖“靠近”的判定范围。当飞船的Rigidbody碰撞体进入这个球体时,OnTriggerEnter被调用:
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { UIManager.Instance.ShowPlanetPanel(GetComponent<PlanetInfo>()); } } private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) { UIManager.Instance.HidePlanetPanel(); } }记得把飞船的Tag设置为Player,否则CompareTag校验不过。这个方案的坑在于:飞船在简易模式下是Transform.Translate,没有直接操作Rigidbody,但碰撞体的触发检测依然依赖Rigidbody存在。所以简易模式下也要给飞船挂Rigidbody,并且设为isKinematic,保证碰撞检测正常工作。
4.3 UI信息面板:Canvas配置与文本刷新
UI面板我用Screen Space Overlay模式,因为它不用处理相机深度和坐标系,最稳。在Canvas下挂一个Panel作为信息展示区,默认SetActive(false),需要时再打开。
CanvasScaler的配置会影响不同分辨率下的显示效果:当UI缩放模式设为ScaleWithScreenSize,Reference Resolution填1920 x 1080,Match设为0.5,这样在竖屏和宽屏缩放时,文字不会被拉伸变形。这是我做过的项目里最通用的配置。
面板内容物很简单:一个Text显示名字,一个Text显示简介,一个Text显示物理数据。初次打开时把它们一次性全部填充,不要做逐帧更新的逻辑:
using UnityEngine; using UnityEngine.UI; public class PlanetUIPanel : MonoBehaviour { public Text nameText; public Text descText; public Text detailText; public void Show(PlanetInfo info) { nameText.text = info.planetName; descText.text = info.shortDescription; detailText.text = $"{info.massText}\n{info.diameterText}\n{info.detailDescription}"; gameObject.SetActive(true); } public void Hide() { gameObject.SetActive(false); } }把UI的Text赋值放进Show方法而不是Update里,是为了避免每帧都做字符串拼接和文本重建。字符串拼接在Update里跑,GC Alloc会一直涨,Profiler里看得到内存块不断分配——这是做Unity性能优化的基本功。Show方法只在进出Trigger时被调用,文本内容不变,GC压力几乎为零。
5. 避坑与常见问题:坐标、帧率与URP渲染的六个现场
5.1 脚本逻辑:公转中心错位、方向反转、物理穿透
现象一:行星转到一半,位置突然跳变或者公转半径越来越大。 原因:公转中心center引用的物体本身在移动,比如太阳节点被挂到了某个空容器下,而容器在动画控制。RotateAround每帧拿的是center的世界坐标,中心一动,行星轨迹就散开。 解决:在Start里把中心世界坐标缓存成Vector3字段,或者干脆保证center物体在场景里保持静态。模拟太阳系的太阳不应当被任何脚本移动,这点在上层设计时就要想清楚。
现象二:物理模式下按W,飞船没有朝forward方向飞,而是朝一个奇怪的方向滑。 原因:飞船模型的默认前方和transform.forward方向不一致。很多模型在建模软件里朝向+X或-Z,导入Unity后forward实际指向的是模型的前方,而不是你定义的“机头”。 解决:用一个空的子节点作为飞船的“驾驶参考点”,把这个子节点的forward对准模型机头,物理和简易模式的移动逻辑都基于这个子节点而不是飞船根节点。调试时先画一条Debug.DrawRay(transform.position, transform.forward * 10, Color.red)看方向对不对。
现象三:切换UNITY到物理模式后,飞船穿过了行星的碰撞体。 原因:Rigidbody开启了useGravity,行星碰撞体是isTrigger,飞船在高速下没触发OnTriggerEnter。 解决:把行星的碰撞体保持为普通Collider,飞船Rigidbody单独处理。速度过高时Unity的连续碰撞检测(Continuous Speculative)可以开启,代码里rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;,这样高速飞行时的穿模概率会明显下降。物理模式的限速逻辑也是给这个坑兜底。
5.2 渲染表现:URP材质丢失、Bloom不出效果、搜错关键词
现象一:项目从默认管线切到URP后,所有行星和飞船变成紫色。 原因:Built-in管线的Standard Shader在URP下不被识别,材质失去了着色器引用,就显示为紫红色。 解决:在编辑器菜单栏执行Edit > Rendering > Materials > Convert Selected Built-in Materials to URP,把场景里选中的材质统一转换。如果转换后还有漏网之鱼,手动新建URP/Lit材质重新拖上去。
现象二:太阳的自发光效果做了,但场景里没有光晕,Bloom也看不出效果。 原因一:URP里Bloom是后处理,需要在场景里有一个Global Volume,并且把Bloom组件激活。很多新手只调了材质Emission,没加Volume,后处理根本没被加载。原因二:Bloom的Threshold默认是0.9,太阳的自发光如果只给了1.0强度,亮度刚好没过阈值,Bloom几乎不可见。 解决:加Global Volume组件,添加Bloom Override,把Threshold降到0.5左右,Intensity调到1.2以上,太阳看起来才有“泛光”的感觉。材质侧把Emission Color调成橙黄色(255, 160, 60),Emission Intensity给到2~3,并使用HDR颜色值。
现象三:在网上搜索“Unity UBR 渲染管线”找不到任何有效的官方文档。 原因:这个项目的原始介绍写的是“UBR渲染管线”,其实Unity官方没有UBR这个缩写,正确名称是URP(Universal Render Pipeline)。UBR很可能是笔误,或者当初按发音转写时疏忽了。按UBR去搜,搜到的全是不相关的内容。 解决:搜索时统一用“URP”或者“Universal Render Pipeline”。这个坑看起来玄学,实际上就是关键词错误导致的检索失败。想确认管线版本,看Project Settings > Graphics里的Render Pipeline Asset路径即可。
5.3 发布与UI:打包后太阳不发光、文字模糊
现象一:编辑器里一切正常,打出Windows包后太阳变黑或者场景变暗。 原因:URP的Asset没有在Quality设置里每一个Level都指定。默认情况下,Quality层级各自维护一套渲染参数,如果你只在某个Level里配了URP Asset,其他Level打包后会丢失。 解决:打开Project Settings > Quality,把所有Level的Render Pipeline Asset都指向同一份URP Asset文件。注意新版本的Unity把渲染设置迁移到了URP全局设置里,但老项目的Quality页签仍然保留覆盖项,两边都要检查。
现象二:打包后UI文字边缘模糊,编辑器里却清晰。 原因:CanvasScaler没有配置ScaleWithScreenSize,UI在低分辨率下被缩放拉伸。 解决:选中最外层Canvas,挂上CanvasScaler后把UIScale Mode改成ScaleWithScreenSize,Reference Resolution设为1920 x 1080,Match设为0.5。这样打包后的UI在不同分辨率下会按比例缩放而非拉伸像素。
现象三:飞船靠近行星时UI频繁闪烁。 原因:OnTriggerEnter和OnTriggerExit在碰撞边界来回抖动,面板不断Show和Hide。 解决:在UIManager或PlanetUIPanel的Show方法里加一个判断——如果当前显示的星球和本次要显示的是同一个,直接return,不做重复SetActive。代码三行:
private string currentPlanet; public void Show(PlanetInfo info, string caller) { if (currentPlanet == info.planetName) return; currentPlanet = info.planetName; // 后续填充逻辑 }6. 收尾验证:从帧率曲线到发布参数的几件小事
6.1 性能验证:不要靠肉眼判断流畅度
模拟太阳系这种场景,卡顿一般不是渲染问题,而是脚本里每帧生成顶点或字符串导致的GC压力。我习惯在项目里挂一个简单的帧率显示脚本,直接放在飞船上,运行时就看得见:
using UnityEngine; using System.Text; public class FpsMeter : MonoBehaviour { private float fps; private float timer; private GUIStyle style = new GUIStyle(); void Update() { timer += Time.unscaledDeltaTime; if (timer >= 0.5f) { fps = 1f / Time.unscaledDeltaTime; timer = 0f; } } void OnGUI() { style.fontSize = 40; style.normal.textColor = Color.green; GUI.Label(new Rect(10, 10, 200, 50), "FPS: " + Mathf.RoundToInt(fps), style); } }跑起来如果FPS稳定在60附近,基本可以发布。如果掉到30以下,打开Profiler的CPU Usage,看哪个脚本的Update函数耗时最多。最常见的就是OrbitDrawer被误写成每帧重算128个顶点,或者UI文本在Update里反复set text。按这个路径排,卡顿的根源最多十分钟就能定位。
6.2 发布参数:Quality、URP与Camera的收尾选项
打包前的检查清单我固定走三遍:Project Settings > Quality,确认每个Level的URP Asset一致;Project Settings > Graphics,确认管线Asset没有空引用;Project Settings > Player里把Color Space设为Linear。这三个地方只要有一个漏掉,打出来的包就会出现“编辑器正常、打包翻车”。
如果目标平台是PC,建议抗锯齿开2x,阴影开Soft Shadows,Bloom保留;如果是移动端,把阴影关掉、抗锯齿关掉、Bloom保留但降低采样。太阳场景的光照是模拟的核心体验,Bloom是太阳发光感的关键,其他特效都可以让路。
URP Asset面板里还有个细节:勾选HDR选项,后处理Bloom才能正确识别高亮区域。不勾的话,太阳自发光强度超过1的部分会被当纯白处理,Bloom效果形同虚设。
最后提醒一个动作:检查Scenes in Build里是否只有你真正想发布的场景。很多人多场景打包,Build后进游戏黑屏,就是因为打进了未初始化的空场景。从那以后我每次打包前,都强制自己从Scene列表、Quality、Graphics、Player Settings四个页面完整走一遍才罢休。这个小习惯已经帮我避免了好几次在评审现场翻车的局面,希望帮到你。
本文还有配套的精品资源,点击获取