UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent

UE5 中用 Python 接口创建 Level Sequence 与设置 TriggerEvent

本文内容可能只能在 UE5 下有用,未在 UE4 环境下实验过。

背景

遇到了一个美术需求,需要批量读取一段动画,制作成 UE 中的 Level Sequence,然后给动画添加几个 Event Track。随后,需要在 Event Track 中添加 Trigger Event,设置插件 uDraper 布料的缓存数据路径。总之,最终效果如下:

20221017111235
无视上图中红色的报错部分,这是因为我截图的时候没有打开对应的关卡,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_system = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
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 截图,对照着截图来看会更容易理解接下来要做的事情。

20221017111235

可以看到首先 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 错误)。最终结果:

20221019093854

点开这些刚刚创建的帧,就会打开蓝图看到这个帧调用的函数:

企业微信截图_16660116796034

上面步骤完成后,unreal.EditorAssetLibrary.save_loaded_asset(lvl_seq, False) 保存即可(第二个参数是只有内容被更改后才保存,这里不做这种判断,直接保存。其实 Python 调用的 API 都可以参考蓝图的文档)。

参考

  1. 官方文档:Sequencer 概述
  2. 官方文档:Sequencer 中的 Python 脚本:这个还是挺有用的,必看
  3. 官方文档:Scripting the Unreal Editor Using Python:必看,关于如何在编辑器中使用 UE
  4. 官方例程,在Engine\Plugins\MovieScene\MovieRenderPipeline\Content\Python路径下、
  5. 官方文档:unreal.MovieSceneEvent
  6. 官方文档:unreal 模块的 PythonAPI
  7. UDN文档:UE4 Sequencer Python Cookbook:可能需要开发者账号,不过内容不多可以不看
  8. Python in Unreal Engine | Inside Unreal
  9. PythonSamples
  10. 官方文档:Setting up Autocomplete for Editor Python Scripting
posted @ 2022-10-19 20:54  夜溅樱  阅读(406)  评论(0编辑  收藏  举报