前言

  记录一下这周搞到头秃的需求。
  需要修改修改蓝图组件的属性。
  过去我修改组件属性都是通过 Actor 实现。
  但是 Actor 需要在 Game World 当中进行修改,而这个时候其实是已经实例化了。
  这里需要将值赋值到蓝图当中,等于下次生成 Actor 的时候已经自动带有配置好的初始值了。

  原先我觉得 Actor 修改就是分分钟的事,蓝图相信也可以直接获取进行修改吧。
  然而我是图样图森破了(:з」∠)

修改蓝图组件属性尝试

  我之前修改资源的属性,比如说修改 Texture2D 资源,都需要通过 Content Browser 获取当前选择的 Asset 资源。
  然后通过 Python 的 set_editor_property 来实现修改。

  所以最开始我也是沿着这个方向去尝试的。

1
2
3
4
5
import unreal
assets = unreal.EditorUtilityLibrary.get_selected_assets()
print(assets)

# LogPython: [Blueprint'"/Game/sequence/NewBlueprint.NewBlueprint"']

  通过上面的方法可以获取到当前选择的 蓝图 Asset。
  但是去 Python 文档查 Asset 完全没有任何有价值的方法可以获取到它内部的 Component 链接

  最初是想通过 Actor 的 component 看看有没有办法获取到 蓝图 内部的 Component , 然并卵 ~
  于是我开始走弯路了。
  我想到蓝图编辑器窗口既然可以获取到这些 Component ,应该会有相关的线索的。
  所以我开始查阅引擎的源码,希望能够从中找到解决方案~

蓝图编辑窗口 SCSEditor 源码查阅

  首先要快速定位蓝图编辑器的源码位置。
  依然是使用老办法,通过 Tooltip 来定位最快。

alt

  然后可以定位到 SBlueprintEditorToolbar.cpp 脚本,并且可以定位模块在引擎的 Kismet 文件夹里面。
  由于这里面的代码已经非常多了,所以新开一个 VScode 在 Kismet 目录下进行快速的搜索。
  下一步我要获取的左侧的 Component 树状图的结构。
  于是我又找了个简单的 Tooltip 来定位

alt

  这里可以直接定位到 SCSCEditor.cpp 脚本,
  然后就开始了我更大弯路的阅读源码,其中还去找了右侧的 Detail 界面生成逻辑去看,但是其实是我想多了。
  我只要找到 蓝图的 Component 是怎么生成出树的即可。

  后来总算找到了对的 函数 OnApplyChangesToBlueprint
  当蓝图的 Component 有任何的变动应该就会触发这个函数来进行 组件树的更新

alt

  Component 更新相关的逻辑就是红框框住的部分。
  可以看到这其中有一个关键操作。

1
2
3
AActor* Actor = GetActorContext();
// 代码省略 ...
AActor* BlueprintCDO = Actor->GetClass()->GetDefaultObject<AActor>();

  这个操作通过 Actor 获取类,然后再通过 类 获取 DefaultObject 来进行操作。
  这个变量的命名就很灵性,于是去查一下 unreal cdo 相关的内容,然后可以追查到 AnswerHub 链接

  根据最佳回答可以知道, CDO 是 Unreal 的 Class Default Object 的简称。
  专门用来存储 类 的实例化初始值对象,也就是当 类实例化的时候,会通过反射系统从 CDO 对象中取值。
  所以针对蓝图这种就更为重要,因为蓝图其实是一个 C++ 类生成器,可以简单理解为 Compile 的时候生成相应的 C++ 类以及对应的 Class Default Object
  进而通过 Class Default Object 来衍生出其他的实例。

  所以修改蓝图 Component 的属性,本质上就是要修改 Class Default Object 即可。

Python 尝试修改 CDO

  通过上面的 C++ 源码,我想到在游戏场景里面获取 Actor 然后模仿上面的方式获取类。

1
2
3
4
5
6
import unreal
actors = unreal.EditorLevelLibrary.get_selected_level_actors()
for actor in actors:
print(actor.get_class())

