前言

  Unreal 引擎有个非常蛋疼的点,就是文件无法直接通过路径进行定位。
  虽然 Content Browser 可以直接搜索文件,但是文件如果很多的时候,搜索起来其实并不方便。
  反而很多时候是别人把路径的截图发过来,还需要自己手动去点点点各种层级目录去到目标位置。
  这个过程真是太痛苦了,于是我就想写一个启动器来解决这个问题。

  怎么样定位到 Content Browser 的对应位置,其实并不难。
  Unreal 的官方 Python 已经提供了相应的处理方法 链接

  只要输入 assets_path 路径数组,就可以让 Content Browser 自动选择对应的物体。
  不过 Python 有个缺点,无法选择文件夹。
  这个问题可以参照 Unreal Python 的教程来解决 ~ 虚幻引擎使用Python开发 How To Use Python Inside Unreal Engine 4
  github 路径地址

  所以要实现这个工具的难点在于如何通过快捷键触发 Launcher 实现输入交互。
  实现的效果大体如下 ↓↓↓

Qt 快捷键触发

  毕竟 Maya 开发用多了,快捷键触发首先就想到了使用 Qt 的事件进行触发。
  之前开发过 CommandLauncher 插件,通过 eventFilter 可以实现诸多黑科技 ~
  所以这次也想照抄差不多的思路。

  在这里才发现之前开发 ComamndLauncher 还是踩了一些坑。
  应为是开发启动器,所以要实现点击外部的时候就要自动消失的功能。
  我之前是通过 eventFilter 来过滤点击事件实现的。
  其实 Qt 有更简单的实现方法,这个是我看 dayu_widgets 的 MDrawer 实现里面学到的。

1
self.setWindowFlags(QtCore.Qt.Popup)

  只要组件设置了 Popup 标记,组件会自动变成没有窗口边框的模式,并且点击组件外部会自动消失。
  所以 Qt 都贴心地把这些功能实现好了(:з」∠)


  上面都是题外话了,这次还是打算用 eventFilter 来过滤键盘事件。
  从而实现在 Unreal 引擎的任意位置敲快捷键可以触发 Launcher
  然而现实并不如我所愿,组件一旦消失了之后,eventFilter 获取到的事件就只有定时的 Timer 事件了。
  键盘输入等等都将不起作用。
  考虑到 processEvent 执行,我也尝试在 tick 里面将 processEvent 启动起来。
  然而还是不行。

  看来很可能是因为没有 QWidget 在运行了,QApplication也没有阻塞 Unreal 的执行, 而 Unreal 本身也有一套触快捷键的机制,也可能是被 Unreal 的机制截取了。
  反正 Qt 的快捷键只有在运行的工具界面上才起作用。

Unreal 自定义快捷键触发自定义命令

  所以要解决这个问题,只能在 Unreal 引擎上面入手。
  通过自定义快捷键命令来触发 Launcher 。
  但是 Unreal 怎么设置快捷键呢?
  最先想到的就是 游戏模式下有 Input Event 相关的蓝图。
  可以进行任意按键的扩展。

  然而我在 Editor Utility 里面测试并不能这么触发游戏用的键盘事件,只有游戏蓝图可以触发这些事件。

alt

  那如何才能在 Unreal 中注册一个快捷键呢?
  于是网上搜了 customize 相关的关键字。
  结果只是找到了 虚幻引擎 设置里面可以设置已有命令的快捷键。

alt

  既然能找到引擎可以设置快捷键的命令,那么可以沿着这条线索去搜索引擎源码里面包含这些快捷键命令,去查它的快捷键是怎么实现的。
  于是我找了个比较好观察的命令, F 键聚焦命令。

alt

  首先查 C++ 源码

alt

  可以定位到命令的名称,下面就是要找命令映射执行的。
  虚幻的聚焦命令其实是在命令行里面执行 CAMERA ALIGN ACTIVEVIEWPORTONLY
  所以配合着定位就很快就找到我们的目标代码。

alt

  基本上通过上述流程就可以知道虚幻是先声明 UI_COMMAND 命令定义命令的描述和触发基本信息。
  然后通过 MapAction 命令来映射到触发执行的命令。

  但是这样还是没能解决我们的疑问,快捷键是如何触发到,又是如何触发 MapAction 之后的命令的。
  因此需要追查执行了 MapAction 的 LevelEditorCommands 的调用。
  这中间踩了一些坑,不过这里就直奔主题了。

alt

  LevelEditorCommands 是专门提供给 LevelEditor 这个 Slate 调用的命令列表, 查 Slate 的类可以查到有很多类似 Qt 的事件回调函数。
  从我们这里快捷键相关的 OnKeyDown 函数可以找到 Viewport 快捷键触发的时候会执行 ProcessCommandBindings
  ProcessCommandBindingsFUICommandList 这个类的派生方法

