
Puerts 蓝图 Mixin 机制深度指南用 TypeScript 增强与覆盖 UE 蓝图类【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts在 Puerts 的 Unreal 集成方案中blueprint.mixin提供了一种将 TypeScript 类下称 TS 类的能力注入到既有蓝图类的能力把 TS 类的方法、字段与事件逻辑混合进蓝图类从而在不改动蓝图资产的情况下用脚本扩展甚至覆盖蓝图实现。本文围绕 doc/unreal/zhcn/mixin.md 展开结合仓库中的脚本与 C 实现系统讲解 mixin 的核心机制、基本用法、进阶配置生命周期、继承重定向、super调用、原生类混入等与注意事项读完即可在真实项目中使用该能力完成蓝图类的脚本化改造。什么是蓝图 Mixin把一个 TS 类假设是类 Amixin 到一个蓝图类类 B的能力核心行为如下如果 A 和 B 都有同样的函数A 的逻辑会替换 B 的支持 UE 的事件比如ReceiveBeginPlay可新增方法或字段。在底层这个能力由 JS 侧入口blueprint.mixin与 C 侧的UJSGeneratedClass::Mixin共同完成。JS 侧实现在 unreal/Puerts/Content/JavaScript/puerts/uelazyload.js#L274-L303它遍历 mixin 类原型上的函数属性收集成一个mixinMethods表然后调用原生函数__tgjsMixin(to.StaticClass(), mixinMethods, ...)完成 UE 侧的 UFunction 注册与替换C 侧在 unreal/Puerts/Source/JsEnv/Private/JSGeneratedClass.cpp#L179-L260 中为每个被 mixin 的函数复制生成一个UJSGeneratedFunction以__puerts_mixin__结尾命名并把Super的NativeFunc重定向为execCallMixin最终调用到 TS 实现。特点安全如果 TS 类和蓝图类有同名函数将会检查两者的兼容性符合 TS 的协变逆变规则不兼容的签名会报错避免运行时才暴露问题高效TS 类可以调用蓝图类的方法且有代码提示强大TS 可新增方法但蓝图不可见TS 能新增字段但蓝图不可见支持网络相关方法RPC的 mixin支持事件 mixin 并能被回调对象生命周期支持脚本持有和引擎持有两种模式支持原生类的BlueprintNativeEvent、BlueprintImplementableEvent方法的 mixin。注意事项如果要覆盖 UE 的事件要注意被 mixin 的类中有对应的事件逻辑可以为空否则在子类调用时可能会有可能调用不到 TS 的逻辑。这是 mixin 事件覆盖的已知边界参见仓库历史 issue #1762 的讨论结论。基本用法完整可运行例子可参考仓库配套演示工程中的 TypeScript/UsingMixin.ts注该示例位于外部示例工程将 Start 脚本改为 UsingMixin 即可运行。下面按步骤拆解。第一步加载被 mixin 的蓝图类使用UE.Class.Load加载蓝图类路径注意路径以_C结尾表示生成的类再通过blueprint.tojs把 UE 类对象转换为 TS 可用的类构造器let ucls UE.Class.Load(/Game/StarterContent/MixinTest.MixinTest_C); const MixinTest blueprint.tojstypeof UE.Game.StarterContent.MixinTest.MixinTest_C(ucls);blueprint.tojs的实现在 unreal/Puerts/Content/JavaScript/puerts/uelazyload.js#L269其类型签名见 unreal/Puerts/Typing/puerts/index.d.ts#L53为tojsT extends typeof Object(cls: Class): T。第二步编写 TS 扩展类TS 扩展类的声明要点先声明同名interface继承目标蓝图类的 TS 类型再在class中编写要覆盖或新增的方法。interface Loggable extends UE.Game.StarterContent.MixinTest.MixinTest_C {}; class Loggable { // 可以覆盖蓝图对应的函数函数签名和 MixinTest_C 声明的不兼容不需要严格一致能满足协变逆变要求即可会报错 Log(msg: string): void { console.log(this.GetName(), msg); console.log(1 3 ${this.TsAdd(1, 3)}); } // 蓝图没有的纯 TS 方法 TsAdd(x: number, y: number): number { console.log(Ts Add(${x}, ${y})) return x y; } }注意这里的interface Loggable extends ...与class Loggable同名合并是 TypeScript 的声明合并技巧——interface 负责让类实例拥有蓝图类全部成员的类型包括GetName等class 则提供具体实现。Log覆盖蓝图同名函数TsAdd是蓝图没有的新增方法。第三步执行 mixin调用blueprint.mixin传入被 mixin 的蓝图类TS 构造器与 TS 扩展类const MixinTestWithMixin blueprint.mixin(MixinTest, Loggable);第四步使用新类MixinTestWithMixin即为新类可直接用于生成 Actorworld.SpawnActor(MixinTestWithMixin.StaticClass(), undefined, UE.ESpawnActorCollisionHandlingMethod.Undefined, undefined, undefined) as Loggable;blueprint.mixin的完整类型签名unreal/Puerts/Typing/puerts/index.d.ts#L54-L57function mixinT extends typeof Object, R extends InstanceTypeT(to: T, mixinMethods: new (...args: any) R, config?: MixinConfig): { new (Outer?: Object, Name?: string, ObjectFlags?: number): R; StaticClass(): Class; };进阶用法前置知识stub 对象与生命周期一个 UE 对象传入到 TSTS 侧会建立一个 stubTS对象与之相对应TS 调用这个 stub 对象会被转发到真实的 UE 原生调用。在 Puerts 中它们的生命周期关系有两种stub 对象由 JS GC 管理stub 对象持有 UE 对象的强引用下称stub 对象持有 UE 对象如果 stub 对象在 TS 无引用将会被 GC进而释放对 UE 对象的强引用如果进一步在 UE 引擎也没有该 UE 对象该 UE 对象会被 GC。UE 对象由 UE GC 管理UE 对象持有 stub 对象的强引用下称UE 对象持有 stub 对象如果 UE 对象在 UE 引擎无引用该 UE 对象会被 GC进而释放对 stub 对象的强引用如果进一步在 TS 也没有引用该 stub 对象该 stub 对象会被 GC。这两种模式分别对应下方MixinConfig.objectTakeByNative的false与true。blueprint.mixin 的参数 3MixinConfig该参数声明typing 中实际还包含noMixinedWarning见 unreal/Puerts/Typing/puerts/index.d.ts#L52type MixinConfig { objectTakeByNative?: boolean, inherit?: boolean, generatedClass?: Class, noMixinedWarning?: boolean };objectTakeByNative默认为false表示stub 对象持有 UE 对象为true表示UE 对象持有 stub 对象。inherit与generatedClass配合使用。默认为false表示重定向的是原蓝图类如果为true将会先动态生成一个继承类然后重定向生成的类该生成类会通过generatedClass字段返回。noMixinedWarning当目标函数已被另一个 VM另一个 JS 环境mixin 过时控制是否打印警告。在 JS 侧它被直接传给原生__tgjsMixin见 unreal/Puerts/Content/JavaScript/puerts/uelazyload.js#L285对应 C 实现中若Warning为真且函数已被 mixin会输出日志 Try to mixin a function[%s:%s] already mixin by anthor vmunreal/Puerts/Source/JsEnv/Private/JSGeneratedClass.cpp#L202-L210。在 JS 侧实现中unreal/Puerts/Content/JavaScript/puerts/uelazyload.js#L274-L303mixin返回前还会把mixinMethods中未在返回类原型上定义的方法补挂到原型上从而保证新增的纯 TS 方法也能被调用当config.inherit为真时config.generatedClass会被赋值为原生生成的类。super 关键字的说明假设有个蓝图类MixinSuperTestDerived继承了蓝图类MixinSuperTestBase这两个类都有Foo方法我们要通过 mixin 覆盖MixinSuperTestDerived上的Foo在 TS 逻辑中需要调用基类蓝图类的Foo要怎么处理直接在前面介绍的不extends任何类的 mixin 类中调用super会报错class DerivedClassMixin { Foo(): void { console.log(i am ts mixin); super.Foo(); } }上述代码会报语法错误。这时可以通过添加一个中转类来解决问题先用interface/class同名合并声明一个指向蓝图基类的占位类再通过Object.setPrototypeOf把它的原型链接到蓝图基类 TS 类的原型上最后让 mixin 类extends这个占位类即可合法使用super.Foo()interface MixinSuperTestBasePlaceHold extends UE.Game.StarterContent.MixinSuperTestBase.MixinSuperTestBase_C {}; class MixinSuperTestBasePlaceHold {} Object.setPrototypeOf(MixinSuperTestBasePlaceHold.prototype, MixinSuperTestBase.prototype); class DerivedClassMixin extends MixinSuperTestBasePlaceHold { Foo(): void { console.log(i am ts mixin); super.Foo(); } }其原理是extends之后TS 会生成对MixinSuperTestBasePlaceHold.prototype上Foo的super调用而该占位类的原型链已经被接驳到蓝图基类的 TS 原型上于是super.Foo()会被转发到蓝图基类的真实实现。新增字段新增字段其实是存放在 stub 对象里因而objectTakeByNative为false时需要保持对 stub 对象的引用否则 stub 对象释放后UE 对象回传将会建立一个新对象原来的数据就丢失了objectTakeByNative为true不需要保持 stub 对象引用但注意不要期望通过持有 stub 对象进而引用 UE 对象该 UE 对象应保证被引擎持有。这与前置知识的两种生命周期一一对应字段数据存在于 stubstub 存活时间决定了字段数据的存续时间。原生类的 mixin只支持BlueprintNativeEvent、BlueprintImplementableEvent方法。比如如下 C 声明的函数class UMainObject : public UObject { GENERATED_BODY() public: UFUNCTION(BlueprintNativeEvent) int32 Mult(int32 a, int32 b) const; UFUNCTION(BlueprintImplementableEvent) int32 Div(int32 a, int32 b) const; int32 Mult_Implementation(int32 a, int32 b) const { UE_LOG(LogTemp, Warning, TEXT(wrong implementation div %d %d), a, b); return a b; } };TypeScript 这样 mixinlet obj new UE.MainObject(); console.log(before mixin start....) obj.Mult(1, 2); obj.Div(4, 5); console.log(before mixin end....) class Calc { // 声明为 BlueprintNativeEvent 的原生方法 Mult(x: number, y: number): number { console.log(Ts Mult(${x}, ${y})) return x * y; } // 声明为 BlueprintImplementableEvent 的方法 Div(x: number, y: number): number { console.log(Ts Div(${x}, ${y})) return x / y; } } interface Calc extends UE.MainObject {}; blueprint.mixin(UE.MainObject, Calc); console.log(after mixin start....) obj.Mult(1, 2); obj.Div(4, 5); console.log(after mixin end....)输出before mixin start.... wrong implementation div 1 2 before mixin end.... after mixin start.... Ts Mult(1, 2) Ts Div(4, 5) after mixin end....可以看到即使是已经new出来的对象mixin 后调用也会调用到新的 TS 方法——这是因为 mixin 重定向的是类上的 UFunction 的NativeFunc对既有实例同样生效。从源码看mixin 对原生类与蓝图类的处理路径一致UJSGeneratedClass::Mixin会把原函数复制为UJSGeneratedFunction将Super原函数的FunctionFlags加上FUNC_Native并重定向其NativeFunc为execCallMixinunreal/Puerts/Source/JsEnv/Private/JSGeneratedClass.cpp#L240-L257从而让 UE 不走解析直接进入 TS 调用链调用时由UJSGeneratedFunction::execCallMixin通过InvokeMixinMethod把参数转发给 TS 实现unreal/Puerts/Source/JsEnv/Private/JSGeneratedFunction.cpp#L29-L50。C BlueprintNativeEvent 函数 bug 修复如果你的 C 函数声明为BlueprintNativeEvent的话如果有 bug可以用该功能替换成正确逻辑。即不需要改动 C 源码与重新编译直接在 TS 侧用 mixin 覆盖BlueprintNativeEvent方法连同其_Implementation逻辑一起被替换即可修复线上/编辑器中的函数逻辑。撤销 Mixinunmixin与mixin配套blueprint命名空间还提供了unmixin用于还原unreal/Puerts/Content/JavaScript/puerts/uelazyload.js#L307-L311function unmixin(to: typeof Object): void;其实现为调用__tgjsMixin(to.StaticClass(), {}, undefined, undefined, undefined, true)——传入空的方法表并置 unmixin 标志。C 侧对应的还原逻辑在UJSGeneratedClass::Restoreunreal/Puerts/Source/JsEnv/Private/JSGeneratedClass.cpp#L262-L356它会遍历类上所有UJSGeneratedFunction恢复其Original指向的原函数的NativeFunc与FunctionFlags、还原被重命名前缀__puerts_old__的旧函数并清理函数映射缓存从而实现 mixin 效果的回滚。小结blueprint.mixin让 TS 可以在运行时覆盖蓝图类乃至原生类的事件函数的实现、新增方法与字段同时通过 TS 的协变逆变签名检查保证覆盖的安全性通过objectTakeByNative灵活控制 stub 对象与 UE 对象的持有关系。使用时牢记三点覆盖 UE 事件前确认类中存在对应事件定义新增字段依赖 stub 对象存活需按生命周期模式正确保持引用跨 VM 重复 mixin 同一函数会产生警告可用noMixinedWarning控制。相关源码入口JS 侧 mixin 实现、C 侧 UJSGeneratedClass::Mixin、类型声明文档原文见 doc/unreal/zhcn/mixin.md。【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考