• Unreal PythonScriptPlugin


    Unreal PythonScriptPlugin

    索引

    Python APIPythonAPI
    PyStub 生成入口FPyWrapperTypeRegistry::GenerateStubCodeForWrappedTypes
    Python API 生成入口PyOnlineDocsWriter::GenerateFiles
    Python VM 启动入口FPythonScriptPlugin::InitializePython
    Python 定义 UClass 后刷新FPyWrapperTypeReinstancer::ProcessPending
    import 入口FPythonScriptPlugin::ImportUnrealModule

    工具链

    • debugger
    • pip install
    • unit test
    • profiler
    • hot reload

    一些问题合集

    // 也许是我不知道

    • 不支持热更,编辑器扩展要重启编辑器,太蠢了
      • 保存 .py 文件要刷新下 UE 反射类型
    • 仅支持编辑器
      • 尽管反射可以动态调用 90% 的情况,但是哪怕剩下 10% 的静态导出,也需要花费很多功夫来支持额外的 Python 语言,因此只搞了编辑器时
    • Python 定义 UClass 无法序列化到 uasset
      • maybe 搞个代理蓝图,参考 Puerts

    能做什么

    TODO,太长了。

    https://vannyyuan.github.io/2021/01/26/unreal/UnrealPython%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/#L19-%E6%B7%BB%E5%8A%A0%E5%8A%A8%E7%94%BB%E5%BA%8F%E5%88%97

    https://github.com/FXTD-ODYSSEY/Unreal-PyToolkit

    • 资源规范检查
    • 资源导入导出
    • 资源工具,比如自动给动画添加 socket
    • 编辑器界面

    Why

    Python 脚本扩展 DCC 编辑器已经是常规操作了

    UE 官方的 Python 插件目前只用于编辑时扩展,但是文档和分析几乎没有

    20tab 的 UnrealEnginePython 是一个非官方的 Python 插件,编辑器和 Runtime 都可以跑,但是不再维护了

    总之,还是自己跟下源码

    Python vs UnLua

    UnLua 的主要缺陷在于缺乏类型和强大的 IDE

    • TArray 没法提示
    • IDE 缺乏重构
    • 在 Lua 无法进行 OOP
    • 无法在 Lua 定义 UClass,UProperty

    UnLua 用了一年,其实日常 UI 开发功能齐全,维护脚本系统省心不少

    但是当我们想要更进大范围使用脚本开发时,最主要的问题就是,UnLua 基本不能独立完成功能开发,只能在叶子节点扩展或者换掉 UFunction 来勾进游戏流程

    官方文档

    https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python/index.html

    • init_unreal.py,初始化脚本,可以额外配置
    • Python 路径,默认如下,可以额外配置
    • Python API
      • 支持 UE 反射得到的 蓝图 API
      • Python 类型和 UE 类型映射,支持 isinstance 和 type
      • Stub 提示文件,Engine\Plugins\Experimental\PythonScriptPlugin\SphinxDocs
      • 编辑器相关
        • Undo/Redo
        • 慢任务进度条
    • RemoteExecution 走 socket 远程执行脚本通信,略

    Python 路径

    项目文件夹中的 Content/Python 子文件 夹。
    
    主虚幻引擎安装中的 Content/Python 子文件 夹。
    
    每个启用的插件文件夹中的 Content/Python 子文件 夹。
    
    用户目录中的Documents /UnrealEngine/Python文件夹。
    例如,在 Windows 10 上,这相当于C:/Users/Username/Documents/UnrealEngine/Python
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    DoString

    执行没什么,就是准备了 unreal 的环境注入 unreal API

    启动

    TODO 看眼反射怎么export到python的

    test_type_hints.py

    => unreal.py

    类型很关键,这个包含 Engine 和 Game 所有蓝图反射 API,类似 UnLua Intellisense

    理想的 type hint,不需要手工标记类型就能各种跳转

    目前实测下来:

    • 支持 UnLua 所有特性
    • 支持泛型,类似 GetComponent
    • 不支持 MulticastDelegate,不知道回调具体参数类型
    • 不支持容器泛型 TArray/TMap,不知道 Item 类型
      • 需要这么写:exampleArray : unreal.Array[str] = unreal.Array(str) # An array of strings

    泛型

    def get_editor_subsystem(subsystem: Union[Class, type[_EditorSubsystemTypeVar]]) -> Optional[_EditorSubsystemTypeVar]:
    
    • 1

    emmylua 不支持

    ---@param Class TSubclassOf_UEditorSubsystem_
    ---@return UEditorSubsystem
    function UEditorSubsystemBlueprintLibrary.GetEditorSubsystem(Class) end
    
    • 1
    • 2
    • 3

    test_wrapper_types.py & PyTest.h

    Python 读取 UE 反射系统:

    • uproperty => Python Property,封装了 getter / setter,对 readonly 有一定保护性
    • UStruct/UObject 构造 => 统一 Python 构造函数

    Python 定义 UClass 等(即写入 UE 反射系统):

    • 定义完了要通知 UE 刷新下类型
    • override,不知道。。最多也就是 unlua override
    @unreal.uclass()
    class PyTestPythonDefinedObject(unreal.PyTestObject):
        @unreal.ufunction(override=True)
        def func_blueprint_implementable(self, value):
            return value * 2
    
        @unreal.ufunction(override=True)
        def func_blueprint_native(self, value):
            return value * 4
    
        @unreal.ufunction(override=True)
        def func_blueprint_native_ref(self, struct):
            struct.int *= 4
            struct.string = "wooble"
            return struct
    
    
    @unreal.uenum()
    class PyTestColor(unreal.EnumBase):
        RED = unreal.uvalue(1, meta={"DisplayName": "Red (255, 0, 0)"})
        GREEN = unreal.uvalue(2)
        BLUE = unreal.uvalue(3)
    
    unreal.flush_generated_type_reinstancing()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    flush_generated_type_reinstancing

    UnrealEd 有一个 Reload 模块专门处理类似 BP 定义 UClass 的事情,最后调一下 Reload->Reinstance(); 进行刷新类型

    其实 UEnum 不需要 Reinstance

    UCLASS(BlueprintType, Transient)
    class UPythonGeneratedClass : public UClass, public IPythonResourceOwner
    
    UCLASS()
    class UPythonGeneratedStruct : public UScriptStruct, public IPythonResourceOwner
    
    UCLASS()
    class UPythonGeneratedEnum : public UEnum, public IPythonResourceOwner
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    unreal_core.py

    • 做了一些 IO 重定向
    • UE 反射系统包装,比如 uclass 实际上会调用 generate_class,把一个纯 Python 类包装到 UPythonGeneratedClass,注册进 UE 反射系统
      • generate_class
      • generate_struct
      • generate_enum
      • ValueDef
      • PropertyDef
      • FunctionDef
    def uclass():
    	'''decorator used to define UClass types from Python'''
    	def _uclass(type):
    		generate_class(type)
    		return type
    	return _uclass
    	
    def ustruct():
    	'''decorator used to define UStruct types from Python'''
    	def _ustruct(type):
    		generate_struct(type)
    		return type
    	return _ustruct
    	
    def uenum():
    	'''decorator used to define UEnum types from Python'''
    	def _uenum(type):
    		generate_enum(type)
    		return type
    	return _uenum
    
    def uvalue(val, meta=None):
    	'''function used to define constant values from Python'''
    	return ValueDef(val, meta)
    	
    def uproperty(type, meta=None, getter=None, setter=None):
    	'''function used to define UProperty fields from Python'''
    	return PropertyDef(type, meta, getter, setter)
    
    def ufunction(meta=None, ret=None, params=None, override=None, static=None, pure=None, getter=None, setter=None):
    	'''decorator used to define UFunction fields from Python'''
    	def _ufunction(func):
    		return FunctionDef(func, meta, ret, params, override, static, pure, getter, setter)
    	return _ufunction
    
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    unreal_pythonautomationtest.py

    单元测试,Automation 之类,不着急

    GeneratePreStreamingForCurrentQueue.py

    使用 CinePrestreamingEditorSubsystem 为 Movie Render Queue 中的作业生成预加载资源

    • 编辑器下直接跑一段脚本,去调用了 UE Subsystem 接口和回调
    • delegate 生命周期问题,这个例子有些困惑,不应该这么麻烦
    • 命名规则,重新搞了一套,Python 没有 U/A/F 前缀,属性和函数变成蛇形命名规范,有点麻烦其实,搜引擎源码不方便;但是已经定下了,保持统一
    # Python needs to keep a reference to the delegate, as the delegate itself
    # only has a weak reference to the Python function, so this lets the Python
    # reference collector see the callback function and keep it alive.
    global delegate_callback
    delegate_callback = prestreaming_subsystem.on_asset_generated
    
    • 1
    • 2
    • 3
    • 4
    • 5

    BasicImport.py

    编辑器下 Spawn Actor,然后 Import

    • 没什么特别的
    import unreal
    
    editor_actor_subsytem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
    
    actor = editor_actor_subsytem.spawn_actor_from_class(unreal.Actor, unreal.Vector())
    
    import_context = unreal.DatasmithInterchangeImportContext()
    import_context.asset_path = "/Game/Python/Datasmith/Interchange/Assets/"
    import_context.anchor = actor.get_editor_property('root_component')
    import_context.async_ = False
    import_context.static_mesh_options.remove_degenerates = False
    
    tessel_options = unreal.DatasmithCommonTessellationOptions()
    tessel_options.options.chord_tolerance = 20.0
    
    import_context.import_options.append(tessel_options)
    
    result = unreal.DatasmithInterchangeScripting.load_file("D:/Models/SLDWKS/mouse_01.SLDPRT", import_context)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    MoviePipelineExampleRuntimeExecutor.py

    • python override C++ UFunction
      • 相当于 UnLua override,Puerts mixin override
      • 写法不太一样
    • python 定义 static UFunction
      • 仅限 static 函数
      • 可以被蓝图调用,但是需要编辑器启动时加载 python 定义代码,否则蓝图编译不过
        • 在 init_unreal.py 里 import 一下
      • 派生 unreal.BlueprintFunctionLibrary 就可以写一些 python util 函数暴露给蓝图了,常用技巧
    • python 定义 UProperty
      • 需要 class scope 声明类型,因为加载 UClass 时就要确定属性
    @unreal.uclass()
    class MoviePipelineExampleRuntimeExecutor(unreal.MoviePipelinePythonHostExecutor):
        # Declare the properties of the class here. You can use basic
        # Python types (int, str, bool) as well as unreal properties.
        # You can use Arrays and Maps (Dictionaries) as well
        activeMoviePipeline = unreal.uproperty(unreal.MoviePipeline)
        exampleArray = unreal.Array(str) # An array of strings
        exampleDict = unreal.Map(str, bool) # A dictionary of strings to bools.
        
        # Constructor that gets called when created either via C++ or Python
        # Note that this is different than the standard __init__ function of Python
        def _post_init(self):
            # Assign default values to properties in the constructor
            self.activeMoviePipeline = None
            
            self.exampleArray.append("Example String")
            self.exampleDict["ExampleKey"] = True
    
        # This is NOT called for the very first map load (as that is done before Execute is called).
        # This means you can assume this is the resulting callback for the last open_level call.
        @unreal.ufunction(override=True)
        def on_map_load(self, inWorld):
            # We don't do anything here, but if you were processing a queue and needed to load a map
            # to render a job, you could call:
            # 
            # unreal.GameplayStatics.open_level(self.get_last_loaded_world(), mapPackagePath, True, gameOverrideClassPath)
            # 
            # And then know you can continue execution once this function is called. The Executor
            # lives outside of a map so it can persist state across map loads.
            # Don't call open_level from this function as it will lead to an infinite loop.
            pass
            
        # This declares a new UFunction and specifies the return type and the parameter types
        # callbacks for delegates need to be marked as UFunctions.
        @unreal.ufunction(ret=None, params=[unreal.MoviePipeline, bool])
        def on_movie_pipeline_finished(self, inMoviePipeline, bSuccess):
            # We're not processing a whole queue, only a single job so we can
            # just assume we've reached the end. If your queue had more than 
            # one job, now would be the time to increment the index of which
            # job you are working on, and start the next one (instead of calling
            # on_executor_finished_impl which should be the end of the whole queue)
            unreal.log("Finished rendering movie! Success: " + str(bSuccess))
            self.activeMoviePipeline = None
            self.on_executor_finished_impl()
            
        @unreal.ufunction(ret=None, params=[str])
        def on_socket_message(self, message):
            # Message is a UTF8 encoded string. The system expects
            # messages to be sent over a socket with a uint32 to describe
            # the message size (not including the size bytes) so
            # if you wanted to send "Hello" you would send 
            # uint32 - 5
            # uint8 - 'H'
            # uint8 - 'e' 
            # etc.
            # Socket messages sent from the Executor will also be prefixed with a size.
            pass
            
        @unreal.ufunction(ret=None, params=[int, int, str])
        def on_http_response_recieved(self, inRequestIndex, inResponseCode, inMessage):
            # This is called when an http response is returned from a request.
            # the request index will match the value returned when you made the original 
            # call, so you can determine the original intent this response is for.
            pass
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    ====== UnrealEnginePython ======

    • 简陋的热更,编辑器下每次实例化都重新加载 py module
    • as_dict:uobject => py dict
    • bind_event,绑定 UE 回调 Python
    • ue_site.py
    • UE 编辑器内的 Python 编辑器,有些多余。。
    • 编辑器界面,支持 PyQt,有些多余。。

    问题合集

    • 没有 pystub
    • self.uobject 和 uobject.get_py_proxy(),有待讨论
    • 源码里大量的手写 Python C API 导出引擎 API
    • Build.cs 构建流程写的太乱了

    案例

    • 这套 worflow 不太对劲,从 C++ 的 PyCharacter 类派生出 BP 类,再绑定 Python 类
    • self.uobject 指向实际的 UObject(实际是 ue_PyUObject wrapper)
    import unreal_engine as ue
    
    class Hero:
    
        # this is called on game start
        def begin_play(self):
            ue.log('Begin Play on Hero class')
            
        # this is called at every 'tick'    
        def tick(self, delta_time):
            # get current location
            location = self.uobject.get_actor_location()
            # increase Z honouring delta_time
            location.z += 100 * delta_time
            # set new location
            self.uobject.set_actor_location(location)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    反射调用

    • 反射调用相当于:
      • self.uobject.set_property('bCanBeDamaged', True)
      • vec = self.uobject.call_function('GetActorRightForward')
    • 命名规范区分动态导出和静态导出:
      • 基于反射的函数是驼峰命名法(或第一个大写字母)的函数。
      • 相反,本机函数遵循 python 风格,使用小写、下划线作为分隔符函数名称
    • 反射获取类型模块
      • import unreal_engine.classes、unreal_engine.structs 和 unreal_engine.enums
      • 相当于 find_class()、find_struct() 和 find_object() 函数 来引用已加载的类/对象

    修改 CDO

    import unreal_engine as ue
    from unreal_engine.classes import EditorProjectAppearanceSettings
    from unreal_engine.enums import EUnit
    
    # access the editor appearance settings
    appearance_settings = ue.get_mutable_default(EditorProjectAppearanceSettings)
    
    # print properties
    ue.log(appearance_settings.properties())
    
    # assign meters as the default distance/length unit (as list of values)
    appearance_settings.DistanceUnits = [EUnit.Meters]
    
    appearance_settings.save_config()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Another example changing action mappings of the input system:

    import unreal_engine as ue
    from unreal_engine.classes import InputSettings
    from unreal_engine.structs import InputActionKeyMapping, Key
    
    inp = ue.get_mutable_default(InputSettings)
    
    km = InputActionKeyMapping()
    km.ActionName = 'Kill'
    key = Key()
    key.KeyName = 'x'
    km.Key = key
    km.bAlt = True
    
    km2 = InputActionKeyMapping()
    km2.ActionName = 'Explode'
    key = Key()
    key.KeyName = 'y'
    km2.Key = key
    km2.bAlt = False
    
    inp.ActionMappings = [km, km2]
    inp.save_config()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    API


    editor = unreal_engine.get_editor_world()
    
    • 1

    (available only into the editor) it allows to get a reference to the editor world. This will allow in the near future to generate UObjects directly in the editor (for automating tasks or scripting the editor itself)

    ====== NimForUE ======

    bad

    • 太小众
      • => IDE 生态差
      • => 栈太深,看不懂,遇到卡点问题解决不了
    • setup 卡了 2h(最终放弃),全量编译 1h,10 G 编译缓存

    看下定义 UE 反射类型
    - DSL 语法,由于 macro 机制实现的看起来优雅,语法接近 UE C++ 宏
    - 实际元编程看不懂,最终走的 UE 反射接口和其他语言方案是一样的
    - umacros.nim, macro 元编程,编译时生成代码

    uEnum EEnumGuestSomethingElse:
      (BlueprintType)
      Value1
      Value2
      Value3
      Value4
      Value5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    proc emitUEnum*(enumType: UEType, package: UPackagePtr): UFieldPtr =
      let name = enumType.name.makeFName()
      const objFlags = RF_Public | RF_Transient | RF_MarkAsNative
      let uenum = newUObject[UNimEnum](package, name, objFlags)
      for metadata in enumType.metadata:
        uenum.setMetadata(metadata.name, $metadata.value)
      let enumFields = makeTArray[TPair[FName, int64]]()
      for field in enumType.fields.pairs:
        let fieldName = field.val.name.makeFName()
        enumFields.add(makeTPair(fieldName, field.key.int64))
        # uenum.setMetadata("DisplayName", "Whatever"&field.val.name)) TODO the display name seems to be stored into a metadata prop that isnt the one we usually use
      discard uenum.setEnums(enumFields)
      uenum.setMetadata(UETypeMetadataKey, $enumType.toJson())
    
      uenum
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ====== BP ======

    • 蓝图 Asset 对应 UBlueprintGeneratedClass,UUserDefinedEnum,UUserDefinedStruct
    • FEnumEditorUtils::CreateUserDefinedEnum
    • FStructureEditorUtils::CreateUserDefinedStruct

    // 这块继承树非常深和绕

    UObject
    	UField
    		UStruct
    			UClass
    				UBlueprintGeneratedClass
    					UAnimBlueprintGeneratedClass
    					UControlRigBlueprintGeneratedClass
    					UWidgetBlueprintGeneratedClass
    
    UObject
    	UField
    		UEnum
    			UUserDefinedEnum
    
    UObject
    	UField
    		UStruct
    			UScriptStruct
    				UUserDefinedStruct
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    Py 这里搞了个单例 UPackage 作为 Outer,因为每个 BP 类型都有对应的 Asset

    TStrongObjectPtr<UPackage>& GetPythonTypeContainerSingleton()
    {
    	static TStrongObjectPtr<UPackage> PythonTypeContainer;
    	return PythonTypeContainer;
    }
    
    UObject* GetPythonTypeContainer()
    {
    	return GetPythonTypeContainerSingleton().Get();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    BP 技术栈

    https://docs.unrealengine.com/5.3/en-US/compiler-overview-for-blueprints-visual-scripting-in-unreal-engine/#finishcompilingclass

  • 相关阅读:
    2022年9月26日 9点 程序爱生活 纳指和恒指可能要创新低,然后迎来反弹,很大概率继续往下。
    函数对象类,函数对象(又称仿函数)
    红队隧道应用篇之NetCat使用(十一)
    软件项目管理 4.1.软件需求管理过程
    从原理剖析带你理解Stream
    C++中变量是按值访问的, Python 中变量的值是按引用访问的示例说明
    宠物社区系统宠物领养小程序,宠物救助小程序系统多少钱?
    谷粒商城项目-环境配置
    【计算机网络】 心跳机制
    神经网络前向传播过程,神经网络反向传播
  • 原文地址:https://blog.csdn.net/zolo_mario/article/details/134231314