前言

  Unreal 加载的资产会保持 打开状态,导致外部无法对资产编辑。
  这在更新资产的时候会比较难受,需要关闭引擎才能删除和更新。
  后来偶然看到项目组的程序用 C++ 写了一个断开 File Handle 的调用。
  于是我参考它的方案用 Python 写了一个,调用起来比 C++ 更方便且高效。

虚幻进行资产管理的好处

  当 Unreal 读取了 uasset 文件资产之后,会通过文件句柄的方式锁定文件。
  避免外部对文件的修改。
  虚幻官方是推荐通过引擎的 Content Browser 对资产进行移动删除等的更新,这样做是有一些好处的,具体可以参考国外 reddit 的讨论 链接

官方文档参考
Working with Assets

好处

  1. 移动资产可以自动更新关联资产的引用
  2. 删除资产可以替换资产的引用信息到新的资产上
  3. 拖拽直接生成资源
  4. 生成缩略图预览

  因为虚幻的这种引用关系是嵌入在 uasset 里面的,所以如果要将资源迁移到别的项目,直接复制是会导致引用错乱的。
  解决方案是使用 Migrate 功能 官方文档参考

虚幻进行资产管理的痛点

  虚幻引擎的这一套文件管理方式也会引入一些别的问题。

  比如替换资产会引入莫名其妙的重定向器。
  重定向器的好处是保留旧资产的引用,这样不需要修改关联资产的路径,通过重定向器资产来链接到新资产。
  但是对新手来说不了解这个机制就非常的不友好,因为默认情况下重定向器是隐藏的!!
  如果命名为重定向器同名会提示命名冲突,但是隐藏的文件是看不到,就会让人很困惑。
  通常这种情况可以使用 Fix Up Redirectors in Folder 来删除隐藏的重定向器。

重定向器的问题

  下面举出具体的例子来描述遇到的问题。

alt

  目前有文件有上述的资产,想要用 Cylinder_2 替换 Cylinder

alt

  如果删除的时候没有引用关系的话,窗口会更加简洁,如上图所示。
  只是删除的时候还是需要虚幻找一遍文件的引用关系,导致删除大量无用资源的时候会比较慢。

alt

  由于 Cylinder 别隔壁的蓝图引用,直接删除 Cylinder 会弹出关联资源引用的窗口。

alt

  上面的资源替换弹窗选择 OK 之后,目录显示是很干净的。

alt

  但是当我们加一个新的资源,并且打算命名为之前的名字的时候,会发现该目录存在同名资源。

alt

  如果打开系统的文件浏览器就会发现的确有个同名小型的 uasset

alt
alt

  虚幻里面想要看到需要开启 Redirector 的过滤。

alt

  清理重定向器需要右键文件夹,选择 Fix Up Redirectors in Folder 来更新资源

alt

  清理之后就可以正常命名了。


  虚幻这么设计也是有原因的,可以参考重定向器的文档 链接
  文档里面有所提及,更新资源的引用路径需要读取资源,如果有大量引用的话可能会花费很长的时间进行读取,协同开发也会受到影响。
  重定向器可以保留旧资源,引用到新资源上,而无需对关联资源做任何的修改。
  如果不清理重定向器会导致上面或者文档里面提到的不少问题,还是挺难顶的。

痛点

  1. 协同开发很容易因为路径更新造成冲突 (二进制无法解决冲突,可以接入版本控制来避免冲突)
  2. 某些情况资产更新需要重新导入才能完整更新,直接删除资产会修改到关联资产路径。
  3. 文件句柄占用导致需要关闭引擎才能更新 uasset 资产。(这个问题可以接入 UE4 的版本控制解决)

  另外还有一个痛点,就是当我文件做了我不想要的修改的时候,如何将 dirty 标记去掉,保留原来的资源。
  这个痛点我之前都是通过关闭引擎重新打开实现的。
  写文章的时候针对这个痛点我重新搜索了一阵。
  才知道 Unreal 原来早有设置好的解决方案。 官方问答链接

  Unreal 的资源右键有 Asset Actions > Reload 功能。
  reload 即可解决上面提到的问题。

handle 切断 uasset 占用

  虽然强行切断 Unreal 的文件占用未必是件好事。
  但是实现一下这个功能,也并非什么难事。

  主要是借助了 sysinternals 系列的 handle.exe 链接
  这个 handle.exe 可以监视系统内各种句柄的状态,也可以进行切断操作。

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

Nthandle v4.1 - Handle viewer
Copyright (C) 1997-2016 Mark Russinovich
Sysinternals - www.sysinternals.com

