简介:这份《Unity5实战:使用C#和Unity开发多平台游戏》配套源码,面向希望系统理解Unity5组件架构与C#脚本开发的游戏学习者,也适合有一定经验、想复盘跨平台项目组织方式的开发者。压缩包为7z格式,体积约130.09MB;上游文件总数与类型明细暂未提供有效数据,因此不展开具体文件构成。已有492人学习下载,可见其在Unity5实战学习场景中具有一定参考热度。结合资源描述,源码可帮助读者围绕实际项目中的场景搭建、MonoBehaviour生命周期、预设体复用、纹理音频模型接入以及Shader与材质配置展开练习,并进一步理解移动端、桌面端等平台的导出适配与性能优化思路;对初学者可用于反复修改代码、调整参数,对有经验者则便于提炼设计思路、排查常见问题并逐步沉淀自己的跨平台开发方法。
1. 从一份 Unity5 源码说起:它今天还能解决什么问题
把一套 Unity5 的 C# 多平台游戏源码拖进新版编辑器,最先崩的往往不是代码,是心态:材质球一片洋红,预制体上挂着一排 “The referenced script on this Behaviour is missing”,点运行直接黑屏。我前阵子为了给一个教学项目找参考实现,硬着头皮把这样一套源码从头跑到尾,结论有点反直觉——它真正值钱的地方不是那些玩法,而是一个完整工程该有的骨架:场景怎么切、预制体怎么管、一份 C# 代码怎么同时伺候键鼠和触摸、出包时哪几个开关必须动。
这套东西适合两类人:手上有 C# 基础、想把“能跑”推进到“能出安装包”的开发者;以及被新编辑器一层层依赖绕晕、想回头看清最朴素组件模型的人。它的边界也很清楚,5.x 时代的 API 和 C# 语法版本摆在那儿,照抄会翻车,得知道哪些要改、改到什么程度。
2. 工程结构与版本对齐:让 5.x 源码在新编辑器里跑通第一帧
拿到压缩包解压之后,多数人的第一反应是双击场景文件,然后被编辑器版本提示拦下来,选“升级”一路点到底,最后得到一个半死不活的工程。顺序错了。先用原版编辑器把基线跑出来,再决定往哪个方向迁移,这个习惯能省掉后面一整天的对拍工作。
2.1 Assets 目录里的四类资产,先分清楚再动手
Unity 工程的 Assets 目录看起来是一锅粥,其实按职责就能切成四块,每一块在迁移时的风险等级完全不同。
| 目录/类型 | 承载内容 | 迁移风险 | 处理方式 |
|---|---|---|---|
| Scenes | 关卡场景、启动场景 | 中 | 检查 Build Settings 顺序与场景内引用 |
| Scripts | C# 逻辑、少量编辑器扩展 | 高 | 优先处理语法与 API 变更 |
| Prefabs | 玩家、敌人、UI 面板 | 高 | 重点查丢失脚本和材质引用 |
| Resources / 美术 | 贴图、模型、音频、配置文本 | 低到中 | 查压缩格式与加载路径大小写 |
| Plugins | Android 的 .jar/.aar、iOS 的 .a/.mm | 高 | 平台插件基本要重编 |
先把 Scenes 和 Prefabs 之间的关系摸清楚:哪个场景是启动场景,场景里挂了哪些常驻对象,预制体是被 Resources.Load 动态拉起来的还是挂在场景里序列化引用的。这一步不是学术研究,是因为后面所有“跑不起来”的问题,八成都能追溯到这两类引用上。
还有一类东西容易被忽略:源码里如果混着.js后缀的脚本,那是 UnityScript,新版编辑器早就不认了。看到就先单独挪到一个_Legacy目录里备份,别指望它能编译通过,能改写成 C# 的就改,改不动的直接弃用,然后在预制体上把丢失的组件补回去。
注意:压缩包解压后经常出现同名文件覆盖,尤其是
.meta文件。.meta里存的是 GUID,丢了 GUID 等于所有引用断链,这是后面“Missing Script”最主要的来源。
2.2 版本对齐:5.x 工程到新版编辑器的三步迁移
整个过程分三步,每一步都有明确的验收标准,不达标就别往下走。
第一步,冻结基线。把原始工程原封不动复制一份,用与源码同代的 5.x 编辑器打开,确认它本来能不能跑。这一步的价值在于建立“原始状态”这个参照物——后面新版编辑器里出任何问题,你都能判断是迁移引入的,还是源码本来就有毛病。
# 复制一份基线,永远不在原件上做升级实验 cp -r Unity5GameProject Unity5GameProject_baseline # 看一眼工程标记的编辑器版本,决定用哪个版本开 cat Unity5GameProject/ProjectSettings/ProjectVersion.txt # 典型内容:m_EditorVersion: 5.x.xf1ProjectVersion.txt只是编辑器写下的标记,不决定工程能不能开,但它告诉你这份源码的出生年代。5.x 系列的不同小版本之间,Standard Shader 和 UI 系统的行为也有差异,基线尽量用接近的版本开。
第二步,另存一份升级副本,用新版编辑器打开,让 API Updater 自动处理一批改名的方法和属性。它会弹出“是否接受”的对话框,第一次建议全接受,然后把 Console 里的警告逐条看完。API Updater 能改的是签名级别的变化,改不了语义级别的变化,比如Application.LoadLevel变成SceneManager.LoadScene之后,场景名字符串是不是还对得上,它管不了。
第三步,按“先编译、后运行”的顺序推进。编译错误不清零,运行时的表现没有任何参考价值。清完编译错误,再进场景看缺失引用,最后才谈玩法能不能跑通。
提示:不要在同一个工程目录上反复切换编辑器版本。每次切换 Unity 都会重写 Library 和 ProjectSettings,来回切几次之后,问题会变得无法归因。
2.3 用编辑器脚本做一次工程体检
手工在 Hierarchy 里一个个点开预制体找丢失引用,几十个资源下来眼睛就废了。写个编辑器脚本批量扫,十秒钟出报告,这是后面所有排查工作的地基。
// Assets/Editor/ProjectSanityCheck.cs // 放在 Assets/Editor 下,编辑器菜单里会出现「Tools/工程体检」 using UnityEditor; using UnityEngine; using System.Collections.Generic; public static class ProjectSanityCheck { [MenuItem("Tools/工程体检/扫描丢失脚本")] public static void ScanMissingScripts() { // 只扫 Assets 下的预制体和场景,Packages 里的资源不参与 string[] guids = AssetDatabase.FindAssets("t:Prefab t:Scene", new[] { "Assets" }); int totalMissing = 0; List<string> report = new List<string>(); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); GameObject go = AssetDatabase.LoadAssetAtPath<GameObject>(path); if (go == null) continue; // 第二个参数 true 表示把未激活的子物体也算进来 Component[] components = go.GetComponentsInChildren<Component>(true); int miss = 0; for (int i = 0; i < components.Length; i++) { if (components[i] == null) miss++; // 丢失的脚本在内存里就是 null } if (miss > 0) { totalMissing += miss; report.Add(path + " -> " + miss + " 处丢失引用"); } } for (int i = 0; i < report.Count; i++) { Debug.LogWarning(report[i], AssetDatabase.LoadAssetAtPath<Object>( report[i].Split(' ')[0])); } Debug.Log("扫描完成,共 " + totalMissing + " 处丢失脚本引用"); } }逐段说明。AssetDatabase.FindAssets的过滤字符串用空格分隔多个类型,这里同时要 Prefab 和 Scene;限定搜索范围new[] { "Assets" }是为了排除包管理器目录,那些资源不归你管。GetComponentsInChildren<Component>(true)的泛型参数用基类Component是刻意的——丢失的脚本具体是什么类型已经无从得知,只能拿基类兜住所有槽位,其中为 null 的就是断掉的引用。Debug.LogWarning的第二个参数传上下文对象,这样在 Console 里点一下日志,Project 面板会直接定位到那个预制体,比只打印字符串好用得多。
这个脚本解决的是“有没有”的问题,接下来才是“为什么”。扫描结果里如果集中在某几个预制体上,通常是这几个文件所在的目录在解压时被覆盖过,.meta和资源文件对不上;如果散落在全工程,多半是源码里用了一批 UnityScript 脚本,迁移时被整体丢弃了。
3. C# 脚本层拆解:生命周期、组件通信与输入抽象
编译通过只是最低门槛,真正决定这套源码能不能当参考的,是脚本层有没有清晰的组织逻辑。Unity5 时代的代码风格普遍偏“直给”,一个脚本干五件事是常态,但里面关于执行顺序和组件通信的处理,仍然值得逐段读。
3.1 MonoBehaviour 生命周期:哪些顺序会咬人
先摆一张表,把执行顺序钉死。这张表不是背知识点的,是排查“为什么我的变量还是 null”的第一现场。
| 阶段 | 方法 | 调用次数 | 适合做什么 | 不要做什么 |
|---|---|---|---|---|
| 初始化 | Awake | 一次 | 缓存自身组件引用、初始化数据结构 | 访问其他对象的 Start 结果 |
| 初始化 | OnEnable | 每次激活 | 注册事件、恢复监听 | 依赖场景内其他对象已就绪 |
| 初始化 | Start | 一次 | 跨对象绑定、读配置、启动流程 | 做耗时加载 |
| 物理 | FixedUpdate | 固定步长 | Rigidbody 相关运算 | 读 Input |
| 逻辑 | Update | 每帧 | 输入、动画参数、计时 | 大量物理运算 |
| 收尾 | LateUpdate | 每帧 | 相机跟随、UI 跟随 | 修改物理状态 |
| 销毁 | OnDisable / OnDestroy | 各一次 | 反注册事件、释放资源 | 假设其他对象还活着 |
最常咬人的是 Awake 和 Start 的边界。Awake 阶段所有物体的 Awake 会先跑完,但各个物体之间谁先谁后没有保证;跨对象的引用如果放在 Awake 里拿,运气不好就是空引用。稳妥的写法是分两层:Awake 只做“我自己能独立完成”的事,Start 再做跨对象交互。
using UnityEngine; // 场景根节点上的启动器,负责按明确顺序拉起各子系统 public class Bootstrap : MonoBehaviour { [SerializeField] private GameManager gameManager; [SerializeField] private UIManager uiManager; private void Awake() { // 只做本对象内部的初始化,不假设别人已经准备好 gameManager.Init(); } private void Start() { // 此时全场景的 Awake 都已执行完,可以安全地做绑定 uiManager.Bind(gameManager); gameManager.StartGame(); } }[SerializeField] private这个组合值得单独说:字段是 private 的,外部改不了,但在 Inspector 里能拖引用,比在代码里写GameObject.Find安全得多。源码里如果有大量GameObject.Find和FindObjectOfType,迁移时优先把它们换成序列化引用,运行时的开销和断链风险都会明显下降。
如果确实需要控制同一阶段内的执行顺序,用Edit > Project Settings > Script Execution Order给关键脚本指定数值,数值小的先执行。这是 Unity5 时代就有的机制,比在代码里塞yield return null靠谱。但别滥用,超过五六个脚本需要手工排序,说明架构该拆了。
3.2 组件通信:GetComponent、事件与单例的边界
GetComponent本身不慢,慢的是在 Update 里反复调用。源码里这类写法很常见:
// 反例:每帧查一次组件,能跑但没必要 void Update() { GetComponent<Rigidbody>().AddForce(Vector3.up); }正确的做法是 Awake 里缓存到字段。这个改动小到可以批量做,收益却很直接。
比组件查找更麻烦的是跨模块通信。单例是 Unity5 时代最流行的方案,public static GameManager Instance;到处都是,好处是随时能拿到,坏处是耦合死、测试难、多场景切换时容易留下悬挂引用。读源码时留意单例的赋值时机,如果写在 Awake 里却没有做重复实例检测,加载第二个场景时就会出现两个管理器互相打架。
更干净的替代是 C# 事件。让数据的持有者负责广播,界面负责订阅,双方都不需要知道对方存在。
using UnityEngine; // 血量变化通过事件广播出去,UI 只订阅,不持有任何管理器引用 public class PlayerHealth : MonoBehaviour { // 参数:当前血量、最大血量 public event System.Action<int, int> OnHealthChanged; private int current; private int max = 100; public void TakeDamage(int amount) { current = Mathf.Max(0, current - amount); if (OnHealthChanged != null) { OnHealthChanged(current, max); } } }这里用了if (OnHealthChanged != null)而不是 C# 6 才有的空条件运算符?.,原因是 Unity5 自带的编译器版本偏老,源码里凡是出现?.、$""字符串插值、nameof、表达式体成员的地方,在新编辑器里能过,在原版 5.x 里就过不了。迁移前先确定目标编辑器,再决定要不要把这些语法糖改回去,别两边来回折腾。
订阅方在OnEnable里挂、OnDisable里撤,这是纪律。忘记撤订阅是内存泄漏和空引用的常见来源——对象被销毁了,事件列表里还留着它的方法引用,下一次广播就抛异常。
3.3 输入抽象:一套代码吃键鼠和触摸
多平台游戏最容易分叉的地方就是输入。源码里如果到处都是Input.GetMouseButtonDown(0),那它在手机上基本是废的。合格的写法是抽一层接口,把“点击”和“拖拽”这两种语义从具体设备里剥离出来。
using UnityEngine; // 输入语义层:上层只关心点没点、拖了多远,不关心是鼠标还是手指 public interface IPointerInput { bool TryGetTap(out Vector2 screenPos); bool TryGetDrag(out Vector2 delta); } // 鼠标实现:直接映射左右键 public class MousePointerInput : IPointerInput { public bool TryGetTap(out Vector2 screenPos) { screenPos = Input.mousePosition; return Input.GetMouseButtonDown(0); } public bool TryGetDrag(out Vector2 delta) { delta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y")); return Input.GetMouseButton(0); } } // 触摸实现:取第一根手指,多点触控需要扩展成数组 public class TouchPointerInput : IPointerInput { public bool TryGetTap(out Vector2 screenPos) { screenPos = Vector2.zero; if (Input.touchCount == 0) return false; Touch t = Input.GetTouch(0); screenPos = t.position; return t.phase == TouchPhase.Began; } public bool TryGetDrag(out Vector2 delta) { delta = Vector2.zero; if (Input.touchCount == 0) return false; delta = Input.GetTouch(0).deltaPosition; return true; } } // 工厂:在编辑器里跑也能切换到触摸实现做验证 public static class PointerInputFactory { public static IPointerInput Create() { #if UNITY_EDITOR return new MousePointerInput(); // 编辑器里默认用鼠标,方便调试 #elif UNITY_ANDROID || UNITY_IOS return new TouchPointerInput(); #else return new MousePointerInput(); #endif } }拆开看。接口只有两个方法,语义极简,好处是上层逻辑(比如拖拽放置建筑、点击拾取道具)完全不用改就能跨平台。鼠标实现里拖拽用的是GetAxis,因为mousePosition的帧间差值在低帧率下抖动明显;触摸实现里直接用deltaPosition,这是系统给的,比自己算稳。工厂里的#if UNITY_EDITOR分支优先级最高,否则在编辑器里模拟安卓平台时,代码会走进触摸分支,结果鼠标点不动,白白浪费半小时怀疑人生。
提示:
Input.simulateMouseWithTouches默认是开的,意味着触摸会被同时映射成鼠标事件。如果新旧两套输入逻辑同时存在,一次点击可能触发两次,表现是“点一下加两分”。迁移时把这条排查掉。
4. 多平台构建:一份 C# 代码出四端的配置差异
同一份代码出 Windows、macOS、Android、iOS 四个包,代码本身能复用八九成,剩下的一两成全在配置和 API 差异上。这部分是很多教程视频跳过的,因为它不出现在代码编辑器里,全在菜单和面板里。
4.1 Player Settings 里真正影响出包的开关
面板项很多,真正会导致“编辑器里好好的、出包就崩”的就那么几个。
| 配置项 | 位置 | 影响 | 建议 |
|---|---|---|---|
| Bundle Identifier | Player > Other Settings | 安卓/iOS 包的唯一标识 | 迁移后必须改掉原始包名 |
| Minimum API Level | Player > Other Settings | 低版本设备能否安装 | 按目标机型定,别盲目拉高 |
| Scripting Backend | Player > Other Settings | Mono 编译快、IL2CPP 性能好 | iOS 强制 IL2CPP,安卓可先用 Mono 调 |
| Api Compatibility Level | Player > Other Settings | 影响可用的 .NET 类库 | 老插件常依赖 .NET 2.0 子集 |
| Graphics API | Player > Other Settings | 影响 shader 兼容性 | 洋红材质多半出在这里 |
| Texture Compression | Player > Other Settings | 包体和画质 | 安卓 ASTC/ETC2,iOS PVRTC/ASTC |
| Strip Engine Code | Player > Other Settings | 裁剪未使用的引擎模块 | 用反射加载的类会被误裁,谨慎开 |
| Scenes In Build | File > Build Settings | 决定打进包里的场景 | 启动场景必须在索引 0 |
Strip Engine Code这个开关坑过不少人:打开之后包体确实小了,但代码里用字符串反射创建的类型可能被裁掉,表现是运行到某个功能就报找不到类。排查思路很简单,关掉这个开关再打一次包,问题消失就说明是裁剪导致的,然后用link.xml把需要保留的程序集列出来。
4.2 条件编译与平台 API 差异
平台判断别散落在业务代码里,集中到一个静态类,改的时候只改一处。
using UnityEngine; // 平台信息的唯一出口,业务层禁止直接写 #if UNITY_ANDROID public static class PlatformInfo { public static string Name { get { #if UNITY_IOS return "iOS"; #elif UNITY_ANDROID return "Android"; #elif UNITY_STANDALONE_OSX return "macOS"; #elif UNITY_STANDALONE_WIN return "Windows"; #elif UNITY_WEBGL return "WebGL"; #else return "Unknown"; #endif } } // 移动端才需要处理安全区、返回键、触摸 UI 放大 public static bool IsMobile { get { #if UNITY_ANDROID || UNITY_IOS return true; #else return false; #endif } } }注意一个细节:在编辑器里把 Build Target 切到 Android,UNITY_ANDROID就是真,但代码仍然跑在桌面环境里。所以涉及“只有真机才有”的能力(比如振动、陀螺仪、返回键),判断条件要写成#if UNITY_ANDROID && !UNITY_EDITOR,否则编辑器里调试时会走进一条根本没实现的路径。这个坑在源码里出现的频率极高,因为写书那会儿大家也不太在意编辑器模拟。
文件路径也是重灾区。Application.persistentDataPath在各平台都不一样,存档别写死绝对路径;Application.streamingAssetsPath在安卓上指向压缩包内部,不能用File.ReadAllBytes直接读,得走UnityWebRequest。
4.3 分辨率、纹理压缩与资源目录
UI 适配靠 Canvas 的三个组件配合:Canvas Scaler选Scale With Screen Size,Reference Resolution填设计稿尺寸,Screen Match Mode选Match Width Or Height并把滑杆拉到合适位置——横版游戏偏宽度,竖版偏高度。源码里如果是Constant Pixel Size,换到手机上就是一团糊或者巨大无比。
纹理这块,导入设置里的Max Size和Compression要按用途分:UI 图集一般 1024 或 2048,模型贴图按需,别一股脑 4096,包体撑爆。压缩格式选Automatic让编辑器按平台决定,手动指定反而容易在某个平台上出问题。
资源加载路径的大小写是另一个隐形炸弹。Windows 上Resources.Load("config/Level_01")和"Config/level_01"都能过,到了 Android 就只认完全一致的那一个。
// 路径不带扩展名,且大小写必须与磁盘完全一致 TextAsset cfg = Resources.Load<TextAsset>("Config/level_01"); if (cfg == null) { // 把失败路径打出来,比一句「加载失败」有用得多 Debug.LogError("配置缺失,检查 Assets/Resources/Config/level_01.txt 是否存在且大小写一致"); }Resources目录的内容会全量打进包,不受引用关系裁剪,这是它方便的地方,也是包体膨胀的元凶。源码里如果有大量资源塞在 Resources 下,迁移时可以考虑改成直接序列化引用,或者按场景拆到不同的资源组里。
5. 排查手册:源码跑不起来的五类高频故障
前面几章讲的是怎么跑对,这一章讲跑不对的时候怎么定位。下面五条是我在这套源码上真实撞过的,按出现频率排序。
5.1 五条现象、原因与解决
第一条,材质全变洋红。现象是场景里所有模型变成高饱和度的粉色,UI 正常。原因是材质引用的内置 shader 在新版编辑器里找不到对应实现,Unity5 时代的 Standard Shader 和后来的渲染管线不是一套东西。解决办法是选中材质,在 Inspector 里把 Shader 下拉框重新指到Standard;数量多的话写个编辑器脚本遍历所有 Material 批量替换。如果目标平台用自定义渲染管线,那就得统一换成管线自带的 Lit shader,这一步没有捷径。
第二条,预制体上挂着一排 Missing Script。现象是打开预制体,Inspector 里显示 “The referenced script on this Behaviour is missing”。原因有三:脚本文件被删或改名、.meta文件丢失导致 GUID 对不上、源码里是 UnityScript 而新版编辑器不认。解决顺序是先跑 2.3 那个扫描脚本定位范围,再看对应目录下.meta是否完整。如果是改名导致的,把文件名改回去最快;如果是 GUID 真丢了,只能在预制体上重新挂脚本并补参数,所以版本管理里.meta必须跟资源一起提交。
第三条,编辑器能跑,打包后黑屏。现象是安装包启动后只有背景色,没有任何 UI 和模型。原因通常是Build Settings里场景列表为空,或者启动场景不在索引 0 的位置,也可能是启动场景的加载逻辑写在一个没被包含进包的脚本里。解决办法是打开 Build Settings 确认场景列表和顺序,再用adb logcat抓真机日志看第一行异常。黑屏问题九成能在日志前三行找到答案。
第四条,手机上没有触摸响应。现象是界面能显示,点按钮没反应,但用鼠标在编辑器里一切正常。原因一般是业务逻辑里写死了Input.GetMouseButtonDown,触摸事件没有映射过来;或者项目切换过输入系统,旧的 Input Manager 被禁用。解决办法是套用 3.3 的输入抽象层,把所有输入调用收敛到一处;同时检查 Player Settings 里的输入处理模式是否与代码匹配。
第五条,真机上读配置失败。现象是编辑器里正常,安卓包启动时报空引用,堆栈指向资源加载那一行。原因是用了File.ReadAllBytes去读 StreamingAssets 或者路径大小写不一致。解决办法是安卓端改用UnityWebRequest读 StreamingAssets,Resources 路径统一改成小写下划线风格并在加载失败时打印完整路径。
注意:这五条里有四条在编辑器里完全看不出来,必须在真机或打包产物上验证。每改完一轮代码就打一次包,别攒到最后一次性验证。
5.2 打开黑匣子:日志、断点与帧调试
Unity 的日志系统比很多人以为的强。Debug.Log的第二个参数传this,Console 里点日志能直接选中场景里对应的物体,比打印坐标高效得多。Debug.DrawRay和Debug.DrawLine画出来的辅助线只在 Scene 视图可见,用来确认射线检测方向特别顺手。
真机日志靠命令行抓:
# 只看 Unity 标签,避免系统日志刷屏 adb logcat -s Unity:V # 崩溃栈单独抓一份,闪退时用这个 adb logcat -b crash -s Unity:V # 想同时看时间戳,加 -v time adb logcat -v time -s Unity:V-s是 silent 的意思,只显示指定标签;Unity:V里的 V 是 verbosity 等级,从低到高是 V/D/I/W/E。闪退类问题优先看-b crash缓冲区,那里存的是崩溃前的最后几行,往往是定位空引用的关键。
帧调试方面,Unity5 时代没有现在这些性能分析工具,靠的是Profiler面板和自己在关键节点打时间戳。迁移到新版之后 Profiler 强了不少,但排查思路没变:先看 CPU 占用最高的几个函数,再看 GC Alloc 那一列,每帧有大量小对象分配的地方基本就是卡顿源。
5.3 迁移前的兼容性自检清单
动手改代码之前,把下面这张表过一遍,能避免大部分返工。
| 检查项 | 怎么查 | 不通过的后果 |
|---|---|---|
| 是否有 UnityScript 脚本 | 搜索*.js | 编译直接失败 |
| 是否用了 C# 6+ 语法 | 搜?.、$"、nameof | 老编辑器编译不过 |
| 是否有缺失的 .meta | 比对资源与 meta 数量 | 引用大面积断链 |
| 场景是否在 Build Settings | 打开面板看列表 | 打包后黑屏 |
| 输入方式是否写死 | 搜Input.GetMouse | 移动端无响应 |
| 资源路径是否大小写混用 | 搜Resources.Load | 安卓端加载失败 |
| 第三方插件是否有平台库 | 看 Plugins 目录 | 特定平台构建报错 |
这张表不是走形式,每一条背后都有具体的失败现象。过完之后再动手改,比改到一半发现方向错了强。
6. 把 Demo 变成可复用模块:一份配置表与一条收尾习惯
源码里最典型的问题是数值硬编码:敌人数量、刷新间隔、玩家血量全写在脚本字段里,改一个数值要重新编译,调平衡的时候极其难受。Unity5 时代没有现在这么多花哨的配置方案,ScriptableObject就是最合适的那把刀。
using UnityEngine; // 关卡配置资产:Project 面板右键 Create/Game/Level Config 生成 [CreateAssetMenu(fileName = "LevelConfig", menuName = "Game/Level Config")] public class LevelConfig : ScriptableObject { public string levelId = "level_01"; public int enemyCount = 10; public float spawnInterval = 1.5f; public int playerHp = 100; }这个类继承ScriptableObject而不是MonoBehaviour,意味着它可以作为独立资产存在磁盘上,被多个场景共享,改数值不触发脚本重编译,运行时也不会产生额外实例。[CreateAssetMenu]让它在右键菜单里可以直接创建,策划同学不用碰代码。使用方只需一个序列化字段public LevelConfig config;,在 Inspector 里拖进去即可。相比 JSON 配置文件,它的优势是引用不会因为改名而断,劣势是不能热更——但对单机项目来说这不是问题。
进阶用法是再包一层:把多个LevelConfig放进一个LevelDatabase资产里,用数组维护,启动时按 id 建字典。这样关卡表的增删只在一个资产上操作,代码里一个foreach就搞定。
最后说个习惯。这套源码我前后跑过三遍,前两遍都栽在同一个地方:直接在原始工程上改,改到一半忘了哪一步是源码自带、哪一步是我自己加的,出问题没法归因。从那以后我每次碰陌生工程,第一件事一定是复制一份基线、记下ProjectVersion.txt的版本号、跑一遍工程体检脚本,三步做完才允许自己动第一行代码。这套流程枯燥,但它把“猜”变成了“查”。
希望帮到你。
本文还有配套的精品资源,点击获取