1. 项目概述为什么要在TypeScript里调用Unreal C如果你正在用Unreal Engine做游戏尤其是那种逻辑复杂、需要快速迭代的项目那你肯定对蓝图和C的“相爱相杀”深有体会。蓝图可视化上手快但项目大了之后那密密麻麻的连线看着就头疼维护和版本管理更是噩梦。C性能强悍结构清晰但编译一次等半天热重载体验远不如脚本语言丝滑。这时候一个叫Puerts的框架进入了我们的视野它宣称能让TypeScriptTS成为Unreal的一等公民直接在TS里调用C的类和方法。这听起来有点“魔法”但背后的逻辑很实在。TypeScript作为JavaScript的超集兼具了动态语言的灵活和静态类型检查的可靠。想象一下你用TS写游戏逻辑享受秒级刷新的热重载同时又能无缝调用底层用C写的高性能模块比如物理计算、网络同步、渲染管线接口。这相当于把C的“力量”和TS的“敏捷”结合在了一起。Puerts就是这个桥梁它通过一套精巧的绑定机制在Unreal的虚拟机V8或QuickJS里创建了TS和C之间的双向通道。我最初接触Puerts是为了解决一个UI频繁更新的性能问题。用蓝图做的复杂UI每次数据变动都卡一下体验很差。后来我们把UI逻辑和数据绑定迁移到了TS里而底层的UI控件管理和渲染指令依然用C实现。实测下来逻辑编写的效率提升了运行时因为避免了蓝图解释执行的开销帧率也稳了。这个项目就是基于这类实战经验为你拆解从零开始在TS中调用Unreal C类的完整流程、核心原理和那些官方文档里不会写的“坑”。2. 核心原理与架构拆解Puerts如何架起这座桥要理解怎么用得先明白它怎么工作。Puerts不是简单的“胶水”它是一个运行在Unreal引擎内部的脚本系统。2.1 核心组件与工作流程Puerts的核心可以简化为三个部分TypeScript/JavaScript引擎默认是V8Google的高性能JS引擎也可以选择QuickJS更轻量。它负责解析和执行你写的TS/JS代码。Puerts核心模块C这是一套用C写的库它做了两件关键事注入与扩展它向JS引擎中注入了大量的“宿主对象”和函数。这些对象在TS看来就像是全局可用的模块或类但实际上它们的实现都在C侧。绑定与转换它提供了一套宏和模板如PUERTS_BINDING让开发者能方便地将自己的C类、函数、属性、枚举“暴露”给TS。同时它负责处理两种语言间的数据类型转换Marshal比如把TS的number转成C的float把C的FString转成TS的string。TypeScript声明文件.d.ts这是给TypeScript编译器看的“接口说明书”。它定义了所有能被TS访问的C类、方法、属性的类型。没有它TS编译器会报错“找不到名称”。Puerts在生成绑定代码时会同步生成这些.d.ts文件。工作流程大致是这样的你的TS代码被编译成JS后由Puerts加载到V8引擎中执行。当TS代码尝试调用一个“看起来像TS模块”的函数例如new UE.SomeActor()时这个调用会被Puerts拦截。Puerts根据函数名找到对应的C绑定信息将TS参数转换成C参数然后调用真正的C函数。C函数执行完毕后返回值再被Puerts转换回TS能识别的类型返回给TS代码。整个过程对TS开发者是透明的感觉就像在调用一个普通的TS库。2.2 两种绑定模式蓝图函数库与C类Puerts主要支持两种将C功能暴露给TS的方式适用于不同场景蓝图函数库Blueprint Function Library绑定这是最简单、最直接的方式。你可以创建一个继承自UBlueprintFunctionLibrary的C类将一些静态的、工具性质的函数比如数学计算、字符串处理、存档读写暴露出去。Puerts可以自动为这些加了特定宏如UFUNCTION的函数生成TS绑定。这种方式适合暴露无状态的工具函数。注意蓝图函数库的绑定是“单向”的TS可以调用这些C函数但通常不能持有这些库的实例或覆写其方法。C类绑定这是更强大、也更复杂的模式。你可以将自己写的任意UObject派生类比如AActor,UActorComponent,UUserWidget的子类暴露给TS。TS不仅可以调用这些类的方法、访问属性甚至可以继承这些C类用TS代码覆写其虚函数如Tick、BeginPlay或者创建这些类的实例。这才是实现“TS写游戏逻辑”的核心能力。本指南将重点放在第二种也是最常用的C类绑定上。理解了它蓝图函数库的绑定自然就通了。3. 环境准备与项目配置在开始写代码之前我们需要把环境搭好。这里以Unreal Engine 5.2版本和Visual Studio 2022为例。3.1 安装必要软件与插件Unreal Engine 5.2确保已安装。Puerts对UE5的支持已经很完善。Node.jsPuerts的构建脚本和部分工具链依赖Node.js。建议安装LTS版本如18.x并确保npm命令可用。Puerts插件有两种方式获取推荐通过Git Submodule适合源码开发# 在你的Unreal项目根目录下 git submodule add https://github.com/Tencent/puerts.git git submodule update --init --recursive然后将puerts/unreal目录复制到你的项目Plugins文件夹下。如果没有Plugins文件夹就创建一个。直接下载Release包从Puerts的GitHub Release页面下载对应UE版本的插件压缩包解压到项目Plugins目录下。TypeScript环境在你的项目目录下或专门为脚本设立的目录如Scripts初始化一个npm项目并安装TypeScript。mkdir Scripts cd Scripts npm init -y npm install typescript --save-dev然后创建一个tsconfig.json文件来配置TypeScript编译选项。一个基础的配置如下{ compilerOptions: { target: es2016, module: commonjs, lib: [es2016], outDir: ../Content/JavaScript, // 编译输出到Unreal能加载的目录 strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, declaration: true, // 生成.d.ts文件对智能提示很重要 sourceMap: true // 生成sourcemap方便调试 }, include: [src/**/*], exclude: [node_modules] }实操心得outDir非常关键必须指向Unreal项目Content下的一个目录如JavaScript因为Unreal的资产管理系统只能加载Content下的文件。我习惯在Content下建一个Script或JavaScript文件夹来存放编译后的.js和.js.map文件。3.2 启用插件与编译用Unreal Editor打开你的项目。点击菜单栏的编辑(Edit)-插件(Plugins)在搜索框输入“Puerts”确保Puerts - Unreal.js Binding插件已被勾选启用。可能需要重启编辑器。关闭编辑器回到你的项目根目录右键点击.uproject文件选择Generate Visual Studio project files。用Visual Studio打开生成的.sln解决方案文件编译你的项目通常选择Development Editor配置。编译成功后再次用Unreal Editor打开项目。如果配置正确你可能会在输出日志Output Log里看到Puerts相关的加载信息。4. 实战将C类暴露给TypeScript假设我们有一个管理游戏时间的C类ATimeManager现在我们要把它暴露给TS让TS能获取游戏时间、设置时间缩放。4.1 编写待绑定的C类首先在C端定义这个类。注意要暴露给TS的类必须是UCLASS并且方法需要用UFUNCTION标记属性用UPROPERTY标记。// TimeManager.h #pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include TimeManager.generated.h UCLASS() class MYPROJECT_API ATimeManager : public AActor { GENERATED_BODY() public: ATimeManager(); // 暴露一个属性当前游戏时间秒 UPROPERTY(BlueprintReadOnly, Category Time) float GameTimeSeconds; // 暴露一个属性时间缩放因子 UPROPERTY(BlueprintReadWrite, Category Time) float TimeDilation; // 暴露一个方法获取格式化后的时间字符串 (HH:MM:SS) UFUNCTION(BlueprintCallable, Category Time) FString GetFormattedGameTime() const; // 暴露一个静态工具方法将秒数转换为时分秒格式 UFUNCTION(BlueprintCallable, Category Time, meta (WorldContext WorldContextObject)) static FString SecondsToHMS(float TotalSeconds, UObject* WorldContextObject); // 重写Tick用于更新游戏时间 virtual void Tick(float DeltaTime) override; protected: virtual void BeginPlay() override; };// TimeManager.cpp #include TimeManager.h #include string ATimeManager::ATimeManager() { PrimaryActorTick.bCanEverTick true; GameTimeSeconds 0.0f; TimeDilation 1.0f; } FString ATimeManager::GetFormattedGameTime() const { int32 TotalSeconds FMath::FloorToInt(GameTimeSeconds); int32 Hours TotalSeconds / 3600; int32 Minutes (TotalSeconds % 3600) / 60; int32 Seconds TotalSeconds % 60; return FString::Printf(TEXT(%02d:%02d:%02d), Hours, Minutes, Seconds); } // 静态方法实现 FString ATimeManager::SecondsToHMS(float TotalSeconds, UObject* WorldContextObject) { // 这里WorldContextObject参数由Puerts/蓝图系统自动传入我们可能用不到但签名要保持一致。 int32 TotalSecs FMath::FloorToInt(TotalSeconds); int32 Hours TotalSecs / 3600; int32 Minutes (TotalSecs % 3600) / 60; int32 Seconds TotalSecs % 60; return FString::Printf(TEXT(%02d:%02d:%02d), Hours, Minutes, Seconds); } void ATimeManager::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 根据时间缩放因子更新游戏时间 GameTimeSeconds DeltaTime * TimeDilation; } void ATimeManager::BeginPlay() { Super::BeginPlay(); // 初始化代码... }4.2 使用Puerts的宏进行绑定这是最关键的一步。我们需要在一个单独的C文件中使用Puerts提供的宏来声明这个类的TS绑定。通常我会在项目的Source/MyProject/目录下创建一个名为PuertsBinding的文件夹来存放所有绑定代码。创建一个新的头文件例如TimeManagerBinding.h// TimeManagerBinding.h #pragma once #include TimeManager.h // 包含我们自己的头文件 #include Puerts.h // 包含Puerts核心头文件 // 使用Puerts的命名空间 using namespace puerts; // 声明绑定类。这个类不会实例化只用于组织绑定代码。 class FTimeManagerBinding { public: // 静态方法用于注册绑定 static void Register() { // 定义一个JS模块名为“Game.TimeManager” DefineClassATimeManager() .Method(GetFormattedGameTime, MakeFunction(ATimeManager::GetFormattedGameTime)) .Property(GameTimeSeconds, MakeProperty(ATimeManager::GameTimeSeconds)) .Property(TimeDilation, MakeProperty(ATimeManager::TimeDilation)) .Register(); // 绑定静态方法。注意静态方法通常绑定到类的构造函数上作为静态成员 // 但Puerts也支持将静态函数注册为独立的模块函数。 // 这里我们演示将其注册为全局工具函数。 DefineFunction(SecondsToHMS, MakeFunction(ATimeManager::SecondsToHMS)); } };但是上面的DefineClass和DefineFunction是简化的伪代码。Puerts的实际绑定语法更丰富。更常见的做法是使用一个统一的绑定注册入口。Puerts推荐使用一个名为PuertsModule的宏来声明模块并在项目启动时调用注册函数。一个更贴近实际项目的绑定文件可能长这样假设项目名为MyGame// MyGameBinding.cpp #include JsEnv.h #include TimeManager.h // 使用Puerts的自动绑定宏 // 第一个参数是C类名第二个参数是其在TS中对应的名字可选默认与C类名相同 PUERTS_BINDING(ATimeManager, ATimeManager) { // 绑定构造函数如果需要TS能new出来 Constructor(); // 绑定方法 Method(GetFormattedGameTime, SelectFunction(FString()const, ATimeManager::GetFormattedGameTime)); // 绑定属性。ReadWrite表示可读可写ReadOnly表示只读。 Property(GameTimeSeconds, MakeProperty(ATimeManager::GameTimeSeconds)); Property(TimeDilation, MakeProperty(ATimeManager::TimeDilation)); // 绑定静态方法。需要指定为StaticMethod。 StaticMethod(SecondsToHMS, SelectFunction(FString(float, UObject*), ATimeManager::SecondsToHMS)); }重要提示Puerts的绑定宏语法可能会随着版本更新而变化。上述PUERTS_BINDING宏是较新版本的写法。最可靠的方法是查阅你所用Puerts版本unreal目录下的示例工程和文档。通常在Plugins/Puerts/unreal/demo目录下有非常详尽的绑定示例。4.3 生成TypeScript声明文件.d.ts绑定代码写完后我们需要生成对应的TypeScript声明文件这样在TS里写代码才有智能提示。编译你的C项目确保绑定代码编译通过。运行Puerts提供的生成脚本。这个脚本通常位于Plugins/Puerts/unreal目录下可能是一个.bat(Windows) 或.sh(Mac/Linux) 文件比如generate_dts.bat。打开命令行切换到你的Unreal项目根目录执行命令。例如.\Plugins\Puerts\unreal\generate_dts.bat或者如果Puerts插件配置正确在Unreal Editor编译完成后它可能会自动触发声明文件的生成。脚本执行成功后会在你配置的输出目录通常是Plugins/Puerts/unreal/dts或你项目目录下的TypeScript文件夹里找到生成的.d.ts文件例如ue.d.ts或MyGame.d.ts。这个文件包含了所有已绑定的C类和函数的TypeScript类型定义。你需要将这个.d.ts文件引用到你的TS项目中。修改你的tsconfig.json{ compilerOptions: { // ... 其他配置不变 typeRoots: [./node_modules/types, ../Plugins/Puerts/unreal/dts], // 添加d.ts文件所在目录 }, // ... }或者更简单直接的方式在你的TS入口文件如src/index.ts最开头添加一行/// reference path../../Plugins/Puerts/unreal/dts/ue.d.ts / // 注意路径需要根据你的项目实际结构调整5. 在TypeScript中调用绑定的C类环境配好了绑定生成了现在终于可以在TS里大展拳脚了。5.1 基础调用访问属性和方法假设我们已经生成了声明文件并且ATimeManager已经绑定到了名为UE的全局命名空间下这是Puerts的默认行为。在你的TS文件例如src/game/TimeLogic.ts中// 引入UE全局命名空间。这步不是必须的但能获得更好的类型提示。 // 如果你的.d.ts文件正确配置直接使用 UE 即可。 class TimeLogic { private timeManager: UE.Actor; // 注意ATimeManager在TS中可能被映射为 UE.Actor 的子类具体看.d.ts文件定义 constructor() { // 1. 查找场景中已有的TimeManager实例假设已经放置了一个 // UE.GameplayStatics 是一个常用的工具类也需要提前绑定。 const actors UE.GameplayStatics.GetAllActorsOfClass(UE.GameplayStatics.GetWorld(), UE.ATimeManager.StaticClass()); if (actors actors.length 0) { this.timeManager actors[0] as UE.ATimeManager; // 类型断言 console.log(找到TimeManager: ${this.timeManager.GetName()}); } else { console.warn(场景中未找到ATimeManager实例。); // 2. 或者我们可以在TS中动态生成一个如果C类允许 // 这取决于绑定配置。如果ATimeManager的构造函数被暴露了理论上可以 // this.timeManager new UE.ATimeManager(); // 但通常Actor的生成需要UWorld上下文更常见的做法是在C端生成并注册到某个全局管理器。 } } public update(): void { if (this.timeManager) { // 访问C属性 const currentTime this.timeManager.GameTimeSeconds; console.log(当前游戏时间: ${currentTime.toFixed(2)}秒); // 调用C实例方法 const formattedTime this.timeManager.GetFormattedGameTime(); console.log(格式化时间: ${formattedTime}); // 修改C属性 // 假设我们想让时间变慢一半 this.timeManager.TimeDilation 0.5; console.log(时间缩放已设置为: ${this.timeManager.TimeDilation}); // 调用C静态方法工具函数 const hmsString UE.ATimeManager.SecondsToHMS(3661.0, null); // 1小时1分1秒 console.log(3661秒等于: ${hmsString}); } } } // 导出这个类以便在其他TS模块中使用 export default TimeLogic;5.2 处理复杂数据类型与回调C和TS之间的数据交换不止是数字和字符串。常见的还有数组、结构体、对象引用以及回调函数。数组TArrayPuerts会自动将C的TArrayT转换为TS的ArrayT。反过来传入TS数组到期望TArray的C函数时也会自动转换。// 假设C有一个方法UFUNCTION(BlueprintCallable) TArrayFString GetPlayerNames(); const names: string[] someActor.GetPlayerNames(); names.forEach(name console.log(name));结构体FStruct结构体会被转换为TS的普通对象Plain Object。你需要确保在TS中构造的对象属性与C结构体成员匹配。// C 结构体 USTRUCT(BlueprintType) struct FMyData { GENERATED_BODY() UPROPERTY() int32 Id; UPROPERTY() FString Name; }; // 对应的C方法UFUNCTION() void SetData(const FMyData Data);// TS 中调用 const myData { Id: 123, Name: TestObject }; someActor.SetData(myData); // Puerts会自动将JS对象转换为FMyData对象引用UObject*TS中持有的是C对象的“代理”或“引用”。你可以直接传递它们。const anotherActor: UE.AActor ...; someActor.SetTargetActor(anotherActor); // 假设SetTargetActor接收一个AActor*参数回调函数Delegate / 函数指针这是非常强大的特性。你可以将一个TS函数传递给C让C在特定时刻回调它。// C 声明一个动态多播委托 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChanged, int32, NewScore); UCLASS() class AMyActor : public AActor { ... UPROPERTY(BlueprintAssignable) FOnScoreChanged OnScoreChanged; };Puerts可以将这个委托暴露给TS。在TS中你可以给这个委托添加回调const myActor: UE.AMyActor ...; const callback (newScore: number) { console.log(分数变更为: ${newScore}); }; // 添加TS回调到C委托 myActor.OnScoreChanged.Add(callback); // 未来当C端触发 Broadcast(newScore) 时这个TS函数就会被调用。 // 注意需要确保TS函数的作用域避免被垃圾回收导致崩溃。通常在Actor销毁时需要移除回调。 myActor.OnScoreChanged.Remove(callback);5.3 继承C类并覆写虚函数这是Puerts最精髓的功能之一用TS继承一个C的UObject类并覆写其虚函数如ReceiveBeginPlay,TickComponent。假设我们有一个C的UMyComponent组件我们想在TS里扩展它C端确保你的类可以被继承并且虚函数标记为virtual和override。Puerts绑定会处理这些虚函数的TS重载。UCLASS() class UMyComponent : public UActorComponent { GENERATED_BODY() public: virtual void BeginPlay() override { Super::BeginPlay(); // C端的默认逻辑 UE_LOG(LogTemp, Warning, TEXT(C BeginPlay called)); } virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // C端的默认逻辑 } // 定义一个可以被TS覆写的虚函数 UFUNCTION(BlueprintNativeEvent, Category MyComponent) void OnCustomEvent(int32 EventId); virtual void OnCustomEvent_Implementation(int32 EventId) { // 默认实现 } };在绑定代码中需要特别声明哪些虚函数允许TS覆写。Puerts的绑定宏通常有对应的标识。TS端通过Puerts提供的特殊API来扩展C类。注意你不能直接用class MyTSComponent extends UE.UMyComponent这样的ES6语法因为UE类系统是UObject体系。Puerts提供了extends函数或装饰器。import * as UE from ue; // 假设通过模块化方式引入 // 使用Puerts的类扩展系统 const MyTSComponent UE.Class.Extends(UE.UMyComponent, { // 构造函数 $ctor: function() { // 调用父类构造函数如果需要 this.$super(); console.log(TS Component Constructed); }, // 覆写BeginPlay ReceiveBeginPlay: function() { // 调用父类实现可选 this.$super.ReceiveBeginPlay(); console.log(TS BeginPlay called!); // 你的TS逻辑 this.SomeTSMethod(); }, // 覆写TickComponent (在UE中组件Tick的函数名可能是 TickComponent) TickComponent: function(DeltaTime: number) { this.$super.TickComponent(DeltaTime); // 你的TS每帧逻辑 // console.log(TS Tick: ${DeltaTime}); }, // 覆写BlueprintNativeEvent OnCustomEvent: function(EventId: number) { console.log(TS OnCustomEvent called with ID: ${EventId}); // 你可以选择是否调用父类实现 // this.$super.OnCustomEvent(EventId); }, // 定义全新的TS方法 SomeTSMethod: function() { console.log(This is a pure TS method.); // 可以访问this上的属性包括从C继承来的 if (this.Owner) { console.log(My owner is: ${this.Owner.GetName()}); } }, // 定义TS属性 tsProperty: 100 }); // 将这个TS类注册到Puerts系统中使其可以被C世界识别和创建 UE.registerClass(MyTSComponent, MyTSComponent, true); // 现在你可以在TS中像使用普通类一样使用它或者通过某种方式让C端创建这个TS子类的实例。 // 例如通过一个全局的工厂方法 export function CreateMyTSComponent(): any { return new MyTSComponent(); }踩坑实录TS覆写C虚函数时函数名必须完全匹配C端的函数名包括大小写。最稳妥的方法是查看生成的.d.ts文件里该函数的准确名称。例如C的BeginPlay在TS端可能是ReceiveBeginPlay。TickComponent的参数列表也要完全一致。6. 调试与性能优化在TS里写逻辑调试和性能是关键。6.1 调试技巧使用console.log这是最直接的调试方式Puerts会将输出重定向到Unreal的Output Log窗口Window-Developer Tools-Output Log。记得在打包发布时移除或禁用不必要的log。Source Map支持如果你在tsconfig.json中配置了sourceMap: true并且在Puerts初始化时启用了Source Map那么在Output Log中打印的错误堆栈就会显示原始的TS文件行号而不是编译后的JS行号这能极大提升调试效率。Chrome DevTools远程调试V8引擎这是最强大的调试手段。Puerts的V8后端支持通过Chrome DevTools进行远程调试。在启动Unreal Editor或打包游戏时需要添加命令行参数-puerts-debug。游戏启动后在浏览器中打开chrome://inspect。你应该能看到一个名为 “Puerts” 或类似的目标点击inspect即可打开一个完整的DevTools窗口可以设置断点、查看变量、调用栈等和在浏览器中调试网页一模一样。实操心得远程调试对查找复杂的逻辑Bug非常有用但会轻微影响性能。建议在开发阶段使用发布时关闭。6.2 性能优化要点避免每帧进行昂贵的TS-C互操作TS调用C函数或访问C属性是有成本的涉及参数转换和上下文切换。不要在Tick函数里频繁调用大量简单的C getter/setter。如果可能将一帧内需要的数据在TS端缓存起来或者将逻辑密集的操作移到C端。注意垃圾回收GCTS对象由V8管理GC。如果你在TS中创建了大量临时对象如数组、对象字面量可能会触发频繁的GC导致卡顿。对于高频调用的函数要注意优化对象创建。谨慎使用闭包和回调将TS函数作为回调传递给C委托时Puerts需要维持对该JS函数的引用以防止被GC。如果注册后忘记移除会导致内存泄漏。务必在Actor或Component销毁时如Destroy或UninitializeComponent中移除所有回调。数据类型转换开销复杂结构体、大数组在TS和C间传递会有序列化/反序列化开销。对于需要高频访问的数据考虑在C端暴露一个获取数据指针或引用的接口需谨慎处理生命周期或者使用共享内存等高级技术Puerts可能提供相关支持。使用TypeScript的优势利用TS的静态类型检查在编译阶段就发现很多潜在错误避免运行时因类型不匹配导致的额外开销或崩溃。合理设计TS模块的接口减少模块间的耦合。7. 常见问题与解决方案实录在实际项目中你肯定会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决办法。问题现象可能原因解决方案TS编译成功但在Unreal中运行时报“UE is not defined”或“Cannot read property ‘XXX’ of undefined”1..d.ts声明文件未正确加载或路径不对。2. Puerts插件未启用或JS环境未初始化。3. TS代码在Puerts环境初始化前就执行了。1. 检查tsconfig.json的typeRoots或/// reference路径是否正确指向生成的ue.d.ts。2. 在Unreal Editor的输出日志中搜索“Puerts”确认插件加载和JsEnv初始化日志。3. 确保你的TS入口文件是在游戏启动或关卡加载后由C端主动调用ExecuteJsFile或类似方法加载的。能调用一些C函数但调用自己绑定的类时报错“function not found”1. 绑定代码.cpp未编译进项目或宏使用错误。2. 绑定生成后未重新生成.d.ts文件。3. C类或方法没有被UCLASS/UFUNCTION标记或标记的Category不符合Puerts的过滤规则。1. 检查包含绑定代码的.cpp文件是否在项目的Build.cs文件中被正确引用。清理解决方案并重新编译。2. 运行generate_dts脚本并确认生成的.d.ts文件中包含你的类和方法签名。3. 确保要暴露的类和方法都有正确的UE反射宏。检查Puerts的绑定宏是否包裹了这些方法。TS覆写C虚函数但C的父类实现没有被调用在TS的覆写函数中没有调用this.$super.XXX()。如果希望保留C父类的默认行为必须在TS函数中显式调用this.$super.XXX(...arguments)。打包后TS代码不生效或找不到TS编译输出的.js文件没有被包含在打包的资产中。确保TS输出目录如Content/JavaScript内的文件被Unreal Cook打包进程包含。通常需要在项目的DefaultGame.ini或DefaultEngine.ini中配置[Cook]段的DirectoriesToAlwaysCook规则或者将.js文件后缀添加到AdditionalAssetExtensions。最保险的方法是将.js文件作为资产导入到Content Browser中。回调函数被执行了一次后再也收不到通知TS函数被GC回收了。当TS函数作为回调传给C后如果TS侧没有保持对该函数的引用V8可能认为它不再被使用而回收。在TS侧将回调函数保存在一个类成员变量或模块级变量中确保其生命周期长于注册它的对象。在对象销毁时主动从C委托中移除回调。传递复杂对象到CC端收到乱码或崩溃TS对象的结构与C结构体USTRUCT定义不匹配或者存在循环引用。严格对照C结构体的UPROPERTY定义来构造TS对象。确保属性名和类型完全一致。避免在需要序列化的对象中包含函数或不可序列化的数据。对于嵌套对象确保其类型也是被正确定义和绑定的USTRUCT。最后再分享一个小技巧项目初期建议建立一个简单的“测试关卡”里面放一个专门用于执行TS命令的Actor。这个Actor在BeginPlay时加载你的主TS入口文件并暴露一些控制台命令如通过键盘按键触发不同的TS函数。这样你可以在编辑器中实时测试TS逻辑而无需重启游戏或重新打包能极大提升迭代速度。当TS逻辑稳定后再将其集成到正式的游戏流程中。