1. 项目概述为什么要在Unreal里用TypeScript如果你是一个Unreal引擎的开发者或者是一个前端出身的程序员现在想进入游戏开发领域那么“Puerts”这个名字你大概率已经听过或者很快就会接触到。简单来说Puerts是一个桥梁它让你能在Unreal引擎里用TypeScript或者JavaScript来写游戏逻辑。这听起来可能有点“跨界”但它的吸引力是实实在在的。想象一下你不再需要为了一些UI逻辑、数据配置或者玩法原型去反复编译C忍受动辄几分钟甚至十几分钟的等待。你可以像写网页应用一样在编辑器里热重载代码改完逻辑切回游戏窗口效果立竿见影。这个项目的核心价值就是“效率”和“灵活性”。对于大型项目用C处理核心底层和性能关键模块用TypeScript处理上层业务逻辑和快速迭代的内容是一种非常理想的架构。前端开发者可以无缝切入游戏逻辑开发享受TypeScript强大的类型系统和现代语法而C工程师则可以更专注于引擎扩展和性能优化把频繁变动的业务逻辑交给脚本层。我最初接触Puerts就是因为团队里既有深耕C的图形大佬也有擅长前端交互的同事我们需要一个能让大家高效协作、同时发挥各自所长的方案。经过几个项目的实战Puerts确实成为了我们技术栈中不可或缺的一环。2. 核心架构与原理拆解2.1 Puerts 如何架起 C 与 TypeScript 的桥梁要理解Puerts首先得明白它底层依赖的是什么。Puerts的核心是V8引擎也就是Chrome浏览器和Node.js里那个高性能的JavaScript引擎。Puerts做的事情就是把V8引擎集成到Unreal引擎的进程里然后在C和JavaScript/TypeScript之间建立一套完整的交互机制。这套机制主要包含两个层面一是“绑定”二是“调用”。绑定指的是将C的类、函数、属性、枚举等暴露给TypeScript环境让TS代码能够像调用本地对象一样使用它们。Puerts提供了多种绑定方式从最自动化的基于反射信息的绑定到手动编写声明文件灵活性很高。调用则是双向的既可以从TypeScript调用C函数也可以从C侧主动执行TypeScript函数或回调。这一切的基础是Puerts在底层维护的一个“上下文”Context环境所有的TS代码都在这个上下文中运行并且通过一系列精心设计的接口与C世界进行数据交换。一个常见的误解是用了脚本语言性能就一定差。实际上对于游戏逻辑中大量的条件判断、状态管理、事件分发等非计算密集型任务V8引擎的性能完全足够甚至因为避免了C的编译链接开销在迭代速度上具有碾压性优势。真正的性能瓶颈通常出现在需要每帧进行大量数学运算如矩阵变换、物理检测或者高频跨语言调用的地方。好的实践是遵循“各司其职”的原则让C负责提供高性能的基础设施如数学库、渲染指令、物理引擎接口然后封装成简洁的API给TypeScript调用而TypeScript则负责组织复杂的业务逻辑流。2.2 TypeScript 在游戏开发中的独特优势为什么是TypeScript而不是Lua、Python或者其他脚本语言这背后有很实际的考量。首先TypeScript是JavaScript的超集拥有极其庞大的开发者生态和工具链。你的团队可能不需要额外学习一门新的语言。其次也是最重要的类型系统。游戏项目的代码量随着开发进程会急剧膨胀没有类型检查的大型JavaScript项目简直就是“泥潭”一个属性名拼写错误可能要到运行时才会崩溃调试成本极高。TypeScript的静态类型检查可以在编码阶段就发现大部分低级错误配合VSCode等编辑器的智能提示开发体验非常流畅。你可以为从C暴露过来的API编写.d.ts声明文件这样在TS里调用时参数类型、返回值类型一目了然甚至能有代码自动补全。这对于团队协作和代码维护来说是质的提升。另外TypeScript的现代语法特性如异步/异步函数async/await、装饰器Decorator可用于实现简单的ECS架构或依赖注入、模块化等都能很好地应用于游戏逻辑的组织。从网络热词可以看到社区对TypeScript的学习热情很高但也遇到了诸如“baseUrl选项被弃用”、“moduleResolutionnode10被弃用”等工具链更新带来的问题。这恰恰说明TypeScript生态在快速演进。在Puerts项目中我们通常会将TypeScript编译目标设置为ES2016或更高模块系统使用CommonJS以更好地与Puerts的模块加载机制配合。了解这些配置细节能避免在项目搭建初期就踩坑。3. 环境搭建与项目初始化实操3.1 引擎准备与Puerts插件安装首先你需要一个Unreal引擎建议4.25以上或5.0以上版本。Puerts是一个引擎插件所以安装方式就是标准的插件安装流程。最直接的方法是从GitHub仓库下载发布版本的压缩包或者克隆源码。我推荐使用发布版本稳定性更有保障。找到插件目录在你的Unreal项目根目录下或者引擎安装目录的Engine/Plugins下创建Plugins文件夹如果不存在。放置插件将下载的Puerts插件包通常包含Puerts目录整个放入Plugins文件夹内。生成项目文件右键点击你项目的.uproject文件选择“Generate Visual Studio project files”。这一步至关重要它会让Unreal Build ToolUBT识别到新插件。编译与启用用IDE如Visual Studio打开生成的项目解决方案编译整个项目。编译成功后启动Unreal编辑器。在编辑器菜单栏点击“编辑” - “插件”在搜索框输入“Puerts”你应该能看到它。确保其“已启用”复选框被勾选。注意有时插件编译可能会因为引擎版本差异而失败。请务必查看Puerts官方文档确认你下载的插件版本与你的引擎版本兼容。如果是从源码编译可能需要同步修改一些引擎源码文件初学者建议直接使用已编译好的发布版。3.2 TypeScript 开发环境配置Puerts插件安装好后你的项目里就具备了运行JavaScript/TypeScript的能力。但为了获得高效的开发体验我们还需要配置本地的TypeScript编译环境。初始化Node.js环境在你的项目根目录下打开命令行执行npm init -y来创建一个package.json文件。这标志着你的项目前端部分开始。安装TypeScript编译器执行npm install typescript --save-dev。建议安装较新的稳定版本如4.9以支持更多特性。创建TS配置文件在项目根目录创建tsconfig.json。这是TypeScript项目的核心配置文件。一个适用于Puerts的基础配置如下{ compilerOptions: { target: es2016, module: commonjs, lib: [es2016], sourceMap: true, outDir: Content/JavaScript, // 编译输出到Unreal可识别的目录 strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, moduleResolution: node, baseUrl: ./, // 注意此选项在TS 5.0的某些设置中可能被标记需结合paths使用 paths: { *: [Types/*] // 方便自定义类型声明查找 } }, include: [ Scripts/**/* // 你的TS源码存放目录 ], exclude: [ node_modules, Content/JavaScript ] }这里有几个关键点outDir我设置为Content/JavaScript这是Unreal引擎内容浏览器能直接识别的目录方便管理。module设为CommonJS因为Puerts的模块加载器与之兼容。关于网络热词中提到的baseUrl和moduleResolution弃用问题在目前常用的配置中只要按上述设置并明确指定moduleResolution为node即可避免警告并保证在未来的TypeScript 7.0中继续工作。编写你的第一个TypeScript文件在项目根目录创建Scripts文件夹然后在里面创建Main.ts。// Scripts/Main.ts console.log(Hello Puerts Unreal!); // 假设我们已经绑定了一个名为‘UE’的全局对象里面包含了引擎类 // 例如打印当前世界的名字这需要C绑定已存在 declare const UE: any; try { if (UE UE.KismetSystemLibrary) { const world UE.KismetSystemLibrary.GetWorld(); if (world) { console.log(World Name: ${world.GetName()}); } } } catch (e) { console.warn(Engine bindings not ready yet:, e); }编译与加载在命令行执行npx tsc -p .或者配置package.json中的scripts为build: tsc -p .然后执行npm run build。编译成功后你会在Content/JavaScript目录下看到生成的Main.js和.js.map源映射文件。在Unreal中执行你需要一种方式告诉Puerts去加载并执行这个js文件。常见做法是在游戏启动时或者通过一个特定的蓝图/C节点来调用Puerts的加载函数。最简单的方法是在游戏模式GameMode的BeginPlay事件中使用Puerts提供的蓝图节点“Execute Js Script”指定脚本文件路径为Content/JavaScript/Main.js。启动游戏你就能在输出日志Output Log中看到“Hello Puerts Unreal!”了。4. C 与 TypeScript 双向绑定深度解析4.1 将 C 类暴露给 TypeScript让TypeScript能调用C是Puerts的核心功能。Puerts提供了几种绑定方式这里介绍最常用也最强大的两种基于反射的自动绑定和手动声明绑定。基于反射的自动绑定这是最便捷的方式尤其适用于已经用UCLASS()、UFUNCTION()、UPROPERTY()等宏标记好的蓝图可访问类。Puerts可以读取Unreal的反射信息自动为这些类生成TypeScript的声明和绑定代码。你需要做的是在编译插件时确保开启了自动绑定功能默认是开启的。在项目的Build.cs文件中添加对Puerts模块的依赖。编写一个特殊的C文件通常叫TsGameInstance.cpp或类似在其中调用一个宏来声明需要自动绑定的模块。例如// TsGameInstance.cpp #include CoreMinimal.h #include Puerts.h IMPLEMENT_MODULE(FDefaultModuleImpl, YourGameModule) // 标准模块实现 // 使用Puerts的宏进行自动绑定声明 PUERTS_MODULE(YourGameModule, YourGameModule);重新编译项目后Puerts会自动生成绑定代码。在TypeScript中你就可以通过一个全局对象通常是UE来访问这些类了例如new UE.YourActorClass()。这种方式的优点是几乎零配置但缺点是生成的API可能比较“原始”暴露了所有反射信息有时不够简洁且对模板类、非UObject的C类支持有限。手动声明绑定这种方式更灵活可以精确控制暴露哪些函数、以什么形式暴露并且可以绑定非UObject的纯C类。你需要编写一个C函数来注册绑定。// MyBinding.cpp #include Puerts.h #include MyPureCppClass.h // 一个普通的C类 // 定义一个函数来包装C类的成员函数 void MyPureCppClass_Add(puerts::CallbackInfoType info) { auto self puerts::DataTransfer::GetPointerFastMyPureCppClass(info.GetHolder()); if (!self) return; int32 a info.GetNumber(0); int32 b info.GetNumber(1); int32 result self-Add(a, b); info.SetReturn(result); } // 注册绑定 void InitMyBindings(puerts::Object exports) { // 定义一个JS类构造函数 auto constructor puerts::DefineClassMyPureCppClass( MyPureCppClass, // 在TS中使用的类名 exports, // 挂载到的父对象 { // 构造函数 [](const puerts::CallbackInfo info) - void* { return new MyPureCppClass(); }, // 析构函数 [](puerts::CallbackInfo info, void* self) { delete static_castMyPureCppClass*(self); } }, { // 成员函数列表 {Add, MyPureCppClass_Add} }, { // 成员属性列表可选 // {SomeProperty, GetterFunc, SetterFunc} } ); } // 在合适的时机如游戏模块启动时调用这个初始化函数 // 例如在某个全局初始化函数中puerts::RegisterFunction(MyModule, InitMyBindings);然后在TypeScript端你需要手动编写对应的声明文件.d.ts这样才能获得类型提示。// Types/MyModule.d.ts declare module MyModule { export class MyPureCppClass { constructor(); Add(a: number, b: number): number; } }手动绑定的工作量更大但能实现最精细的控制是构建稳定、清晰桥接层的关键。4.2 在 TypeScript 中定义并供 C 调用的函数交互是双向的。C侧也经常需要调用TypeScript中定义的函数比如触发一个事件、请求一个回调。Puerts通过“JS函数对象”的概念来实现。在TypeScript中你可以将一个函数赋值给一个由C绑定的对象属性或者通过Puerts的特定API注册函数。方式一属性赋值最常用假设C侧暴露了一个全局对象GameEvents上面有一个属性OnPlayerHit。// 在TS中 declare const GameEvents: any; GameEvents.OnPlayerHit (damage: number, attackerId: number) { console.log(Player hit! Damage: ${damage}, Attacker: ${attackerId}); // 处理伤害逻辑... };C侧只需要在适当的时机如收到伤害时获取这个GameEvents.OnPlayerHit属性并判断它是否是一个函数然后调用它即可。方式二使用Puerts模块API注册Puerts提供了一个require函数可以加载其他JS模块。你也可以利用这个机制让TS模块导出一个函数集合供C调用。// Scripts/MyGameLogic.ts export function onGameStart(levelName: string) { console.log(Game started at level: ${levelName}); // 初始化游戏逻辑... } export function onGameEnd(score: number) { console.log(Game ended with score: ${score}); // 清理逻辑... }在C中你可以这样调用// C 侧 auto jsModule puerts::LoadModule(MyGameLogic); // 加载模块 if (jsModule) { auto onGameStartFunc jsModule-Get(onGameStart); // 获取函数 if (onGameStartFunc onGameStartFunc-IsFunction()) { // 准备参数并调用 puerts::Function onGameStart onGameStartFunc.Aspuerts::Function(); onGameStart.Execute({puerts::String::New(Map_01)}); } }实操心得双向调用的错误处理非常重要。在C调用TS函数时一定要用try...catch包裹因为TS代码可能抛出异常。同样在TS调用C时也要对可能返回的异常或空值进行处理。建议在项目初期就封装一套安全的调用工具函数统一处理错误日志和降级逻辑。5. 实战构建一个简单的交互式UI让我们用一个更具体的例子把上面的知识串起来在Unreal中用TypeScript控制一个UMG用户界面。5.1 创建UMG界面并暴露给TypeScript首先在Unreal编辑器中创建一个Widget Blueprint比如叫WBP_MainMenu。上面放一个按钮Button和一个文本框Text Block。我们需要将这个Widget的某些操作暴露给TS。传统C做法是创建一个继承自UUserWidget的C类然后编写蓝图。在Puerts方案下我们可以选择不写C类而是直接通过绑定系统来操作它。编写C绑定代码我们创建一个C类来封装对UMG的基本操作。这个类本身不需要是UCLASS因为它主要作为TS和引擎之间的桥梁。// TsUMGBridge.h / .cpp class FTsUMGBridge { public: // 创建一个Widget并添加到视口 static UUserWidget* CreateAndShowWidget(const FString WidgetBlueprintPath, int32 ZOrder 0); // 查找Widget上的子组件 static UWidget* FindWidgetChild(UUserWidget* Parent, const FString Name); // 为按钮绑定TS回调这里简化实际需要更复杂的机制来管理回调生命周期 static void BindButtonClick(UButton* Button, v8::Localv8::Function JsCallback); // 设置Text Block的文本 static void SetTextBlockText(UTextBlock* TextBlock, const FString Text); };然后按照4.1节中的手动绑定方法将这个FTsUMGBridge类的静态函数暴露给TypeScript假设我们将其挂载到全局对象UE.UMG下。编写TypeScript声明和逻辑// Types/UE.d.ts (补充声明) declare namespace UE { namespace UMG { function CreateAndShowWidget(widgetPath: string, zOrder?: number): any; // 返回UserWidget指针 function FindWidgetChild(parent: any, name: string): any; function BindButtonClick(button: any, callback: () void): void; function SetTextBlockText(textBlock: any, text: string): void; } } // Scripts/UI/MainMenuUI.ts class MainMenuUI { private rootWidget: any; constructor() { // 1. 创建并显示UI this.rootWidget UE.UMG.CreateAndShowWidget(/Game/UI/WBP_MainMenu.WBP_MainMenu); if (!this.rootWidget) { console.error(Failed to create main menu widget!); return; } // 2. 查找子组件 const startButton UE.UMG.FindWidgetChild(this.rootWidget, StartButton); // 假设按钮名 const titleText UE.UMG.FindWidgetChild(this.rootWidget, TitleText); // 假设文本框名 // 3. 绑定事件 if (startButton) { UE.UMG.BindButtonClick(startButton, this.onStartButtonClicked.bind(this)); } // 4. 设置初始文本 if (titleText) { UE.UMG.SetTextBlockText(titleText, 欢迎来到Puerts世界); } } private onStartButtonClicked(): void { console.log(Start button clicked from TypeScript!); // 在这里可以调用C函数开始游戏、切换关卡等 // 例如UE.KismetSystemLibrary.ExecuteConsoleCommand(open Map_01); // 或者调用一个自定义的C游戏管理器 } public destroy(): void { // 清理资源从视口移除Widget等需要C暴露对应接口 } } // 导出或全局初始化 export function initMainMenuUI(): MainMenuUI { return new MainMenuUI(); }5.2 处理生命周期与内存管理这个简单例子引出了一个关键问题内存管理与生命周期。在C和JavaScript两个拥有不同垃圾回收机制的环境间传递对象需要格外小心。C对象在TS中的引用当TS通过绑定拿到一个C对象指针如UUserWidget*时Puerts会为其在JS环境中创建一个“包装对象”。这个包装对象内部持有对C对象的弱引用或智能指针。通常对于继承自UObject的对象Puerts会利用Unreal的垃圾回收GC机制。只要UObject本身没有被C侧销毁TS侧就可以安全持有。但最佳实践是在TS中不要长期持有大量C对象引用应在不用时主动置空以便JS的GC能及时回收包装对象本身。JS函数在C中的引用当C保存了一个JS函数回调如按钮点击回调时Puerts会为其创建一个“持久句柄”Persistent Handle。你必须确保在C对象销毁前释放这个句柄否则会导致JS函数无法被GC引起内存泄漏。上面的BindButtonClick实现是简化的实际需要将v8::Persistentv8::Function保存到某个地方并在按钮或桥接类销毁时调用其Reset()方法。循环引用避免TS对象持有C对象而C对象又通过回调长期持有TS对象这会造成循环引用两者都无法被释放。设计时应明确所有权和生命周期通常由C侧主导生命周期如UI的创建与销毁TS侧作为“使用者”而非“所有者”。6. 调试、性能优化与最佳实践6.1 TypeScript 代码调试技巧在Unreal中调试TypeScript代码不像调试C那样可以直接断点但也有成熟的方法。源映射Source Map这是最重要的工具。在tsconfig.json中设置sourceMap: true后编译生成的.js文件会附带一个.js.map文件。当在Puerts环境中执行JS代码出错时控制台打印的调用栈信息会通过源映射自动转换回对应的TypeScript文件和行号极大方便定位问题。Console.log 与 Unreal 日志输出console.log、console.warn、console.error在Puerts中默认会输出到Unreal的“输出日志”Output Log中你可以像查看C日志一样查看它们。这是最直接的调试手段。使用 Chrome DevTools 远程调试高级Puerts支持与V8 Inspector集成这意味着你可以像调试网页一样用Chrome开发者工具来调试游戏内的TypeScript代码。这需要一些配置在启动游戏时传递--v8-inspector参数并在Chrome中打开chrome://inspect配置端口。配置成功后你可以看到游戏进程点击“inspect”就能打开一个完整的DevTools窗口里面可以设置断点、查看变量、执行表达式等。这对于复杂逻辑调试非常强大。编写可测试的代码将核心业务逻辑与引擎依赖分离。例如你的游戏状态机、配置管理器、技能计算器等尽量写成纯TypeScript类只通过接口与C交互。这样你可以用Node.js环境配合Jest或Mocha等测试框架对这些逻辑进行单元测试无需启动庞大的Unreal编辑器。6.2 性能关键点与优化策略在游戏运行时性能永远是首要考虑。使用Puerts时关注以下几点减少跨语言调用每一次从TS调用C函数或反之都有一定的开销。避免在每帧循环如Tick函数中进行大量的、细粒度的跨语言调用。例如不要在一个遍历1000个单位的循环里每次循环都调用C去获取单位的位置。正确的做法是在C侧一次性将所有单位的位置数据打包成一个数组或结构体通过一次调用传给TS或者直接由C处理密集计算。数据传输优化在C和TS之间传递数据时优先使用基本类型number, boolean, string或简单的平面对象。避免传递复杂的、嵌套很深的对象或大型数组。对于需要频繁传递的大量数据如顶点数据、网络包考虑使用ArrayBuffer或SharedArrayBuffer进行二进制数据传输效率最高。善用TypedArray对于数值计算在TS中使用Float64Array、Int32Array等类型化数组性能远高于普通JavaScript数组。如果C侧传递过来的是二进制缓冲区在TS中可以直接用类型化数组视图去操作。对象池在TS中频繁创建和销毁对象尤其是那些会对应C复杂对象的包装对象会产生GC压力。对于频繁使用的对象如向量Vector、变换Transform等实现一个简单的对象池来复用它们。Profile工具使用Unreal自带的性能分析工具如Unreal Insights和Chrome DevTools的Performance面板通过V8 Inspector连接来定位性能瓶颈。明确问题是出在TS脚本逻辑本身还是跨边界调用上。6.3 项目组织与工程化最佳实践当项目规模变大良好的工程结构至关重要。代码结构YourProject/ ├── Content/ │ └── JavaScript/ # TypeScript编译输出目录 ├── Scripts/ # TypeScript源码目录 │ ├── Core/ # 核心框架、工具类、管理器 │ ├── Gameplay/ # 游戏玩法逻辑 │ ├── UI/ # 用户界面逻辑 │ ├── Data/ # 配置表加载与处理 │ └── Libs/ # 第三方TS库或自研库 ├── Types/ # TypeScript声明文件 (.d.ts) │ ├── UE/ # 自动生成或手写的Unreal引擎API声明 │ ├── Custom/ # 自定义C绑定声明 │ └── Global.d.ts # 全局类型声明 ├── tsconfig.json └── package.json声明文件管理为所有C暴露的API维护准确的.d.ts文件。可以尝试使用工具从C头文件或绑定代码自动生成声明文件的骨架然后手动完善类型细节。这能保证类型安全并提升开发体验。构建流程将TypeScript编译集成到Unreal的构建流程中。可以在Build.cs中添加构建后事件PostBuildStep在编译C完成后自动执行npm run build。更高级的做法是使用Unreal的“自定义构建工具”Custom Build Tool功能将tsc设置为一个构建工具这样在IDE中编译项目时会自动触发TS编译。热重载与开发体验在开发期可以监听Scripts目录的文件变化自动触发tsc编译。同时在游戏运行时通过一个控制台命令或快捷键让Puerts重新加载指定的JS文件实现真正的“热重载”而无需重启游戏或关卡。7. 常见问题排查与解决方案实录在实际开发中你肯定会遇到各种各样的问题。这里记录了一些典型问题及其解决思路。7.1 绑定与调用相关问题1在TypeScript中调用C函数返回undefined或调用失败。排查步骤检查绑定是否成功确认你的绑定代码自动或手动确实被编译并链接到了最终的可执行文件中。可以在C绑定函数的开头加一句日志输出看是否被执行。检查函数签名确保TS中调用的函数名、参数个数和类型与C暴露的完全一致。C默认使用pascalCase命名而TS习惯camelCase注意转换。对于重载函数Puerts可能只绑定了一个版本。检查上下文确保TS代码执行时它试图访问的C对象如UE.UMG已经存在于Puerts的全局上下文中。有时绑定初始化顺序可能导致某些模块在TS代码执行时还未就绪。查看日志仔细查看Unreal的输出日志Puerts和V8通常会输出详细的错误信息包括“函数未找到”、“参数类型不匹配”等。问题2C调用TypeScript回调时游戏崩溃。主要原因通常是JS回调函数内部抛出了未捕获的异常并且这个异常穿透到了C侧而C侧没有妥善处理。解决方案在C调用JS函数的代码处必须使用try...catch块包裹。Puerts的Execute方法可能会抛出异常。捕获后可以记录错误日志并决定是否继续执行或进行降级处理。7.2 类型与编译相关问题3TypeScript编译时报错“找不到名称‘UE’”或“模块‘MyModule’没有默认导出”。原因声明文件.d.ts没有正确放置或配置。TypeScript编译器找不到这些类型定义。解决确保你的声明文件在tsconfig.json的include路径内或者被/// reference path... /指令引用。检查tsconfig.json中的baseUrl和paths配置确保能正确解析模块路径。对于全局声明如UE通常放在一个.d.ts文件中并且该文件被项目包含即可无需显式导入。对于模块声明在TS中使用import * as MyModule from MyModule;或import { MyClass } from MyModule;时确保声明文件使用了正确的export语法。问题4遇到网络热词中的警告“选项‘baseUrl’已弃用”或“选项‘moduleResolutionnode10’已弃用”。背景TypeScript在不断更新一些旧配置选项会被新的、更明确的选项取代。解决根据你的TypeScript版本tsc --version调整tsconfig.json。对于baseUrl它通常与paths配合使用来配置模块别名。这个选项本身在TS 5.0中在某些上下文中被标记为“已弃用”但如果你需要它目前继续使用是安全的。长期来看可以关注TS官方文档看是否有新的替代方案。对于moduleResolution明确设置为node或node16/nodenext。node10是一个旧策略直接改为node即可。node策略会模拟Node.js的模块解析方式这对于Puerts加载通过require引入的CommonJS模块是合适的。7.3 运行时与性能相关问题5游戏运行一段时间后出现卡顿或内存缓慢增长。排查方向内存泄漏JS侧使用Chrome DevTools的Memory面板通过V8 Inspector连接拍摄堆快照Heap Snapshot对比不同时间点的快照查看哪些对象在持续增长且未被释放。重点关注事件监听器是否未移除、闭包是否捕获了大型对象、定时器setInterval是否未清理。内存泄漏C侧检查C侧保存的JS函数持久句柄是否在对象销毁时被正确释放。使用Unreal的内存分析工具。频繁的GC如果JS堆内对象创建和销毁非常频繁V8的垃圾回收会频繁触发导致卡顿。优化策略见6.2节使用对象池、复用对象、减少临时对象创建。逻辑性能问题在TS的每帧更新循环中执行了过于复杂的计算或低效的算法。使用性能分析工具定位热点函数考虑将计算移至C或优化TS算法。问题6某些复杂的C类型如TArray、TMap在TypeScript中难以使用。现状Puerts对STL容器和Unreal的TArray、TMap等有基础支持但直接暴露给TS使用时API可能不直观且性能有损耗。最佳实践不要将整个复杂容器直接暴露。而是由C提供针对性的访问函数。例如对于TArrayFVector提供GetPositionAt(index)和GetPositionCount()函数而不是直接返回整个数组。或者提供一个迭代器接口。对于需要批量操作的场景可以考虑将数据序列化为简单的JS数组再传递。