如何实现Unity游戏的便捷代码式模组支持?
可行性分析
完全可行,但需注意三类核心问题:
- 安全风险:加载外部C#代码等同于允许执行任意逻辑,需提醒用户仅加载信任模组,或额外实现签名验证机制。
- 性能开销:运行时编译代码会占用启动时间,动态加载的程序集无法享受AOT优化(IL2CPP模式下更明显)。
- 版本兼容性:Unity的Mono/IL2CPP模式对动态代码支持差异较大,IL2CPP下部分反射、动态API可能受限。
实现步骤
1. 定义模组规范与公共API
先在游戏内约定模组必须实现的接口,同时暴露可供调用的核心API:
// 模组必须实现的接口,定义生命周期方法 public interface IGameMod { void OnModLoaded(); void Start(); } // 暴露给模组的公共API public static class MyGameAPI { public static void RegisterItem(ItemData item) { // 游戏内物品注册逻辑 } }
2. 读取外部C#代码文件
通过Unity的Application.persistentDataPath定位到目标模组目录,遍历并读取所有.cs文件:
using System.IO; using UnityEngine; void LoadMods() { string modRootDir = Path.Combine(Application.persistentDataPath, "Mods"); if (!Directory.Exists(modRootDir)) return; foreach (string csFilePath in Directory.GetFiles(modRootDir, "*.cs", SearchOption.AllDirectories)) { string modCode = File.ReadAllText(csFilePath); CompileAndLoadMod(modCode); } }
3. 编译并加载模组程序集
解决Roslyn的NuGet兼容问题后,用Roslyn完成代码编译与加载:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System.Reflection; using System.Collections.Generic; using System.IO; void CompileAndLoadMod(string code) { // 配置编译选项:生成动态链接库 var compileOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) .WithOptimizationLevel(OptimizationLevel.Release); // 添加必要的程序集引用 var references = new List<MetadataReference>(); // Unity核心程序集 references.Add(MetadataReference.CreateFromFile(typeof(MonoBehaviour).Assembly.Location)); // 游戏主程序集(包含IGameMod和MyGameAPI) references.Add(MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location)); // .NET基础库 references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // 生成语法树并编译 var syntaxTree = CSharpSyntaxTree.ParseText(code); var compilation = CSharpCompilation.Create($"Mod_{Guid.NewGuid()}") .WithOptions(compileOptions) .AddReferences(references) .AddSyntaxTrees(syntaxTree); // 编译到内存流 using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); // 处理编译错误 if (!emitResult.Success) { foreach (var error in emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) { Debug.LogError($"模组编译失败: {error.GetMessage()}"); } return; } // 加载编译后的程序集 ms.Seek(0, SeekOrigin.Begin); Assembly modAssembly = Assembly.Load(ms.ToArray()); InitializeMods(modAssembly); }
4. 初始化模组并绑定生命周期
遍历程序集中实现IGameMod的类型,实例化后绑定到Unity的生命周期:
void InitializeMods(Assembly assembly) { foreach (Type modType in assembly.GetTypes()) { if (typeof(IGameMod).IsAssignableFrom(modType) && !modType.IsInterface && !modType.IsAbstract) { // 实例化模组 IGameMod mod = Activator.CreateInstance(modType) as IGameMod; mod.OnModLoaded(); // 用Wrapper转发Unity生命周期(比如Start) GameObject modGO = new GameObject($"Mod_{modType.Name}"); ModLifecycleWrapper wrapper = modGO.AddComponent<ModLifecycleWrapper>(); wrapper.TargetMod = mod; } } } // 封装类:将MonoBehaviour生命周期转发给模组 public class ModLifecycleWrapper : MonoBehaviour { public IGameMod TargetMod { get; set; } void Start() { TargetMod?.Start(); } // 可按需添加Update、OnDestroy等生命周期方法 }
Roslyn NuGet包无法解析的解决方案
Unity对NuGet的复杂包支持有限,需手动导入Roslyn的核心DLL:
- 下载对应版本的
Microsoft.CodeAnalysis.CSharpNuGet包(推荐选支持.NET Framework 4.7.2的4.x版本)。 - 解压NuGet包,进入
lib/net472目录,提取以下DLL:Microsoft.CodeAnalysis.dllMicrosoft.CodeAnalysis.CSharp.dllMicrosoft.CodeAnalysis.CSharp.Scripting.dllMicrosoft.CodeAnalysis.Scripting.dllSystem.Collections.Immutable.dllSystem.Reflection.Metadata.dll
- 将这些DLL复制到Unity项目的
Assets/Plugins文件夹。 - 在Unity Player Settings中,设置API Compatibility Level为
.NET Framework(不要选.NET Standard)。 - 选中Plugins目录下的DLL,在Inspector中设置目标平台(如Windows),确保
Allow unsafe code按需开启。
额外注意事项
- IL2CPP适配:若游戏用IL2CPP编译,动态加载的程序集无法被AOT优化,部分反射API可能失效,需提前做兼容性测试。
- 错误调试:需为模组开发者提供编译错误日志,方便定位问题。
- 模组卸载:若支持热重载,需实现模组的卸载逻辑(销毁Wrapper GameObject、释放程序集资源)。
内容的提问来源于stack exchange,提问作者GoogleCoder
相关产品推荐
相关产品推荐

