| Python API | PythonAPI |
| PyStub 生成入口 | FPyWrapperTypeRegistry::GenerateStubCodeForWrappedTypes |
| Python API 生成入口 | PyOnlineDocsWriter::GenerateFiles |
| Python VM 启动入口 | FPythonScriptPlugin::InitializePython |
| Python 定义 UClass 后刷新 | FPyWrapperTypeReinstancer::ProcessPending |
| import 入口 | FPythonScriptPlugin::ImportUnrealModule |
// 也许是我不知道
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
Python 脚本扩展 DCC 编辑器已经是常规操作了
UE 官方的 Python 插件目前只用于编辑时扩展,但是文档和分析几乎没有
20tab 的 UnrealEnginePython 是一个非官方的 Python 插件,编辑器和 Runtime 都可以跑,但是不再维护了
总之,还是自己跟下源码
UnLua 的主要缺陷在于缺乏类型和强大的 IDE
TArray 没法提示UnLua 用了一年,其实日常 UI 开发功能齐全,维护脚本系统省心不少
但是当我们想要更进大范围使用脚本开发时,最主要的问题就是,UnLua 基本不能独立完成功能开发,只能在叶子节点扩展或者换掉 UFunction 来勾进游戏流程
https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python/index.html
Python 路径
项目文件夹中的 Content/Python 子文件 夹。
主虚幻引擎安装中的 Content/Python 子文件 夹。
每个启用的插件文件夹中的 Content/Python 子文件 夹。
用户目录中的Documents /UnrealEngine/Python文件夹。
例如,在 Windows 10 上,这相当于C:/Users/Username/Documents/UnrealEngine/Python
执行没什么,就是准备了 unreal 的环境注入 unreal API
TODO 看眼反射怎么export到python的
=> unreal.py
类型很关键,这个包含 Engine 和 Game 所有蓝图反射 API,类似 UnLua Intellisense
理想的 type hint,不需要手工标记类型就能各种跳转
目前实测下来:
GetComponentexampleArray : unreal.Array[str] = unreal.Array(str) # An array of stringsdef get_editor_subsystem(subsystem: Union[Class, type[_EditorSubsystemTypeVar]]) -> Optional[_EditorSubsystemTypeVar]:
emmylua 不支持
---@param Class TSubclassOf_UEditorSubsystem_
---@return UEditorSubsystem
function UEditorSubsystemBlueprintLibrary.GetEditorSubsystem(Class) end
Python 读取 UE 反射系统:
Python 定义 UClass 等(即写入 UE 反射系统):
@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()
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
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
单元测试,Automation 之类,不着急
使用 CinePrestreamingEditorSubsystem 为 Movie Render Queue 中的作业生成预加载资源
# 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
编辑器下 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)
unreal.BlueprintFunctionLibrary 就可以写一些 python util 函数暴露给蓝图了,常用技巧@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
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)
self.uobject.set_property('bCanBeDamaged', True)vec = self.uobject.call_function('GetActorRightForward')import unreal_engine.classes、unreal_engine.structs 和 unreal_engine.enumsfind_class()、find_struct() 和 find_object() 函数 来引用已加载的类/对象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()
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()
editor = unreal_engine.get_editor_world()
(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)
bad
看下定义 UE 反射类型
- DSL 语法,由于 macro 机制实现的看起来优雅,语法接近 UE C++ 宏
- 实际元编程看不懂,最终走的 UE 反射接口和其他语言方案是一样的
- umacros.nim, macro 元编程,编译时生成代码
uEnum EEnumGuestSomethingElse:
(BlueprintType)
Value1
Value2
Value3
Value4
Value5
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
// 这块继承树非常深和绕
UObject
UField
UStruct
UClass
UBlueprintGeneratedClass
UAnimBlueprintGeneratedClass
UControlRigBlueprintGeneratedClass
UWidgetBlueprintGeneratedClass
UObject
UField
UEnum
UUserDefinedEnum
UObject
UField
UStruct
UScriptStruct
UUserDefinedStruct
Py 这里搞了个单例 UPackage 作为 Outer,因为每个 BP 类型都有对应的 Asset
TStrongObjectPtr<UPackage>& GetPythonTypeContainerSingleton()
{
static TStrongObjectPtr<UPackage> PythonTypeContainer;
return PythonTypeContainer;
}
UObject* GetPythonTypeContainer()
{
return GetPythonTypeContainerSingleton().Get();
}
BP 技术栈
https://docs.unrealengine.com/5.3/en-US/compiler-overview-for-blueprints-visual-scripting-in-unreal-engine/#finishcompilingclass