# LogPython: <Object '/Game/sequence/NewBlueprint.NewBlueprint_C' (0x00000209683ADD00) Class 'BlueprintGeneratedClass'>

  果然 Actor 获取到的 Class 是特殊的 BlueprintGeneratedClass
  然而在 Python 里面也提供太多特殊的调用方式。
  我用 dir 列出这个类可以调用的方法,发现还有 get_default_object 方法,继承于 unreal._ObjectBase 链接

alt

1
2
3
4
5
6
import unreal
actors = unreal.EditorLevelLibrary.get_selected_level_actors()
for actor in actors:
print(actor.get_class().get_default_object())

# LogPython: LogPython: <Object '/Script/Engine.Default__BlueprintGeneratedClass' (0x0000020943CB0100) Class 'BlueprintGeneratedClass'>

  然而类的显示路径虽然变了,但是实际上的类型是不对的,我要获取到 Actor 类型才是对的。
  于是我又搜索了一下关于 Python 蓝图 修改 CDO 相关的信息,还真的搜到官方的工程师提供的方案 链接
  没想到官方工程师给的方案更加简单,可以直接从蓝图路径中获取到 CDO ,只需要在路径上加上 _C 后缀 ,Actor获取也给省了。
  我测试了官方的代码,通过 unreal.get_default_object 这个方法获取到的 object 是 Actor 类型,是对的。

1
2
3
4
5
6
7
import unreal
bp_gc = unreal.load_object(None, "/Game/sequence/NewBlueprint.NewBlueprint_C")
bp_cdo = unreal.get_default_object(bp_gc)
print(bp_cdo)
print(type(bp_cdo))
# LogPython: <Object '/Game/sequence/NewBlueprint.Default__NewBlueprint_C' (0x000002096AEC4D00) Class 'NewBlueprint_C'>
# LogPython: <type 'Actor'>

  本来以为官方提供的就是最佳解决方案,困扰良久的问题总算解决了。
  最后证明我还是太Naive

  当我获取 Actor 的 root_component 之后,我发现没法获取 component。

1
2
3
4
5
6
7
8
9
import unreal
bp_gc = unreal.load_object(None, "/Game/sequence/NewBlueprint.NewBlueprint_C")
bp_cdo = unreal.get_default_object(bp_gc)
print (bp_cdo.root_component)
print (bp_cdo.get_component_by_class(unreal.ActorComponent))
print (bp_cdo.get_component_by_class(unreal.SceneComponent))
# LogPython: None
# LogPython: None
# LogPython: None

  上面测试用的蓝图比较简单,我当时用项目的蓝图还是可以获取到 root_component 但是获取不到它的子对象。
  于是到头来 Python 还是无法解决这个问题。

C++ 获取 CDO Component

  我又追查了一下 C++ 的 API 。
  发现 Actor 可以通过 GetComponents 获取

1
2
3
4
5
6
7
8
9
10
11
12
TArray<UActorComponent *> UPyToolkitBPLibrary::GetCDOInheritedComponents(AActor *CDO)
{
TArray<UActorComponent *> ComponentArray;
TInlineComponentArray<UActorComponent *> Components;
CDO->GetComponents(Components);
for (UActorComponent *Component : Components)
{
ComponentArray.Add(Component);
}

return ComponentArray;
}

  但是只是这样还是没法获取全部的 Component , 一些后续添加的 Component 还是通过 Python 无法获取。
  于是我又开始去查 SCSCEditor.cpp 的源码,因为我相信既然可以生成组件树,一定是可以获取到 Component 的。
  但是当时总是想着 Component 的线索去追查,完全没想到这个东西居然和 Node 有关。
  所以就踩了个巨坑,浪费了好多时间,然后最后实在没办法了,找了程序来帮忙,然后程序也没有搞过,还是得自己查。
  最后又从网上挣扎了一下,没想到找到了解决方案 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