alt

  从上面的截图可以看到 ProcessCommandBindings 会获取注册的所有 Command ,然后注意判断当前触发是否符合 Command 的描述。
  最后会调用 Command 的 Action 来执行。

  这个就是 Unreal Slate 快捷键触发的时候进行的处理流程。
  所以如果要自定义快捷键,那么需要将相应的命令添加到 LevelEditorCommands 里面。
  于是在查一下 LevelEditor 相关的文档,可以定位到 AppendCommands 命令。

alt

  源码上就是直接进行将额外的 FUICommandList 添加到 LevelEditorCommands

Unreal 自定义快捷键命令扩展

  由于第一次进行命令扩展,最好能在源码里面找到个相对简单的例子,然后抄一抄代码。
  于是我搜索 TSharedPtr<FUICommandList> 找个简单的例子,还好很快就找到非常简单的例子 AdvancedPreviewSceneCommands.cpp

alt

  这个例子刚好是 A 开头的,非常好找。
  而且代码也很少,只有三个 UI_COMMAND。
  后面就是模仿着这个命令扩展写一个 cpp 和 h 文件。
  再然后就是 Plugin 的代码里面引入这个命令,然后获取 ILevelEditor 执行 AppendCommands
  这里遇到了一个坑,如果插件初始化的时候就执行的话,还没有生成 SLevelEditor 组件。
  所以获取不到,会导致程序崩溃。

  所以后面我将代码放到 Python 激活的 Tick 里面调用。
  下面是截取加上注释的代码

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

// ... Python 执行 initialize.py

// NOTE 获取 LevelEditor 模块
FLevelEditorModule &LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
// NOTE 通过 LevelEditor 模块获取当前的 ILevelEditor
TSharedPtr<ILevelEditor> LevelEditor = LevelEditorModule.GetFirstLevelEditor();

// NOTE 通过 LevelEditor 模块获取当中的命令
TSharedRef<FUICommandList> CommandList = LevelEditorModule.GetGlobalLevelEditorActions();

// NOTE 定义临时的结构体用来传递回调函数 (也可以用 Lambda | 只是 Lambda 报错无法定位)
struct Callback
{
static void RunCommand()
{
FString LauncherScript = TEXT("py \"") + FPaths::ProjectPluginsDir() / TEXT("PyToolkit/Content/UE_Launcher/launcher.py") + TEXT("\"");
GEngine->Exec(NULL, LauncherScript.GetCharArray().GetData());
}
};

// NOTE 映射 UI_COMMAND 和执行命令
CommandList->MapAction(
FPyCommandList::Get().OpenLauncher,
FExecuteAction::CreateStatic(&Callback::RunCommand)
);
// NOTE 将命令添加到 LevelEditor 里面
LevelEditor->AppendCommands(CommandList);

  以上就是在 Unreal 上注册自定义快捷键的操作。
  和 Qt 不同,这个快捷键触发命令是根据 Slate 划分的,不同的 Slate 需要加到不同的 FUICommandList 里面
  所以我这里的添加只能在 LevelEditor 下触发,如果打开什么蓝图界面之类的窗口都无法触发这个快捷键了。
  我这个流程做全局快捷键的确不太方便,不知道 Unreal 有没有相关暴露方法来简化操作。
  也有可能 Unreal 本身设计就是不期望有全局统一的快捷键触发的。

Launcher 启动器

  完成了上面最难的操作之后。
  启动器本身反倒是没有什么难度了。

  主要是判断路径是否合法,如果不合法给用户提示,如果合法就进行跳转。
  这里支持判断文件的 uasset 本地路径和 Unreal 里面获取的引用路径。
  背后都是转换到引用路径进行判断的。


  开发中也有遇到一些坑。
  默认的组件方方正正的,不太好看,于是我想加个圆角。
  网上查一下也不难, Stack Overflow 上有 链接
  但是这么处理并不透明。

alt

  但是这么处理并不透明。
  网上查的结果都是 透明背景

1
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)

  但是我已经设置了,还是有黑色背景。
  后来发现居然是 setWindowFlags Popup 的锅,只要在加上一个无边框命令就正常了。

1
2
self.setWindowFlags(QtCore.Qt.Popup|QtCore.Qt.FramelessWindowHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)

  这么设置之后的确是不会有黑色背景了,但是却变成了全透明。

alt

  后来在查了一下,原来我上面 Stack Overflow 的链接操作是要在 PaintEvent 里面重新附着颜色才行的。
  我当时注释掉了设置渐变颜色的代码,结果就变成全透明了。

总结

  以上就是这次开发路径定位启动器的坑。
  其实启动器开发完成之后,后面的玩法很多样,只是不知道美术人员是否喜欢这种类似 Listary 启动各种命令的操作。
  最近在研究通过插件嵌入 Unreal 的菜单,下一篇就来讲讲怎么扩展 Unreal 的右键菜单。
  其实和 快捷键 的操作有共通的地方。