对资源的规范化我们能做什么
对资源的规范化我们能做什么
游戏开发中,资源的管理是一项非常精细的活,不同类型的资源往往拥有完全不同的管理方式和配置方式,本文希望从 5 个不同的管理级别对资源管理进行解构
- 致命资源错误不让保存
- 保存了不让提交
- 提交了可以快速发现问题
- 发现问题可以快速修复
- 版本问题记录
对于可以使用
AssetPostprocessor
解决的问题,这里就不做介绍了,这种问题一般都非常简单,如果不清楚如何使用,可以搜一下相关实现
本文提到的工具,后面如果时间充足,整理完毕后,应该会开源,涉及代码的部分因为会比较冗长,可能会只贴原理部分
致命资源错误不让保存
这里不让保存的原理非常简单,我们需要监听 prefab 保存的事件,在保存前,检查自定义的规则,一旦发现错误,直接抛一个异常即可阻止保存
需要注意的是,虽然我们会监听足够的保存事件,但仍然有取巧的办法突破这个限制,对于这种情况,一般都是 git blame 找到是谁干的,定性为恶意行为
要实现这个过程,我们需要使用 InitializeOnLoad
或 InitializeOnLoadMethod
在 Editor 编译时,自动注册我们的监听函数,这里需要监听如下几个事件
- PrefabUtility.prefabInstanceUpdated
- ApplyAll Prefab 保存事件
- UnityEditor.SceneManagement.PrefabStage.prefabSaving
- Open Prefab 保存事件
- UnityEditor.SceneManagement.PrefabStage.prefabStageOpened
- Open Prefab 打开事件
- Selection.selectionChanged
- 选中 Prefab 的事件
这里的 1 和 2 对应两种不同情况的 prefab 保存事件,可以统一处理过程,3 和 4 是为了处理某些情况下无法获取到当前 prefab 路径的问题,临时缓存一下为接下来有效性验证做准备
selectionChanged 比较特殊,在 Unity Assets 中,我们可以直接点击某个 prefab 对最外层的 GameObject 进行修改,所以这个事件是为了阻止这类错误改动
至于后续的处理过程就比较简单,这里我们可以通过自定义 ScriptObject 配置,来粒度控制每种 prefab 的具体处理过程,这里就不在赘述了
Preset Prefab 只读
在说这个规则之前,我们要明白 Unity prefab 嵌套规则,当 A 嵌套了 B,此时在 A 中 修改 B 的属性,我们 apply 的时候,有两个分支,一个是 apply 到 A,另一个是 apply 到 B,这两个分支对应的结果会 完全不一致
如果我们 apply 到 A,那么当我们单独编辑 B 时,可能会导致部分属性无法直接同步到 A 上,而这个并不是我们希望看到的,如果我们 apply 到 B 上,大部分情况下所有嵌套了 B 的 prefab 都会同步这个改动
这个规则就是为了禁止在 A 上直接修改 B 中核心组件
受到 UGUI-Editor 这个工具的启发,我们可以将一些非常通用的组件做成一个个的 preset,通过 preset 窗口将已有的内容拖到对应的 UI 中
比如 UI 中通用的确认、取消按钮,可以作为 preset 资源供开发者选取
比较可惜的是这个库作者应该是不维护了,我自己是改动了一下,适配了 2021,因为引入了 preset 的制作流程,那么我们应当更进一步,只要发现 prefab 中存在 preset 资源,需要按照规则将部分组件设为只读,不可修改
这里对通用的关闭按钮增加了一个规则,这个 prefab 下的所有 Image 组件会被设为 readonly
核心代码也非常简单,就一行
component.hideFlags = HideFlags.NotEditable;
在致命错误禁止保存的过程中,我们监听了足够多的 prefab 回调,在 prefab 打开时,我们调用这个 NoEdit 函数,对 preset 中的预制按照规则将每个 commponet 设为 NotEditable 即可
基于这个流程,我们可以保证凡是 preset 中的资源,不可以在任何嵌套的 prefab 中直接修改,仅允许直接打开这个 preset 时进行修改
GameObject Copy
我们有部分 prefab 的制作流程是交给美术进行的,美术在制作部分资源时往往没有程序那么严谨,通常会特别喜欢从现成的组件中直接 copy,这个时候可能会导致一些极其难查的问题出现,曾经也是非常头大…
比如游戏中的红点,通过直接挂载 Mono 脚步的方式来实现,每个脚本上会标记当前红点的路径,此时这个 GameObject 被复制并保存到其他 UI 上,这个问题直接让你查到裂开
跟上面一样,我们仍然将所有的规则都配置到 ScriptObject 中
这里的规则就是,禁止 View.RedDotUI 组件的复制
ScriptObject 的代码比较简单我就不贴了,下方为禁止复制的处理过程,稍微有些绕,不贴代码很难讲清楚处理过程
internal class CopyAutomation
{
static CopyAutomation() { EditorApplication.hierarchyWindowItemOnGUI += _HierarchyWindowItemOnGUI; }
private static double _threshold = 0.1f;
private static double _last_tick = 0;
private static void _HierarchyWindowItemOnGUI(int instance_id, Rect selection_rect)
{
Event e = Event.current;
// 如果是无效的指令
// 直接退出
if(e.type != UnityEngine.EventType.ValidateCommand)
{
return;
}
// 这里 id = 0时, 说明没有选择任何对象
// Hierarchy 传入 id = 当前选择对象
if(Selection.activeInstanceID != 0 && instance_id != Selection.activeInstanceID)
{
return;
}
switch(e.commandName)
{
// 只处理如下两种指令
case"Duplicate":
case"Paste":
// 需要等 Hierarchy 已经刷新后, 再进行校验
// 此时 Selection 当前选择的对象就是 复制好的
EditorApplication.delayCall += _ValidateComponent;
break;
}
}
private static void _ValidateComponent()
{
EditorApplication.delayCall -= _ValidateComponent;
// 如果是 Prefab 实例, 那么不做任何校验
if(PrefabUtility.IsPartOfPrefabInstance(Selection.activeObject))
{
return;
}
CopyConfigs configs = CopyConfigs.default_configs;
if(!configs.enable)
{
return;
}
double tick = TimeSpan.FromTicks(DateTime.Now.Ticks).TotalSeconds;
// 此处阻止短时间内的多次检测
if(_last_tick - tick > _threshold)
{
return;
}
_last_tick = tick;
var components = Selection.activeGameObject.GetComponentsInChildren<Component>();
HashSet<string> error_com = new HashSet<string>();
foreach(var component in components)
{
Type type = component.GetType();
foreach(CopyConfig config in configs.configs)
{
if(!configs.enable)
{
continue;
}
if(!string.Equals(type.FullName, config.type))
{
continue;
}
if(!error_com.Contains(type.FullName))
{
error_com.Add(type.FullName);
}
UnityEngine.Object.DestroyImmediate(component);
}
}
if(error_com.Count <= 0)
{
return;
}
string all_name = string.Empty;
foreach(string name in error_com)
{
all_name = \$"{all_name}, {name}";
}
EditorUtility.DisplayDialog("警告", \$"当前检测到不允许复制的组件 {all_name}, 已经自动移除", "确定");
}
}
保存了不让提交
不让提交这个事情我们就需要 git hook 这个老朋友出场了,这个过程非常简单,在运行 Unity 时,检查 .git/hook/pre-commit
脚本是否为最新的,不是就复制一份过来
HookConfig.conf 这个文件是为了照顾一些 GUI 的用户,确实需要修改一些禁止修改的文件时,通过开关临时关闭这条规则
对于命令行用户跳过 hook 就很简单了 git commit -m “xxx” -n
提交了可以快速发现问题
资源规范的制定往往是一点点建立的,某些资源在最开始通过了上述所有验证,但是新增规则后,现有的资源就存在问题,此时感知问题的存在是非常非常重要的,而且更重要的是,这个感知过程编写的成本要足够低
这个时候就需要 Odin Validator 3.1 版本出马了
Odin 的团队真是神了,期待一下 基于 UIToolkit 的版本,如果你还没听过 Odin 那可真的太遗憾了…
这条检测规则为:凡是发现 ParticleSystem 中 max particles 数量大于 30,直接报错
具体的代码会贴在下面
发现问题可以快速修复
当我们已经拥有感知问题的能力后,你一定不希望一些小的问题都需要自己亲力亲为来修复,这条规则同样是基于 Odin Validator 3.1 在感知问题的同时,Odin 给了一个快速 Fix 的方式
注意这里红框的部分,凡是有这个小扳手的错误,我们都可以直接点击 Execute 一键修复
下方代码定义了两组规则,首先粒子禁止开启 prewarm
,maxParticles
默认不允许超过 30,如果是ParticleSystemRenderMode.Mesh
的方式,则会限制为 5
这里对 ParticalSystem 属性的修改,在不同版本的 Unity 中会不一致,这里是 Unity 2021,是通过 SerializedObject 来间接修改的
#if UNITY_EDITOR
using Sirenix.OdinInspector.Editor.Validation;
using UnityEngine;
using UnityEditor;
[assembly:
RegisterValidationRule(typeof(ParticleSystemValidator), Name = "粒子合法校验", Description = "Some description text.")]
public class ParticleSystemValidator : RootObjectValidator<ParticleSystem>
{
protected override void Validate(ValidationResult result)
{
ParticleSystem ps = Object;
if(ps.main.prewarm)
{
result.AddError("ParticleSystem should not prewarm!").
WithFix(
Fix.Create(
"Disable Prewarm",
() =>
{
var so = new SerializedObject(ps);
var prewarm = so.FindProperty("prewarm");
if(prewarm is null || !prewarm.boolValue)
{
return;
}
prewarm.boolValue = false;
so.ApplyModifiedProperties();
}
)
);
return;
}
var renderer = ps.GetComponent<ParticleSystemRenderer>();
int max_limit = 30;
if(renderer.renderMode == ParticleSystemRenderMode.Mesh)
{
max_limit = 5;
}
if(ps.main.maxParticles > max_limit)
{
result.AddError($"ParticleSystem max particles > {max_limit}!").
WithFix(
Fix.Create(
$"Down to {max_limit}",
() =>
{
var so = new SerializedObject(ps);
var max = so.FindProperty("InitialModule.maxNumParticles");
if(max is null || max.intValue <= max_limit)
{
return;
}
max.intValue = max_limit;
so.ApplyModifiedProperties();
}
)
);
}
}
}
#endif
版本问题记录
我们公司采购了 UWA Pipeline,所以自然会将 UWA 的资源检测纳入到整体流程中,在资源检测这一项中,UWA 扮演了非常重要的一个角色,比如上面的粒子规则就是参考 UWA 资源监测中的内容 确定并实现的。以及每天一次的资源报告,让我们可以对项目中总体问题的变化做到心中有数
UWA 的资源检测更像一个知识库,可以有效的帮助提升团队的平均水平,每条是否存在问题、应该怎么解决、为什么需要优化,都列的非常清楚
在项目的前期,可以快速建立整体资源的标准,在中后期,可以将报告的优化任务轮流交给组内的开发者
- 感谢你赐予我前进的力量