TArray<UActorComponent *> UPyToolkitBPLibrary::GetCDONodeComponents(AActor *CDO)
{
UBlueprintGeneratedClass *ActorBlueprintGeneratedClass = Cast<UBlueprintGeneratedClass>(CDO->GetClass());

// NOTE https://answers.unrealengine.com/questions/558236/how-to-get-a-component-from-a-classdefaultobject.html
TArray<UActorComponent *> Components;
const TArray<USCS_Node *> &ActorBlueprintNodes = ActorBlueprintGeneratedClass->SimpleConstructionScript->GetAllNodes();
for (USCS_Node *Node : ActorBlueprintNodes)
{
Components.Add(Node->ComponentTemplate);
}
return Components;
}

  SimpleConstructionScript 获取到 USimpleConstructionScript 这个类
  这个类是 蓝图生成 Actor 的时候调用给 Actor 添加组件用的。
  这里并不包含所有的 Component ,只有一些蓝图额外添加的 Component 才会放到这里。

  还好有人已经做好了获取的参考代码,要我自己查到是这么个逻辑估计得天荒地老了(:з」∠)
  于是通过上面这个蓝图的暴露就可以获取到 蓝图 中所有的 Component (但是是没有层级关系的)
  直接获取的 Component 中用 set_editor_property 设置值,就可以实现蓝图 Component 的修改了~

  由于项目使用了一些自定义的 Component 组件,每每都要去查 C++ 源码看有什么 property 可以设置是非常不方便的。
  幸好之前推荐多次的 Unreal Python 教程已经提供了解决方案 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TArray<FString> UPyToolkitBPLibrary::GetAllProperties(UClass *Class)
{
TArray<FString> Ret;
if (Class != nullptr)
{
for (TFieldIterator<FProperty> It(Class); It; ++It)
{
FProperty *Property = *It;
if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit))
{
Ret.Add(Property->GetName());
}
}
}
return Ret;
}

  最新版本的引擎提示 UProperty 改名为了 FProperty
  目前编译可以通过,估计下一个版本就会直接报错了,所以这里我稍稍修改了一下。

  通过这个方法就可以确切知道组件下有什么属性可以通过 Python 进行设置了。

2021-6-1 更新 - 不需要 C++ 更新

  最近才发现 CDO 对象可以直接获取到子对象,不需要 C++ 辅助这么麻烦。
  不过这个获取也只限于继承于 C++ 的蓝图,如果是蓝图里面手动添加的,依然无法直接通过 cdo 对象获取
  但是如果是实例化到场景中的 Actor ,倒是可以直接 get_editor_property 获取到

1
2
3
4
5
6
7
8
9
10
import unreal
asset_lib = unreal.EditorAssetLibrary
# NOTE 这个蓝图是新建继承 character 的蓝图
bp = asset_lib.load_asset('/Game/test/NewBlueprint1.NewBlueprint1')
bp_gc = unreal.load_object(None, "%s_C" %bp.get_path_name())
bp_cdo = unreal.get_default_object(bp_gc)
print (bp_cdo.get_editor_property("CapsuleComponent"))
print (bp_cdo.get_editor_property("mesh"))
# LogPython: <Object '/Game/test/NewBlueprint1.Default__NewBlueprint1_C:CollisionCylinder' (0x000001844AE95D00) Class 'CapsuleComponent'>
# LogPython: <Object '/Game/test/NewBlueprint1.Default__NewBlueprint1_C:CharacterMesh0' (0x0000018469623040) Class 'SkeletalMeshComponent'>

  获取到的对象可以直接进行修改。

1
2
capsule = bp_cdo.get_editor_property("CapsuleComponent")
capsule.set_editor_property("capsule_radius",10)

  如上可以设置胶囊体的半径,但是需要关闭蓝图窗口再打开才能看到视图的更新

总结

  一个人搞 Unreal Python 就是痛苦无比,所有的雷都得自己解决。
  解决需求根本无法评估到底需要多少时间(:з」∠),这次就踩到了天坑.
  真正解决问题的代码其实也就那么几行就够了,但是走了弯路就耗费了好多倍的时间o(╥﹏╥)o
  ε=(´ο`*)))唉,但是就像我昨天吐槽的一样, C++ 的开发效率实在是太低了,能用 Python 还是尽量用 Python 解决了。