UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent
本文内容可能只能在 UE5 下有用,未在 UE4 环境下实验过。
背景
遇到了一个美术需求,需要批量读取一段动画,制作成 UE 中的 Level Sequence,然后给动画添加几个 Event Track。随后,需要在 Event Track 中添加 Trigger Event,设置插件 uDraper 布料的缓存数据路径。总之,最终效果如下:
无视上图中红色的报错部分,这是因为我截图的时候没有打开对应的关卡,Sequence 找不到相关的引用。打码部分是动画名字,工程内容不太方便暴露所以打码
至于为什么非得要用 Event Track 来设置路径,而不是在 Actor 的 Component 相关属性中直接设置路径,然后添加到 Sequence 中,只能说这是 uDraper 插件的问题,直接设置会弹出个弹窗说“路径缺少 xxxx 文件”(因为该路径只有缓存数据而没有布料相关的数据),但是如果在 Event Track 中通过 Event 帧调用函数Cache(缓存路径)
设置就没这个问题,能够正常读取缓存文件。因此,就只能这么干了。
可能有点绕,其实就是我需要在动画的第一帧调用 uDraper 提供的蓝图函数 Cache
,并传入DirectoryPath
类型的对象来指定布料缓存数据路径。
另外,如果读者不太清楚或者没试过在 Level Sequence 中触发 Event,可以看看官方介绍文档,里面详细说明了如何在 Sequence 中添加 Event 帧,在指定的帧调用函数,从而实现在某个特定时刻执行某种行为、打开门、生成角色等功能。此文档中的操作流程和我们在代码中相关流程是一致的,因此后面我不会解释代码中为什么会出现某个步骤。
Python 脚本
直接进入正题,我们先看一眼完整的 Python 脚本:
def main():
# 拿到场景编辑 subsystem
level_editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
# 从关卡中获取 actor,注意获取的时候确认名字就叫这个,最好选中后用指令打印出来名字
# 因为在编辑器的场景预览中显示的名字不一定就是引擎设置的真名
actor = unreal.find_object(level_editor.get_current_level(), "guzhuang_C_1")
camera_actor = unreal.find_object(level_editor.get_current_level(), "Camera_0")
# 获取组件
cloth = unreal.find_object(actor, "cloth")
# 这里 asset_list 是一个列表,里面全是 animation 的路径,类似于 /Game/Anim/dance.dance
for i in asset_list:
# 取个名字
cur_anim = i.split("/")[-1].split(".")[0]
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
# 传入参数创建新的 sequence,seq_path 是一个全局变量,配置了 sequence 的保存路径
lvl_seq = unreal.AssetTools.create_asset(
asset_tools, asset_name=cur_anim, package_path=seq_path, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew())
# _fps 也是全局配置,保存了 sequence 的帧率
frame_rate = unreal.FrameRate(numerator=_fps, denominator=1)
lvl_seq.set_display_rate(frame_rate)
# 创建 actor binding
actor_binding = lvl_seq.add_possessable(actor)
# 添加场景中的 camera,其实可以在脚本中创建新的,但是我发现创建 camera 的话在脚本执行完后新建的
# Camera 会一直保留在场景中,所以最终还是选择直接用场景中现有的 camera
cam_cut_track = lvl_seq.add_master_track(
unreal.MovieSceneCameraCutTrack)
cam_cut_section = cam_cut_track.add_section()
# 添加 camera section
cam_binding = lvl_seq.add_possessable(camera_actor)
cam_binding_id = lvl_seq.make_binding_id(
cam_binding, unreal.MovieSceneObjectBindingSpace.LOCAL)
cam_cut_section.set_camera_binding_id(cam_binding_id)
# 添加动画
loaded_asset = unreal.AnimSequence.cast(
unreal.EditorAssetLibrary.load_asset(i))
frame_num = loaded_asset.get_editor_property(
'number_of_sampled_keys')
frame_rate = loaded_asset.get_editor_property('target_frame_rate')
# 设置 sequence 有内容的范围
lvl_seq.set_playback_start(0)
lvl_seq.set_playback_end(frame_num)
unreal.LevelSequenceEditorBlueprintLibrary.refresh_current_level_sequence()
### 添加动画 track ###
anim_track = actor_binding.add_track(
unreal.MovieSceneSkeletalAnimationTrack)
# 添加动画
anim_section = anim_track.add_section()
anim_section.set_range(0, frame_num)
anim_section.params.animation = loaded_asset
# 设置动画范围
cam_cut_section.set_range(0, frame_num)
### draper settings ###
# cloth
cloth_binding = lvl_seq.add_possessable(cloth)
cloth_track = cloth_binding.add_track(unreal.MovieSceneEventTrack)
cloth_section = cloth_track.add_event_trigger_section()
cloth_section.set_range(0, frame_num)
# 其实今天的重点在这里
cloth_channel = cloth_section.get_channels()[0]
cloth_cache_binding = unreal.SequencerTools.create_quick_binding(
lvl_seq, cloth, "Cache", False)
cloth_new_event = unreal.SequencerTools.create_event(lvl_seq, cloth_section, cloth_cache_binding, ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"])
cloth_channel.add_key(time=unreal.FrameNumber(
0), new_value=cloth_new_event)
# 此处省略对 skirt 和 waist 的操作,因为和上面 cloth 的步骤一样
# 保存最终生成的 seq
unreal.EditorAssetLibrary.save_loaded_asset(lvl_seq, False)
unreal.log("=== INFO: Seq Creation is Completed ===")
比较简短,但是我们还是来看看具体做了什么,以及涉及到的相关接口、类。
查找场景中的 Actor
首先 level_editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
,这里通过unreal.get_editor_susystem
函数(相关文档)获取了 Subsystem unreal.LevelEditorSubsystem
(相关文档)。这里其实就是获取了场景编辑器 Subsystem 后面方便我们通过这个 subsystem 对场景中的 Actor 进行访问甚至修改。
顺带一提,其 Python 调用函数可以想象成在蓝图中调用函数,实际上确实也差不太多,都是通过反射实现的,所以蓝图能调用、访问 Python 都可以调用。
在获得了 Level Editor Subsystem 之后,我们就可以调用 unreal.find_object
函数,在当前打开了的场景中寻找到我们需要绑定到 sequence 的 actor 。这里需要注意一下,find_object
中传入的 actor 名字一定要确认是引擎标识的名字,而不是在 Level Editor 中看到的名字(例如我遇到过在场景中物品名称叫做guzhuang2
,实际上引擎中记载的名字是guzhuang_C_1
),最好在场景编辑器中通过 Python(REPL),选中 actor 并执行:
actor = actor_system.get_selected_level_actors()[0]
print(actor)
确认 actor 的真实名称以及其是否保存在当前场景中(有些 actor 看起来是在当前场景中实际上可能是别的场景的 actor 的引用,可能是因为直接复制了别的场景的 actor 并粘贴到当前场景下),如果名字不对或者不是保存在当前场景中那么无法通过上面的unreal.find_object(level_editor.get_current_level(), "guzhuang_C_1")
方法找到需要的 actor。
除了寻找 actor,实际上find_object
也可以用来获取 actor 身上挂载的组件,例如上方例程中就通过find_object
来获取名为cloth
的组件。
在获取到我们需要的 actor 之后就可以开始 sequence 的创建了。
创建新的 sequence
其实就这部分:
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
# 传入参数创建新的 sequence,seq_path 是一个全局变量,配置了 sequence 的保存路径
lvl_seq = unreal.AssetTools.create_asset(
asset_tools, asset_name=cur_anim, package_path=seq_path, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew())
# _fps 也是全局配置,保存了 sequence 的帧率
frame_rate = unreal.FrameRate(numerator=_fps, denominator=1)
lvl_seq.set_display_rate(frame_rate)
先获取到 UE 辅助创建 asset 的工具类,直接AssetTools.create_asset
并传入对应参数就行,这里没什么坑也没什么可以说的。创建好之后要注意frame_rate
这块,一定要确保 sequence 的帧率是你想要的帧率。最后通过 LevelSequence 类函数set_display_rate
设置帧率即可。
添加 Binding、Track、动画
然后我们看回到一开始的 sequence 截图,对照着截图来看会更容易理解接下来要做的事情。
可以看到首先 Sequence 中会有一个对某个 actor 的引用,actor 下面有一个组件的引用(如 cloth 组件的引用),组件引用下面还有一个 Track;或者 actor 的引用下面就是直接一个 Track(如 Animation Track)。
因此对应的代码:
# 创建 actor binding
actor_binding = lvl_seq.add_possessable(actor)
# ... 省略一部分无关代码
for i in asset_list:
# 取个名字
cur_anim = i.split("/")[-1].split(".")[0]
# 省略创建 sequence 的代码...
# 添加动画
loaded_asset = unreal.AnimSequence.cast(unreal.EditorAssetLibrary.load_asset(i))
frame_num = loaded_asset.get_editor_property('number_of_sampled_keys')
frame_rate = loaded_asset.get_editor_property('target_frame_rate')
# 设置 sequence 有内容的范围
lvl_seq.set_playback_start(0)
lvl_seq.set_playback_end(frame_num)
unreal.LevelSequenceEditorBlueprintLibrary.refresh_current_level_sequence()
### 添加动画 track ###
anim_track = actor_binding.add_track(unreal.MovieSceneSkeletalAnimationTrack)
# 添加动画
anim_section = anim_track.add_section()
anim_section.set_range(0, frame_num)
anim_section.params.animation = loaded_asset
还是比较简单明了的,通过unreal.EditorAssetLibrary.load_asset
加载指定路径i
的资产,并转换为AnimSequence
格式,读取动画长度并创建 track,这里需要指定 track 的类型,例如如果是控制动画的 track,那么给 actor binding 创建 track 的时候就要指定是 unreal.MovieSceneSkeletalAnimationTrack
。创建 track 后给 track 添加 section,最后内容、有效区间都是要在 section 上进行设定。
对于本例子中 cloth
等组件过程也是类似的步骤:
# ...
cloth = unreal.find_object(actor, "cloth")
### draper settings ###
for i in asset_list:
# 取个名字,这里用动画名字,实际上可以用别的名字
cur_anim = i.split("/")[-1].split(".")[0]
# 省略上面提到过的部分 ...
# cloth
cloth_binding = lvl_seq.add_possessable(cloth)
cloth_track = cloth_binding.add_track(unreal.MovieSceneEventTrack)
cloth_section = cloth_track.add_event_trigger_section()
cloth_section.set_range(0, frame_num)
# 其实今天的重点在这里
cloth_channel = cloth_section.get_channels()[0]
cloth_cache_binding = unreal.SequencerTools.create_quick_binding(
lvl_seq, cloth, "Cache", False)
cloth_new_event = unreal.SequencerTools.create_event(lvl_seq, cloth_section, cloth_cache_binding, ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"])
cloth_channel.add_key(time=unreal.FrameNumber(
0), new_value=cloth_new_event)
前面解释过的添加绑定、创建 track、section 部分这里跳过。重点主要在创建 MovieSceneEvent
帧这里。这里主要通过 unreal.SequencerTools.create_quick_binding
创建新的 MovieSceneEvent
,并且将此 Event 与传入的成员函数(要能在蓝图中调用的函数,这里也是借助了反射来找到要绑定的函数)进行绑定。顺带一提,这里绑定的是cloth
的成员函数Cache
,实际上也可以是其他类的成员函数。例如说我们的 Actor 有一个用来更新状态的成员函数KillSelf
,那么可以变成unreal.SequencerTools.create_quick_binding(lvl_seq, actor, "KillSelf", False)
,这里的 actor
是指向 Actor 的指针。create_quick_binding
函数的用法、参数可以在官方文档查看。
创建了函数绑定之后就可以通过 unreal.SequencerTools.create_event
创建 MovieSceneEvent
了。比较值得注意的是 Payload 传入参数部分 ["(path=\"D:/UGit/XiaoLan_PC/content/XiaoLan/Effects/Character_Effects/BoWuGuan/ZhengChangShuoHua/cloth\")"]
。Cache
函数需要的参数是 unreal.DirectoryPath
类型的,而这里相当于通过传入 unreal.DirectoryPath
成员变量 path
的内容构造了一个 unreal.DirectoryPath
对象,并传入给 Cache
函数。
创建完成 MovieSceneEvent
后通过 channel.add_key
添加到 Track 中。上面提到的 Payload
的部分最终会通过 ImportText
函数处理,转换成 unreal.DirectoryPath
对象(所以如果你参数输入格式不对的话会提示 ImportText xxxx 错误
)。最终结果:
点开这些刚刚创建的帧,就会打开蓝图看到这个帧调用的函数:
上面步骤完成后,unreal.EditorAssetLibrary.save_loaded_asset(lvl_seq, False)
保存即可(第二个参数是只有内容被更改后才保存,这里不做这种判断,直接保存。其实 Python 调用的 API 都可以参考蓝图的文档)。
参考
- 官方文档:Sequencer 概述
- 官方文档:Sequencer 中的 Python 脚本:这个还是挺有用的,必看
- 官方文档:Scripting the Unreal Editor Using Python:必看,关于如何在编辑器中使用 UE
- 官方例程,在
Engine\Plugins\MovieScene\MovieRenderPipeline\Content\Python
路径下、 - 官方文档:unreal.MovieSceneEvent
- 官方文档:unreal 模块的 PythonAPI