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

2022-10-31 17:59:52 浏览数 (1)

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 脚本:

代码语言:javascript复制
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 并执行:

代码语言:javascript复制
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

其实就这部分:

代码语言:javascript复制
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)。

因此对应的代码:

代码语言:javascript复制
# 创建 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 等组件过程也是类似的步骤:

代码语言:javascript复制
# ...
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 都可以参考蓝图的文档)。

参考

  1. 官方文档:Sequencer 概述
  2. 官方文档:Sequencer 中的 Python 脚本:这个还是挺有用的,必看
  3. 官方文档:Scripting the Unreal Editor Using Python:必看,关于如何在编辑器中使用 UE
  4. 官方例程,在EnginePluginsMovieSceneMovieRenderPipelineContentPython路径下、
  5. 官方文档:unreal.MovieSceneEvent
  6. 官方文档:unreal 模块的 PythonAPI

0 人点赞