前言

  前段时间有遇到一个需求,需要在资源保存的时候做一些验证,检查资源是否正确。
  这就需要实现保存的时候触发一个 HOOK 执行相应的检验函数。

C++ 实现

  面对这个问题,我简单查了一下 Python 相关的调用,似乎没有找到合适的调用函数,于是就去 C++ 里面加一个保存 HOOK 了
  其实当时是找到很是的函数了, 只是 Validator 误导了我,所以我就没有仔细去研究。

  关于 C++ 的保存调用是相当的简单,在 UPackage 下有相关的 Event 链接

alt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void FPyToolkitModule::StartupModule(){

PluginName = "PyToolkit";
Content = FPaths::ProjectPluginsDir() / PluginName + TEXT("/Content");

// NOTE 读取 Content 目录下的 settings.json 配置文件
FString JsonSetting = Content + TEXT("/settings.json");
TSharedPtr<FJsonObject> JsonObject = FPyCommandList::ReadJson(JsonSetting);
SettingObject = JsonObject->GetObjectField("script");

// 省略若干代码...

UPackage::PreSavePackageEvent.AddRaw(this, &FPyToolkitModule::OnPackageSaved);
}

void FPyToolkitModule::OnPackageSaved(UPackage *Package)
{
static FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Content"), FText::FromString(Content));
FString Path = Package->GetName();
// NOTE 读取 SettingObject 配置文件的 OnPackageSaved 属性调用相应的 py 脚本
FString ClosedScript = FText::Format(FTextFormat::FromString(SettingObject->GetStringField("OnPackageSaved")), Arguments).ToString() + FString(TEXT(" ")) + Path;
GEngine->Exec(NULL, ClosedScript.GetCharArray().GetData());
}

  通过上面的代码可以非常简单实现保存的时候调用相关的 Python 脚本的效果。
  只是要确保 Content 目录下要有 settings.json 的配置文件,配置 OnPackageSaved 项来指定调用的脚本。
  否则启动的时候会因为找不到 json 文件直接 引擎崩溃 (还是做个检测处理比较妥当(:з」∠))

Python Hook 实现

  后来非常偶然地加了一个网友,在交流的过程中,他教会了我使用 subsystem 的 Hook 实现导入的时候触发对应的 Python 函数进行处理。
  于是我在这个基础上我又仔细研究了 EditorValidatorSubsystem 也顺利利用纯 Python 来解决我最开始提到的保存触发函数的问题。

  首先回到最开始的问题,保存的时候触发相应的函数。
  可以利用 EditorValidatorSubsystem 来实现 Python文档

  这个类提供了 add_validator 方法,可以给 Asset 添加相应的 Validate 功能
  添加 validator 需要继承 EditorValidatorBase 这个类,然后重载相关的功能。

  这种操作,具体可以参考我之前写的 Unreal Python ToolMenuEntryScript 使用研究 文章
  利用 Unreal Python 继承一个 C++ 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import unreal

# NOTE 生成一个 EditorValidatorBase 类
@unreal.uclass()
class OnAssetSaveValidator(unreal.EditorValidatorBase):

# NOTE 保存资源的时候会调用这个函数
@unreal.ufunction(override=True)
def can_validate_asset(self,asset):
msg = 'Save Hook Trigger'
unreal.SystemLibrary.print_string(None,msg,text_color=[255,255,255,255])
return super(OnAssetSaveValidator,self).can_validate_asset(asset)


def main():
validator = OnAssetSaveValidator()
validator.set_editor_property("is_enabled",True)
validate_subsystem = unreal.get_editor_subsystem(unreal.EditorValidatorSubsystem)
validate_subsystem.add_validator(validator)

if __name__ == "__main__":
main()

  在 unreal 里面执行上面的代码,保存的时候就会自动打印 Save Hook Trigger 的语句了。
  经过我的测试,我发现只有 can_validate_asset 可以正常触发,其他的函数尝试 override 了,但是保存的时候没有起作用。
  不过有 can_validate_asset 触发就足够了。
  我们可以检测传入的 asset 类型,然后调用相应的保存处理函数。
  这后面可以做的事情就很多了,资源的校验,配置选项的自动处理,自动生成配套资源 等等等。
  我之前就做了一个根据当前材质实例的勾选自动切换母材质的功能。


  同样地,借助 subsystem 还可以实现 import 的 HOOK,
  具体用到了 ImportSubsystem
  其中提供了四种不同情况的调用,用得多的是 on_asset_post_import 导入之后对导入的资源进行后处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import unreal

def import_log(factory, created_object):
msg = "Import Hook Trigger"
unreal.SystemLibrary.print_string(None, msg, text_color=[255, 255, 255, 255])
print(factory, created_object)

def main():
# import_subsystem.on_asset_post_import.add_callable(OnAssetImport.import_log)
import_subsystem = unreal.get_editor_subsystem(unreal.ImportSubsystem)
# NOTE 全局变量才不会被 GC 导致 hook 失效
global on_asset_post_import_delegate
on_asset_post_import_delegate = unreal.OnAssetPostImport_Dyn()
on_asset_post_import_delegate.add_callable(import_log)
import_subsystem.set_editor_property("on_asset_post_import",on_asset_post_import_delegate)


if __name__ == "__main__":
main()

  需要注意的是 delegate 需要用全局变量指定,否则使用一段时间之后 hook 会因为 gc 问题失效。
  这个问题非常类似我当时做批量渲染工具时候遇到的问题 Unreal Python Sequencer 批量渲染总结

  如果导入资源的命名有相应的规范,就可以自动把相关的属性勾选上,特别是 Texture 之类的处理会非常好用,美术也不会忘记勾选导致各种问题。

总结

  纯 Python 的实现目前只能拘束在 Import 和 Save 两个应用场景。
  如果需要额外的应用场景比如说 打开资源的时候触发 等,就需要依靠 C++ 来实现了。