Unity开发者必看:C# DLL封装全流程与IL2CPP兼容性指南

发布时间:2026/8/3 6:21:50

Unity开发者必看:C# DLL封装全流程与IL2CPP兼容性指南 1. 项目概述为什么Unity开发者需要自己封装DLL如果你是一个Unity开发者尤其是项目规模稍微大一点或者团队协作开发你大概率会遇到这样的场景手头有一套经过千锤百炼的通用工具类比如一个超级稳定的网络通信模块、一套自己写的数学库或者是一套处理特定资源格式的解析器。这些代码你希望在当前项目里用也希望在下一个、下下个项目里都能无缝复用而不想每次都把一堆C#脚本文件复制粘贴过去更不想因为某个项目的临时改动而污染了这套“祖传”的核心代码。另一种更常见的情况是你写了一些涉及敏感算法或者核心业务逻辑的代码不希望团队成员或者合作方能够轻易地看到和修改源码。这时候把代码编译成动态链接库DLL文件就成了一种非常自然的选择。DLL就像一个封装好的黑盒子你只需要把它扔进Unity的Plugins文件夹然后在代码里调用它公开的接口就行了内部的实现细节被隐藏和保护了起来。我最初接触这个需求是因为要和一个外部团队共享一套物理模拟算法。直接给源码风险太高而把算法核心编译成DLL只暴露几个关键的输入输出接口就完美解决了知识产权保护和代码交付的问题。整个过程用到的工具链非常明确Unity 2022 LTS作为运行时环境Visual Studio 2022作为代码编辑和编译工具语言就是C#。听起来很简单对吧但实际操作中从项目配置、编译选项到Unity端的引用每一步都有不少细节需要注意否则很容易遇到“DLL加载失败”、“找不到指定模块”或者“方法签名不匹配”这类让人头疼的问题。这篇内容我就结合自己踩过的坑把从零开始封装一个能在Unity 2022里正常使用的DLL的完整流程和核心要点拆解清楚。2. 环境准备与项目创建奠定正确的基础万事开头难而一个正确的开始能避免后续80%的奇怪问题。封装给Unity使用的DLL和我们平时写一个普通的C#控制台应用或者类库项目在项目类型和配置上有本质区别。2.1 工具链的确认与安装首先确保你的开发环境是匹配的。我强烈建议使用Unity Hub安装Unity 2022.3 LTS或更高版本但确保是2022版本系列因为LTS版本长期支持稳定性最好。Visual Studio这边使用Visual Studio 2022并且安装时务必勾选“使用Unity的游戏开发”工作负载或者至少确保安装了“.NET桌面开发”和“使用C#的桌面开发”这两个组件。这能保证VS2022自带Unity项目所需的.NET框架和目标包。检查一下你的VS2022里有没有“.NET Framework 4.x Targeting Pack”和“.NET Framework 4.7.1/4.8 Developer Pack”。Unity 2022默认使用的.NET兼容级别是“.NET Framework”或“.NET Standard 2.1”其底层运行时对应的是.NET Framework 4.x。如果你创建的项目类型不对可能会编译出Unity无法识别的DLL。2.2 创建正确的类库项目打开VS2022点击“创建新项目”。这里是最关键的一步不要选择“类库(.NET Framework)”或者“类库(.NET Standard)”。虽然它们的名字里都有“类库”但默认配置可能不适用于Unity。你应该在搜索框里搜索“类库”然后选择那个名为“类库旧版”的项目模板。这个“旧版”模板创建的是一个面向**.NET Framework**的传统类库项目它与Unity的Mono运行时或IL2CPP后端兼容性最好。如果找不到“旧版”也可以选择“类库(.NET Framework)”但创建后需要手动调整一些配置。给项目起个名字比如MyUnityUtility选择好位置。在接下来的配置对话框中“框架”一定要选择.NET Framework 4.7.1或4.8。这是目前与Unity 2022配合最稳定、问题最少的版本。低于4.7.1可能缺少一些API高于4.8则可能引入兼容性问题。点击“创建”项目就初始化好了。注意有些教程会建议创建“.NET Standard 2.0/2.1”类库。理论上Unity支持.NET Standard但在实际混合编译尤其是涉及原生插件交互或特定平台时.NET Framework类库的兼容性通常更稳妥特别是当你需要使用一些Windows特有的API时。如果你确定你的代码只使用纯粹的、跨平台的.NET API那么.NET Standard也可以但为了省去后续麻烦我建议初学者统一使用.NET Framework 4.7.1/4.8。2.3 初始项目结构清理与理解创建完成后VS会生成一个包含Class1.cs文件的项目。你可以直接把它删掉。现在你的解决方案资源管理器里应该主要就是一个MyUnityUtility.csproj项目文件。右键点击项目选择“属性”打开项目属性页。这里有几个地方需要检查“应用程序”标签页确保“目标框架”显示的是你刚才选择的.NET Framework 4.7.1或4.8。“生成”标签页留意“输出路径”默认是bin\Debug\。我们编译的DLL就会生成在这里。你可以暂时不管它。“生成事件”标签页这里后期可以配置一些自动化脚本比如编译后自动将DLL复制到Unity项目的Plugins文件夹非常方便。我们稍后会讲到。现在基础的空项目已经准备好了。它的“基因”是正确的这确保了最终产出的DLL能够在Unity的环境中健康运行。3. 编写可供Unity调用的C#代码项目创建好了接下来就是写代码。但这不仅仅是把功能实现那么简单你需要时刻想着这段代码是要被Unity引擎调用的。3.1 命名空间与基础类设计首先为你的DLL定义一个清晰的命名空间。这有助于在Unity中引用时避免命名冲突。例如namespace MyCompany.Utility { // 你的类将在这里定义 }接着开始编写你的工具类。假设我们要封装一个简单的数学工具包含一个计算向量点积的方法。创建一个新的C#文件比如VectorMath.cs。using System; namespace MyCompany.Utility { public static class VectorMath { /// summary /// 计算两个三维向量的点积。 /// /summary /// param namea向量A/param /// param nameb向量B/param /// returns点积结果/returns public static float DotProduct(float x1, float y1, float z1, float x2, float y2, float z2) { return x1 * x2 y1 * y2 z1 * z2; } // 你可以添加更多静态方法这是一个工具类常见的做法。 public static float DistanceSquared(float x1, float y1, float z1, float x2, float y2, float z2) { float dx x2 - x1; float dy y2 - y1; float dz z2 - z1; return dx * dx dy * dy dz * dz; } } }注意这里我使用了static静态类和静态方法。对于纯粹的工具函数这是最推荐的方式调用时无需实例化类非常方便例如VectorMath.DotProduct(...)。3.2 处理与Unity引擎的交互如果你的DLL需要调用Unity自身的API比如Debug.Log、GameObject、Vector3等情况就复杂一些。因为你的类库项目默认并没有引用Unity的DLL如UnityEngine.dll直接写UnityEngine.Vector3编译器会报错。正确的做法是在VS项目中添加对Unity引擎DLL的引用。找到你Unity编辑器的安装目录。通常路径类似C:\Program Files\Unity\Hub\Editor\2022.3.xxfxx\Editor\Data\Managed\UnityEngine.dll。在VS的解决方案资源管理器中右键点击你的项目下的“引用”选择“添加引用”。在弹出的窗口中点击“浏览”导航到上述路径选择UnityEngine.dll并添加。如果需要使用UnityEditor命名空间下的API注意这通常只在Editor环境下有效运行时DLL不能用同样方式添加UnityEditor.dll。添加引用后你就可以在代码中使用Unity的类型了。但是这里有一个极其重要的原则如果你希望这个DLL能在所有Unity平台Windows、Mac、Android、iOS等以及运行时不仅仅是编辑器使用那么你的代码必须避免引用UnityEditor.dll并且要谨慎使用那些仅在编辑器下可用的API。通常只引用和使用UnityEngine.CoreModule通过UnityEngine.dll中的内容是安全的。例如我们可以创建一个使用Unity原生Vector3类型的工具方法using UnityEngine; // 现在可以引用了 namespace MyCompany.Utility { public static class AdvancedMath { public static float UnityDotProduct(Vector3 a, Vector3 b) { return Vector3.Dot(a, b); // 直接使用Unity内置方法 } public static Vector3 ProjectPointOnLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd) { Vector3 lineVec lineEnd - lineStart; Vector3 pointVec point - lineStart; float t Vector3.Dot(pointVec, lineVec) / Vector3.Dot(lineVec, lineVec); t Mathf.Clamp01(t); // 使用Unity的Mathf return lineStart lineVec * t; } } }3.3 可见性控制与API设计封装DLL的一个重要目的就是隐藏实现细节。在C#中我们使用访问修饰符来控制可见性。public: 公开的可以被Unity中的C#脚本访问。这是你希望暴露给外部的接口。internal: 程序集内可见在DLL内部可以访问但Unity脚本无法直接调用。适合用于DLL内部的辅助类和方法。private/protected: 类内或继承链内可见。良好的DLL设计应该只将必要的接口设为public其他所有辅助逻辑、内部状态都设为internal或private。这样使用你DLL的人只会看到一个清晰、简洁的API列表而不会被内部复杂的实现所干扰同时也保护了你的代码逻辑。例如你有一个复杂的路径寻找算法namespace MyCompany.AI { public class Pathfinder // 对外公开的主类 { public Vector3[] CalculatePath(Vector3 start, Vector3 end) { // 公开接口内部调用私有或内部方法 InternalMapData map _mapManager.GetInternalData(); return _internalSolver.FindPath(start, end, map); } // 内部或私有的辅助类和字段 private MapManager _mapManager new MapManager(); private class InternalSolver { ... } // 私有内部类完全隐藏 } internal class MapManager { ... } // internal类DLL外不可见 }4. 编译配置与生成DLL代码写好了接下来就是把它变成.dll文件。编译不是简单地点一下“生成”其中的配置选项直接影响DLL在Unity中的兼容性。4.1 关键编译配置详解右键项目 - “属性”我们重点看“生成”标签页和“高级”按钮。配置与平台在VS顶部工具栏确保“解决方案配置”是“Release”而不是“Debug”。Debug版本包含调试符号文件更大且可能因优化级别不同导致一些微妙问题。发布给他人或用在自己项目里都用Release版。平台通常选择“Any CPU”但这里需要特别注意。“高级”生成设置目标CPU对于“Any CPU”平台点击“高级”按钮。在“高级生成设置”对话框中将“目标CPU”从“AnyCPU”改为“x86”或“x64”。我强烈推荐选择“x64”。因为现代Unity编辑器基本都是64位的且最终发布的PC平台也以64位为主。选择特定架构可以让编译器进行一些特定优化并避免一些潜在的兼容性问题。如果你明确需要支持32位平台则可以选“x86”或者为不同平台分别编译。调试信息在Release配置下选择“无”或“pdb-only”。“pdb-only”会生成独立的程序数据库文件.pdb这个文件在Unity中如果放在同目录可以在出错时显示具体的行号对调试有帮助但非必需。如果追求DLL最小化就选“无”。输出路径默认的bin\Release\或bin\x64\Release\就可以。你可以记下这个完整路径比如C:\MyCode\MyUnityUtility\bin\x64\Release\。4.2 执行编译与查找生成文件点击VS菜单栏的“生成” - “生成解决方案”或按F6。如果代码没有错误输出窗口会显示“生成成功”。现在打开文件资源管理器导航到你项目下的输出路径例如C:\MyCode\MyUnityUtility\bin\x64\Release\。你应该能看到至少两个文件MyUnityUtility.dll这就是我们需要的动态链接库文件。MyUnityUtility.pdb调试符号文件如果上一步选择了生成。MyUnityUtility.dll就是我们的成果物。你可以把它复制出来备用。4.3 使用生成后事件实现自动化每次修改代码后都要手动去复制DLL太麻烦了。我们可以利用VS的“生成后事件”自动完成这个步骤。假设你的Unity项目路径是D:\UnityProjects\MyGame。在VS中右键项目 - “属性” - “生成事件”标签页。在“生成后事件命令行”框中输入以下命令copy /Y $(TargetPath) D:\UnityProjects\MyGame\Assets\Plugins\MyUnityUtility.dll$(TargetPath)是VS的宏代表最终生成的DLL的完整路径如C:\...\Release\MyUnityUtility.dll。copy /Y是复制并强制覆盖。目标路径是你Unity项目的Assets\Plugins文件夹。如果Plugins文件夹不存在你需要先创建它。这是Unity识别外部DLL的标准位置之一。点击“确定”保存。这样每次在VS中成功编译生成项目后最新的DLL就会自动被复制到Unity项目的指定位置。你只需要切换回Unity编辑器它就会自动检测到更新并重新导入这个DLL非常高效。5. 在Unity中部署、引用与测试DLLDLL生成并放到正确的位置后Unity编辑器会自动将其识别为一种特殊的资源——插件Plugin。但要让脚本能顺利调用还需要注意部署的规则和引用的方式。5.1 DLL在Unity中的部署规则Unity对DLL的存放位置有约定俗成的规则不同的位置会影响DLL的加载顺序和可用平台。Assets/Plugins这是最常用、最推荐的位置。放在这里的DLL会被Unity自动加载并且可以通过Inspector窗口配置其平台设置例如某些DLL只用于Windows某些只用于Android。Assets/Plugins/x86, Assets/Plugins/x86_64如果你同时有32位和64位的同名DLL可以分别放在这两个子文件夹下Unity会根据目标平台自动选择加载。Assets/StreamingAssets这个文件夹下的内容不会被Unity自动编译或处理会原封不动地打包进最终应用。通常不把托管DLL放这里因为Unity不会自动加载它。这里更适合放一些需要运行时动态读取的资源文件或者一些特殊格式的原生插件。最佳实践对于我们自己用C#编写的托管DLL一律放在Assets/Plugins目录下。你可以在这个目录下再建立子文件夹来分类管理比如Assets/Plugins/MyCompany/。将DLL文件例如MyUnityUtility.dll拖入Unity项目的Assets/Plugins文件夹。Unity控制台会显示导入进度。导入完成后在Project窗口选中这个DLL你可以在Inspector窗口中看到它的导入设置。5.2 Inspector配置详解选中DLL文件Inspector窗口会出现“Plugin Inspector”。这里有几个关键设置Select platforms for plugin选择该插件在哪些平台生效。默认是“Any Platform”。如果你的DLL包含了平台相关的代码比如调用了Windows API那么务必取消勾选其他平台如Android、iOS否则在打包那些平台时会报错。Load on Startup是否在启动时加载。对于托管DLL通常保持默认勾选即可。Validate References验证引用。如果勾选Unity会检查DLL中是否有对不存在的程序集的引用有助于提前发现问题。对于我们自己编写的纯C#托管DLL通常只需要检查一下平台设置是否正确即可其他保持默认。5.3 在Unity脚本中调用DLL方法DLL部署好之后在Unity中调用其公开方法就和使用普通的C#类一样简单。你需要确保你的Unity脚本能够“看到”DLL中的命名空间。在Unity中创建测试脚本在Unity中创建一个新的C#脚本比如TestDLL.cs。添加using指令在脚本文件顶部添加你DLL中定义的命名空间。using MyCompany.Utility; // 引入我们DLL的命名空间 using UnityEngine;调用公开方法现在你就可以像使用Unity自带类一样使用DLL中的类和方法了。public class TestDLL : MonoBehaviour { void Start() { // 调用我们DLL中的静态方法 float dotResult VectorMath.DotProduct(1f, 2f, 3f, 4f, 5f, 6f); Debug.Log($点积结果: {dotResult}); // 输出: 点积结果: 32 // 调用使用了Unity类型的DLL方法 Vector3 vecA new Vector3(1, 0, 0); Vector3 vecB new Vector3(0, 1, 0); float unityDot AdvancedMath.UnityDotProduct(vecA, vecB); Debug.Log($Unity向量点积: {unityDot}); // 输出: 0 // 测试更复杂的方法 Vector3 point new Vector3(5, 0, 0); Vector3 lineStart Vector3.zero; Vector3 lineEnd new Vector3(10, 0, 0); Vector3 projectedPoint AdvancedMath.ProjectPointOnLine(point, lineStart, lineEnd); Debug.Log($投影点: {projectedPoint}); // 输出: (5.0, 0.0, 0.0) } }挂载并运行将TestDLL脚本挂载到场景中的任意GameObject上运行游戏。如果一切配置正确你将在Unity的控制台看到对应的日志输出。这个过程如果顺利就证明你的DLL已经被Unity成功加载并且其中的代码可以正常执行。这标志着从编写、编译到集成的完整链路已经打通。6. 高级话题处理依赖、版本与IL2CPP在简单的工具类之外现实项目中的DLL可能会更复杂涉及到引用其他第三方DLL、处理版本冲突以及面对Unity的IL2CPP编译后端。6.1 管理第三方依赖如果你的DLL项目引用了其他的NuGet包或第三方DLL比如Newtonsoft.Json用于JSON处理你需要将这些依赖一并提供给Unity。将依赖DLL一并放入Plugins在VS中你的项目引用了其他库。编译时这些依赖默认不会复制到输出目录除非将其引用属性“复制本地”设置为True。你可以在VS的解决方案资源管理器中展开“引用”找到对应的依赖右键属性将“复制本地”设为True。这样编译时这些依赖DLL也会生成在输出目录下。将所有DLL一起部署将主DLLMyUnityUtility.dll和所有它依赖的第三方DLL如Newtonsoft.Json.dll一起复制到Unity项目的Assets/Plugins文件夹中。Unity在导入主DLL时会尝试解析其依赖。如果依赖DLL也在Plugins目录下通常就能自动找到。注意依赖冲突如果Unity项目本身或其其他插件已经包含了不同版本的同一个依赖例如Unity项目里通过Package Manager安装了Newtonsoft.Json13.0而你的DLL依赖的是12.0就可能发生冲突。这可能导致类型加载异常或运行时错误。解决方法是尽量统一依赖版本或者使用Assembly-CSharp程序集不直接引用的、经过强命名版本隔离的依赖。6.2 程序集定义文件与版本控制当项目规模扩大你可能会有多个DLL它们之间可能有依赖关系。Unity 2017.3之后引入了程序集定义文件Assembly Definition File,.asmdef来管理内部代码的编译程序集。对于外部DLL.asmdef文件也能用来管理依赖。你可以为你的DLL创建一个同名的.asmdef文件但这不是必须的托管DLL本身就是一个完整的程序集。更常见的用法是在Unity主工程中如果你有独立的模块想引用这个外部DLL你可以在该模块的.asmdef文件的“Assembly Definition References”或“Override References”中添加对这个外部DLL程序集的引用。在Inspector中你可以搜索到已导入的MyUnityUtility程序集。关于版本建议在DLL的项目属性中设置明确的版本号右键项目 - 属性 - “程序包”标签页 - “程序集版本”和“文件版本”。这有助于在出现问题时进行排查。6.3 IL2CPP兼容性深度解析Unity构建应用时有两个主要的脚本后端Mono和IL2CPP。IL2CPP会将C#代码先编译成中间语言IL再转换成C代码最后编译为原生机器码这带来了更好的性能和安全性也是发布到iOS等平台的唯一选择。但IL2CPP对代码的“纯洁性”要求更高。可能导致IL2CPP下DLL出问题的常见原因反射大量或动态的反射操作在IL2CPP下可能失效因为IL2CPP的代码剪裁Stripping可能会移除未被静态分析引用的类型和方法。如果你的DLL内部使用了反射需要在Unity的“Player Settings” - “Other Settings” - “Managed Stripping Level”中降低剪裁等级如设为Low或者使用[Preserve]属性标记需要保留的代码。不支持的.NET API虽然Unity支持.NET Standard 2.1但IL2CPP并非支持所有API。一些非常陈旧的、平台特定的API如System.AppDomain的某些方法可能不可用。编写DLL时应尽量使用最通用、最常见的API。原生代码交互如果你的DLL还通过[DllImport]调用了原生C DLL那么在IL2CPP构建时需要确保也有对应平台如iOS、Android的原生库文件并且正确配置其平台属性。测试IL2CPP兼容性在Unity编辑器中切换到IL2CPP后端进行测试是不现实的。最可靠的方法就是实际构建。为目标平台如Windows、Android进行一次Development Build并勾选“Create Visual Studio Solution”对于Windows或“Export Project”对于Android然后分析构建日志中的错误和警告。早期、频繁地为目标平台进行构建测试是确保DLL兼容性的最佳实践。7. 故障排除与调试技巧实录即使按照步骤操作你也可能会遇到各种问题。下面是我在多次封装DLL过程中遇到的典型问题及其解决方法。7.1 常见编译与加载错误错误信息可能原因解决方案DllNotFoundException: MyUnityUtility或The specified module could not be found.1. DLL未放置在Assets/Plugins或其子目录下。2. DLL依赖的其他原生库非C# DLL缺失。3. 平台设置错误例如DLL设置为仅限Editor但你在播放模式或打包后调用。1. 检查DLL文件位置。2. 使用Dependency Walker或dumpbin /dependentsVS命令行工具检查DLL的依赖确保所有依赖库都可用。3. 在Unity中选中DLL检查Inspector中的平台设置。BadImageFormatException最常见的原因DLL的位数与当前运行环境不匹配。例如在64位Unity编辑器或播放器中加载了32位x86编译的DLL反之亦然。在VS中确认项目生成的目标平台x86/x64与你的Unity编辑器位数一致。统一使用x64是最省心的选择。检查DLL属性右键文件-属性-详细信息看是否有“32位”或“64位”提示。TypeLoadException或MissingMethodException1. DLL的.NET Framework版本高于或低于Unity运行时支持的范围。2. 方法签名不匹配你调用的方法参数与DLL中实际的不符。3. 引用了Unity不支持的命名空间或API。1. 确保DLL目标框架为.NET Framework 4.x如4.7.1。2. 仔细核对方法名、参数类型和数量。3. 确保DLL没有引用UnityEditor等仅在编辑器下可用的程序集。Unity编辑器控制台无错误但代码不执行或返回默认值1. 代码逻辑本身有bug。2. 在Unity中修改了DLL代码后没有重新编译和替换DLL文件。3. Unity没有重新编译脚本有时存在缓存问题。1. 在VS中调试你的类库项目需要附加到Unity编辑器进程较复杂。2. 确认生成后事件已执行或手动替换了DLL。3. 尝试在Unity中Assets - Reimport该DLL文件或重启Unity编辑器。7.2 使用PDB文件进行符号调试如果你在生成DLL时选择了生成PDB文件并且将它和DLL一起放在了Assets/Plugins下那么当DLL中的代码抛出异常时Unity的错误堆栈跟踪将显示具体的文件名和行号而不是一个模糊的偏移地址。这能极大提升调试效率。具体做法就是将MyUnityUtility.pdb文件放在与MyUnityUtility.dll相同的目录下即Assets/Plugins。Unity在导入DLL时会一并处理PDB文件。下次DLL中发生异常时你看到的错误信息就会包含类似at MyCompany.Utility.VectorMath.DotProduct(Single x1, Single y1, Single z1, Single x2, Single y2, Single z2) in C:\MyCode\MyUnityUtility\VectorMath.cs:line 15这样具体的信息。7.3 版本冲突与程序集重定向当你更新DLL后有时Unity可能会因为缓存了旧版本而出现奇怪的行为。彻底清理的方法是关闭Unity编辑器。删除项目中的Library和obj文件夹Temp文件夹也可删。注意Assets和ProjectSettings不要删。重新打开Unity项目它会重新导入所有资源包括最新的DLL。如果项目中存在多个版本的同一DLL比如一个在Plugins一个通过其他方式引入可能会导致AmbiguousMatchException等错误。确保整个项目中只存在一份你需要的DLL文件。7.4 一个真实的排查案例从“找不到指定模块”到完美运行我曾经封装一个包含复杂图像处理的DLL在编辑器里运行良好但打包成Windows独立应用后一运行就崩溃日志显示DllNotFoundException。排查过程如下第一反应检查DLL是否被打包。确认Assets/Plugins下的DLL平台设置包含了“Standalone”。使用依赖检查工具用dumpbin /dependents MyImageLib.dll命令查看发现它依赖一个名为libtiff-5.dll的C原生库。我的开发机上因为安装了其他软件系统路径里有这个库所以编辑器能运行。解决方案找到这个原生库文件将其一并放入Assets/Plugins/x86_64因为我的目标是64位Windows。在Unity中选中这个原生DLL在Inspector中将其平台设置为“Standalone”并只勾选“x86_64”。重新打包问题解决。这个案例的教训是对于托管C# DLL如果它内部通过P/Invoke调用了原生代码那么这些原生依赖也必须一并打包并且正确设置平台。

相关新闻