Unity 中的小技巧
脚本修改资源导入设置的正确姿势
❌错误姿势
如果当前程序中只有待修改的资源本身作为 GameObject 变量为已知, 则使用 AssetDatabase.GetAssetPath()
来获取到资源的 Assets 相对路径, 如果已经有资源的绝对路径了, 只要处理为相对路径即可, 总之第一步就是先获取到待修改资源的相对路径.
有了相对路径之后, 使用 AssetImporter.GetAtPath()
获取到资源的导入设置, 但是此时还只是通用的导入设置, 之后需要进行转换为正确的类型, 比如纹理: as TextureImporter
, 模型则使用 as ModelImporter
, 这样便获取到了指定类型的导入设置信息了.
之后对信息进行设置, 这一步很简单, 直接对转换后的变量进行赋值即可.
最后设置脏标志, 保存, 刷新即可.
1 | EditorUtility.SetDirty(asset); |
上面的步骤基本是对的, 但是最关键的一步也就是最后一步保存是大错特错的, 这样设置后你会发现资源的导入设置就会变得非常奇怪. 因为有些设置很简单, 没有附加设置, 但是有些设置是具有附加设置的, 比如 Mip Maps 开关, 当开启 Mip Maps 时会多出现很多关于 Mip Maps 的设置选项. 使用上述方法关闭 Mip Maps 设置后就会出现 Mip Maps 是关闭了, 但是其附加设置却没有关闭!!!
✔正确姿势
对资源的导入设置正确修改之后, 应该进行重新导入, 以关闭模型的法线导入为例:
1 | // 关闭法线导入 |
既不需要 SaveAssets, 也不需要 Refresh, 这才是资源导入设置的正确修改姿势.
脚本修改 Prefab 资源中的 Animator 设置
- 使用
AssetDatabase.LoadAssetAtPath
加载预制体 - 使用 `` 获取预制体上的 Animator 组件
- 修改 Animator 组件的设置
- 使用
EditorUtility.SetDirty
设置预制体脏标志 - 循环执行上述步骤, 修改所有预制体上的所有 Animator 组件的设置, 并为每一个预制体设置脏标志
- 调用保存方法
AssetDatabase.SaveAssets();
保存所有的预制体改动
1 | for (var i = 1; i < 9; i++) |
如何使用 Unity Profiler 分析指定的代码段耗时?
使用 UnityEngine.Profiling.Profiler.BeginSample("在 Hierarchy 中显示的名称")
和 UnityEngine.Profiling.Profiler.EndSample()
可以对两者中间的代码段进行单独的性能分析.
比如下面的代码可以单独对 AssetBundle.LoadFromFile(assetPath)
这行代码进行性能分析.
1 | if (assetBundle == null) |
Debug.Log() 高亮游戏物体和路径
众所周知, Debug.Log() 中的第二个参数可以传入一个 UnityEngine.Object 物体, 用于迅速定位游戏物体.
如果传入自身, 就可以快速定位打印日志的游戏物体; 如果传入日志相关的游戏物体, 自然就可以快速定位到相关的游戏物体.
但是如何定位一个文件夹呢? 比如现在要打印一个日志: "文件夹 XXX 下有 XXX 个资源文件, 超过最大数量限制, 请尽快拆分文件夹!", 需要让相关的处理者们快速定位到有问题的路径.
可以使用 AssetDatabase.LoadAssetAtPath<Object>()
来实现. 这个 API 不仅可以获取 Project 面板中的资源文件, 也可以获取目录.
1 | foreach (var key in detectResult.Keys.Where(key => key.Name.Equals(".git") == false)) |
C# 移除字典中所有符合特定规则的数据
1 | /// <summary> |
在脚本中清空 Console 窗口
在想要清空控制台的位置调用下面的方法即可清空控制台.
[注] 此方法在安卓平台下无法通过编译!
使用 Conditional 区分平台调用, #if 区分平台编译.
1 | /// <summary> |
UGUI 按钮点击之后持续高亮的 Bug
Unity 中按钮的外观可以设置为图片切换模式, 这种模式下有一个高亮状态, 当鼠标悬浮在按钮上时, 按钮就会处于高亮状态, 但是我发现默认设置下, 按钮点击之后, 按钮的高亮状态就无法触发了, 除非按下另一个按钮.
查阅资料发现是 "导航" 的问题, 设置为 none
就可以了.
在 Button 控件中有一个 Navigation
属性, 设置为 none
即可解决问题. 【当然前提是项目中不使用导航来做界面操作】
闭包
之前在学习委托事件的时候了解了一下闭包, 没想到后来在游戏制作中就犯了一个闭包的错误.
使用的是 Unity 引擎, 先看一个 Start 方法:
1 | private void Start() |
执行步骤:
- 新建一个菜单项, 用 gameObject 保存.
- 修改菜单项各项属性. 其中名称属性就是遍历索引 index 的值, 即第一个菜单项的名称是 0, 第二个是 1, 第十个是 9 等等;
- 给菜单项上的按钮注册事件, 事件的参数为 gameObject, 即将菜单项自身作为参数传递到方法中.
那么问题来了, 假设有 4 个菜单项, 那么在事件 Event_ItemSelected
中输出按钮的名称的话, 依次点击菜单项会输出什么呢?
不是很了解闭包的人应该会说出这个答案: 0, 1, 2, 3
, 而正确的答案是: 3, 3, 3, 3
!
什么是闭包?
在上面的代码段中, gameObject
变量是在 Start 方法中定义的, 那么按理来说只要出了 Start 方法, gameObject 变量就不存在了. 但是我们在按钮的 Event_ItemSelected
事件中却又访问了变量 gameObject, 这种现象就是闭包.
当一个变量脱离了其自身的作用域后, 根据上下文关系继续在某些方法或者类中发挥作用的现象就是闭包. 或者说闭包可以让变量脱离其作用域继续发挥作用.
答案解析
在上面的代码中, 每次循环都会创建一个菜单项, 并将 gameObject 变量作为参数绑定事件, 所以最后的结果就是 菜单项 0, 菜单项 1, 菜单项 2, 菜单项 3 都使用了 gameObject 变量作为参数, 而 gameObject 是一个变量啊, 不是一个常量, 创建菜单项 0 的时候, gameObject 确实是菜单项 0, 但是等到了创建菜单项 3 的时候, gameObject 也就变成菜单项 3 了, 因此调用事件的瞬间, 使用的也是那个瞬间的 gameObject 值, 也就是 3.
纠正
1 | private void Start() |
使用这段代码就可以纠正那个错误, 两段代码只有一个区别, 就是 gameObject 变量定义的位置不同. 一个在 for 循环外, 一个在 for 循环内.
在 for 循环外部定义时, 每一个菜单项使用的都是同一个 gameObject;
在 for 循环内部定义时, 每一次循环都会重新定义一个 gameObject, 每一个菜单项之间使用的都是不同的 gameObject;
重命名序列化的变量时如果保持引用不丢失?
[注] 参考链接: Unity 的各种小实验
一般情况下, 从 Unity 编辑器上对脚本中的变量复制引用之后, 如果修改了脚本中变量的名称, 那么 Unity 重新编译之后变量的引用就是变成空.
Unity 提供了一个解决办法, 就是使用 [FormerlySerializedAs("OldNameString")]
特性.
比如目前变量 playerID 的值是 "岚", 现在需要修改变量的名称, 为了维持变量的引用, 修改名称之后先写一句: [FormerlySerializedAs("playerID")]
, 之后将变量名修改为 playerName, 保存, 重新编译后会发现 Unity 中 playerName 的值还是 "岚", 此时可以删除这个特性, 保留着也没问题.
Unity 的脚本编译规则
[注] 参考链接: Unity 的各种小实验
脚本编辑规则
最终所有代码都会生成 dll, 放在 Project/Library/ScriptAssembiles 目录中.
脚本分为运行时和编辑时两类, 运行时脚本最终会编译进游戏包中, 而编辑时脚本仅用于编辑器模式下, 不会被打包进游戏包.
脚本编译顺序
最先编译 Plugins 目录下的, 然后是 Plugins 下的所有 Editor 子目录, 然后编译其他目录, 最后编译其他 Editor 目录.
先编译的不可以访问后面的数据, 所以 Plugins 下的代码不能访问其他代码, 后编译的可以访问先编译的脚本.
各目录脚本最终所在的 dll
- Plugins 下非 Editor 目录脚本编译进 Assembly-CSharp-firstpass.dll
- Plugins 下的 Editor 目录脚本编译进 Assembly-CSharp-Editor-firstpass.dll
- 其他非 Editor 目录脚本编译进 Assembly-CSharp.dll
- 其他的 Editor 目录脚本编译进 Assembly-CSharp-Editor.dll
脚本控制 Unity 编辑器的暂停与退出
退出功能:
1 |
|
暂停功能:
1 | UnityEditor.EditorApplication.isPaused = true; |
Unity 中常用的 C# 特性
特性 | 作用 | 应用场景 |
---|---|---|
[Serializefield] |
序列化特定的可序列化字段 | 将私有的可序列化字段进行标识后, Unity 的 Inspector 面板中将会显示该字段 |
[HideInInspector] |
隐藏字段 | 和 [Serializefield] 正好相反, 此特性是为了在 Inspector 面板中隐藏字段的显示 |
[Header(string)] |
字段添加头部注释 | Unity 的 Inspector 面板中字段前会显示一段提示文字, 主要用于字段分类 |
[Tooltip(string)] |
字段添加气泡提示 | 鼠标放到 Inspector 面板的字段上会显示气泡提示 |
[TextArea] |
文本区域 | 使 string 类型的变量在 Inspector 面板显示为文本框, 可多行输入 |
[Range(float,float)] |
滑动条 | 使 float 类型的变量在 Inspector 面板中显示为滑动条 |
[Space(float)] |
空白 | 布局时使用, 在 Inspector 面板的相应位置生成一块空白 |
特性 | 作用 | 应用场景 |
---|---|---|
[RequireComponent(typeof(XXX)] |
组件依赖 | 当脚本需要依赖特定组件的时候使用此特性添加依赖, 依赖的组件将被自动添加并不可移除 |
[DisallowMultipleComponent] |
禁止重复组件 | 当组件需要只能存在一个的时候可以使用此特性, 强制特定组件在同一个游戏物体上只能添加一个 |
[ExecuteInEditMode] |
使脚本在编辑器模式下也能运行 | 目前还没用到过~ 😂 |
[SelectionBase] |
修改 Scene 窗口中物体的默认选中 | 有时候我们在 Scene 窗口中, 通过单击看到的物体查找它在 Hierarchy 面板中的位置, 但是我们这个物体是没有外观的, 他的子物体有外观, 这时候就可以将此物体的一个组件使用此特性修饰, 当单击有外观的子物体时, 定位的不是子物体, 而是我们想要的那个没有外观的物体 |
[CanEditMultipleObjects] |
多选编辑 | 让游戏物体可以被多选同时编辑 |