usage: handle [[-a [-l]] [-u] | [-c <handle> [-y]] | [-s]] [-p <process>|<pid>] [name] [-nobanner]
-a Dump all handle information.
-l Just show pagefile-backed section handles.
-c Closes the specified handle (interpreted as a hexadecimal number).
You must specify the process by its PID.
WARNING: Closing handles can cause application or system instability.
-y Don't prompt for close handle confirmation.
-s Print count of each type of handle open.
-u Show the owning user name when searching for handles.
-p Dump handles belonging to process (partial name accepted).
name Search for handles to objects with <name> (fragment accepted).
-nobanner Do not display the startup banner and copyright message.

No arguments will dump all file references.

  首先可以通过 handle 的命令 查询当前进程下相关联的文件占用

1
handle.exe -p PID PATH

  pid 是进程的 id , PATH 是查询的路径。
  通过输入上述的命令可以在命令行下查询 Unreal 对相关文件的占用。

alt

  如上图所示,指定 pid 可以避免其他程序的干扰。
  但是 python 如何获取到 Unreal 的 pid 呢?
  这一步其实不需要第三方模块,用 Python 自带的功能就可以解决。

1
2
3
4
5
6
import os
import sys
print(sys.executable)
# G:\RedAppEditor\Engine\Binaries\Win64\UE4Editor.exe
print(os.getpid())
# 36688

  可以看到 Python 自身 pid 就是 Unreal 的 pid 了。
  利用这个方法就可以完成占用的解除了。

  最后就是接触占用的命令

1
handle.exe -p 36688 -y -c 4358

   -c 后面接 上面截图的获取路径前的 id
  通过上面的方式就可以解除占用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import os
import re
import sys
import subprocess
from multiprocessing.dummy import Pool

import unreal

sys_lib = unreal.SystemLibrary()
util_lib = unreal.EditorUtilityLibrary()
red_lib = unreal.RedArtToolkitBPLibrary()
paths = unreal.Paths()

DIR = os.path.dirname(__file__)
THREAD_COUNT = 8
HANDLE = os.path.join(DIR, "handle.exe")
PID = os.getpid()

def convert_to_filename(path):
content = sys_lib.get_project_content_directory()
path = path.replace("/Game/", content)
path = os.path.abspath(path).replace("\\", "/")
if os.path.isdir(path):
return path
path = os.path.splitext(path)[0]
return "%s.uasset" % path


def unreal_progress(tasks, label=u"进度", total=None):
total = total if total else len(tasks)
with unreal.ScopedSlowTask(total, label) as task:
task.make_dialog(True)
for i, item in enumerate(tasks):
if task.should_cancel():
break
task.enter_progress_frame(1, "%s %s/%s" % (label, i, total))
yield i, item


def get_release_pid(path):
commands = []
path = convert_to_filename(path)
path = paths.make_platform_filename(path)
if not os.path.exists(path):
return commands
command = " ".join([HANDLE, "-p", str(PID), path])
# NOTE 获取文件占用信息 生成断开文件占用命令
output = str(subprocess.check_output(command, shell=True))
output = output.split("www.sysinternals.com")[-1]
output = output.replace(r"\r", "")
for line in output.split(r"\n")[:-1]:
if not line:
continue
collections = re.split(r"[ ]+", line.strip())
if len(collections) < 5:
continue
pid = collections[5]
command = " ".join([HANDLE, "-c", str(pid), "-y", "-p", str(PID)])
commands.append(command)

return commands

def main():

# NOTE 接触当前目录下的资源
handle_path = red_lib.get_current_content_path()
commands = get_release_pid(handle_path)

# print("\n".join(commands))
pool = Pool(THREAD_COUNT)
if commands:
# NOTE 通过 Pool 简化多线程调用 批量解开占用
# NOTE 使用 imap_unordered 可以获取 遍历器 可以接入到 虚幻的进度条
itr = pool.imap_unordered(
lambda command: subprocess.call(command, shell=True), commands
)
for i,res in unreal_progress(itr, total=len(commands)):
pass
pool.close()
pool.join()

print(u"占用解除成功")
else:
print(u"无需解除占用")

总结

  虽然实现了这种暴力解除文件占用的功能,但是其实并不是一个好的处理。
  本地文件断开占用挪动到别的地方,老位置的内存对象依然保留这的,所以也还是会让用户感到困惑。
  这个操作也不是虚幻官方所推荐的操作,不过了解了一下 handle.exe 的用法也甚好。