
1. 项目概述为什么Unity跨平台开发绕不开条件编译做Unity开发尤其是涉及到多平台发布比如同时要上安卓、iOS、PC、WebGL甚至是一些主机平台有一个问题你迟早会碰到同一份代码在不同平台上的行为需要不一样。比如你想在安卓上调用原生的震动功能在iOS上使用GameCenter的成就系统在PC上读取特定的本地文件路径在WebGL上则要避免使用某些阻塞主线程的同步操作。如果为每个平台都写一套独立的脚本那维护成本会高到无法想象代码也会变得臃肿不堪。这时候条件编译就成了我们的救命稻草。它不是什么黑科技而是编译器提供的一个基础功能在代码编译成最终程序之前根据预先定义好的“符号”也就是我们说的“宏”来决定哪些代码块被包含进去哪些被剔除掉。Unity引擎本身就为我们准备了一整套针对不同平台的内置宏比如UNITY_ANDROID、UNITY_IOS、UNITY_STANDALONE_WIN等等。直接使用这些宏就能轻松写出“一份代码多处适配”的逻辑。但内置宏只是解决了“平台识别”的问题。在实际项目中我们往往有更复杂的差异化需求。比如你的游戏在“中国区版本”和“海外版本”需要接入不同的SDK在“开发调试模式”下需要开启详细的日志和作弊功能而在“发布模式”下则要彻底关闭。这些需求已经超出了“平台”的范畴进入了“项目配置”和“业务逻辑”的领域。这时自定义宏就闪亮登场了。通过在Unity编辑器里或者构建脚本中定义我们自己的符号我们可以实现更精细、更灵活的代码控制。所以这个项目的核心就是深入理解并熟练运用Unity的内置平台宏和自定义宏构建一套清晰、可维护的代码差异化方案。这不仅仅是写几个#if那么简单它涉及到项目架构的整洁性、团队协作的规范性以及后期维护的便捷性。接下来我会结合我踩过的无数个坑带你从原理到实践彻底掌握这套技巧。2. 核心需求解析什么情况下必须使用宏在动手写代码之前我们先得搞清楚到底哪些场景是必须、或者强烈推荐使用条件编译宏的。盲目使用只会增加代码的复杂度。根据我的经验主要有以下几类刚性需求2.1 平台专属API调用这是最经典、也是最无可替代的场景。当你的代码需要调用某个平台特有的原生接口时必须用宏隔离。移动端原生功能震动、陀螺仪、相机、相册访问、推送通知等。iOS用的是UnityEngine.iOS.NotificationServices或新的UserNotifications框架而安卓则需要通过AndroidJavaClass和AndroidJavaObject来调用Java接口。这两套API绝对不能混编。平台特定输入手柄按键映射、触屏手势处理等。虽然Unity Input System在一定程度上做了统一但深层适配时仍有差异。文件系统路径Application.persistentDataPath在各个平台上的实际路径是不同的但如果你需要访问平台特定的目录如iOS的Library/Caches或安卓的外部存储就可能需要平台特定的代码。2.2 性能与行为适配不同平台的硬件能力和运行环境天差地别代码行为也需要相应调整。线程与异步操作在WebGL平台由于运行在浏览器单线程环境中所有阻塞性操作如Thread.Sleep和真正的多线程都无法使用。必须使用#if UNITY_WEBGL来包裹或替换这些代码通常用协程Coroutine或基于Promise的异步模式替代。图形与渲染设置虽然Unity的渲染管线做了抽象但某些高级特效或底层图形API的调用如DirectX、Metal、Vulkan的特有功能可能需要条件编译。内存与资源管理移动端内存敏感可能需要更激进的资源卸载策略而PC端则可以更宽松一些。2.3 商业与分发配置这是自定义宏大显身手的地方它帮助我们管理同一套代码下的不同产品变体。渠道SDK接入国内安卓渠道繁杂每个渠道如应用宝、小米、华为的登录、支付、分享SDK都不一样。通过定义如CHANNEL_XIAOMI、CHANNEL_HUAWEI这样的宏可以在打包时自动链接对应的SDK代码。区域化版本全球发布时不同地区可能受法律或政策影响需要屏蔽或替换某些功能如社交分享、用户生成内容。可以用REGION_CN、REGION_GLOBAL等宏来控制。版本类型区分开发版DEVELOPMENT_BUILD、测试版ALPHA_BUILD/BETA_BUILD和发布版。开发版可以开启控制台、内存查看器、测试关卡传送门等调试功能。2.4 代码可读性与维护性使用宏的终极目的是为了让代码更清晰而不是更混乱。如果把所有平台的if-else逻辑都用运行时判断比如if (Application.platform RuntimePlatform.Android)来写会有两个大问题所有平台的代码都会被编译进最终包即使目标平台用不到这会导致包体无谓增大在移动端尤其致命。代码逻辑混杂可读性差。条件编译能将无关平台的代码彻底从目标版本中移除保证最终产物的纯净。注意Application.platform是运行时判断而#if UNITY_ANDROID是编译时判断。前者所有代码都参与编译包体大后者只有符合条件的代码参与编译包体小。对于平台特性代码优先使用编译时宏。3. Unity内置平台宏全解析与使用指南Unity已经为我们定义好了几乎所有主流平台的识别宏。了解它们的具体定义和适用范围是第一步。3.1 常用平台宏清单下面这个表格整理了最常用的一些内置宏建议收藏宏定义描述典型应用场景UNITY_EDITOR在Unity编辑器中运行代码时定义。编辑器工具开发仅在编辑模式下生效的调试代码。UNITY_STANDALONE针对独立的桌面平台Windows, Mac, Linux。通用PC/Mac逻辑。常与更具体的宏结合使用。UNITY_STANDALONE_WIN仅针对Windows平台。调用Windows API处理Windows特定路径如注册表。UNITY_STANDALONE_OSX仅针对macOS平台。调用macOS原生API。UNITY_STANDALONE_LINUX仅针对Linux平台。服务器端或特定Linux发行版逻辑。UNITY_ANDROID针对Android平台。调用JNI接口使用安卓特有功能震动、Toast等。UNITY_IOS针对iOS/tvOS平台。调用Objective-C API通过[DllImport(__Internal)]使用GameCenter、StoreKit等。UNITY_WEBGL针对WebGL平台。处理浏览器交互避免使用线程、同步阻塞API使用JS交互。UNITY_PS4,UNITY_XBOXONE等针对特定游戏主机平台。主机平台的成就系统、社交功能、手柄输入等。UNITY_SWITCH针对Nintendo Switch平台。Switch平台特有功能。UNITY_6464位操作系统。处理大内存寻址相关代码现在基本默认。3.2 使用模式与最佳实践知道了有哪些宏接下来看看怎么用得好。直接写#if是最基础的但我们需要更优雅的模式。基础用法void Vibrate() { #if UNITY_ANDROID // 调用Android原生震动代码 using (AndroidJavaClass unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) using (AndroidJavaObject currentActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity)) using (AndroidJavaObject vibrator currentActivity.CallAndroidJavaObject(getSystemService, vibrator)) { vibrator.Call(vibrate, 500); // 震动500毫秒 } #elif UNITY_IOS // 调用iOS原生震动仅部分机型支持 // 这里通常需要通过C#调用一个用Objective-C写的插件 Handheld.Vibrate(); // Unity的封装但功能有限 #else // 其他平台如PC、WebGL可能不支持或模拟震动 Debug.Log(Vibration not supported on this platform.); #endif }进阶用法使用#region组织代码当平台差异化代码块很大时用#region折叠起来能让代码结构非常清晰。public class PlatformUtility { public static string GetExternalStoragePath() { #if UNITY_ANDROID #region Android平台实现 // ... 复杂的JNI代码 ... return androidPath; #endregion #elif UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX #region PC平台实现 // ... 使用System.IO组合特定路径 ... return pcPath; #endregion #else #region 其他平台回退方案 return Application.persistentDataPath; #endregion #endif } }处理未定义情况#else和#warning/#error一定要为未预期的平台提供回退方案或明确提示。public void UsePlatformSpecificFeature() { #if UNITY_ANDROID // Android实现 #elif UNITY_IOS // iOS实现 #else // 重要对于必须实现的功能可以用#error阻止编译 // #error “This feature must be implemented for the target platform!” // 或者用#warning给出警告但允许编译 #warning “Platform-specific feature not fully implemented for this platform. Using fallback.” UseFallbackFeature(); #endif }使用#error可以在编译阶段就发现问题避免运行时崩溃这在团队协作中非常有用。3.3 常见陷阱与避坑指南UNITY_EDITOR的误用UNITY_EDITOR宏只在编辑器内运行或编译时生效。如果你在编辑器模式下打了安卓包Build然后在这个包里运行UNITY_EDITOR是不定义的。很多人误以为在编辑器里测试就等于测试了UNITY_EDITOR分支其实大错特错。测试平台代码请务必使用目标平台构建或Build Run。宏的嵌套与逻辑宏预处理指令也支持逻辑运算如(与),||(或),!(非)。例如#if UNITY_IOS !UNITY_EDITOR表示“在iOS真机或模拟器上但不在Unity编辑器里”。合理使用它们可以精确控制条件。平台宏的互斥性通常一个构建目标只会定义一组相关的宏。比如为Android构建时不会定义UNITY_IOS。但UNITY_EDITOR可能和其他平台宏同时定义比如在编辑器里选择Android作为活动平台时。理解这一点对编写编辑器工具很重要。代码折叠导致的视觉欺骗在IDE中被#if条件排除的代码会变灰甚至折叠。有时你会忘记这些代码的存在导致在修改功能时只改了当前平台可见的代码而忽略了其他平台的实现从而引入bug。定期检查所有平台分支的代码是一个好习惯。4. 自定义宏的威力从项目配置到代码架构如果说内置宏是Unity给的“标准答案”那么自定义宏就是我们自己出的“附加题”它能解决项目里更个性化、更复杂的问题。4.1 如何定义自定义宏有三种主要方式适用于不同场景Unity编辑器设置Player Settings路径File - Build Settings - 选择目标平台 - Player Settings... - Player - Other Settings - Scripting Define Symbols。格式符号之间用分号;隔开例如DEVELOPMENT_BUILD;USE_ANALYTICS。特点这是最常用的方式。可以针对每个平台进行不同的宏定义。比如在Android平台定义CHANNEL_HUAWEI在iOS平台定义USE_APPLE_SIGNIN。代码中使用#define位置必须在C#脚本文件的最顶部在任何using语句之前。#define LOCAL_DEBUG // 这个宏只在本文件内有效 using UnityEngine;特点作用域仅限于当前文件。通常用于单个脚本内部的局部调试或条件开关不适合做全局配置。通过构建脚本或命令行参数在使用UnityEditor.BuildPipeline进行自动化构建时可以通过BuildPlayerOptions.extraScriptingDefines或修改PlayerSettings.SetScriptingDefineSymbolsForGroup来动态设置宏。特点适用于CI/CD持续集成/持续部署流水线可以根据构建分支、版本号等自动注入不同的宏定义比如AUTOMATED_BUILD、VERSION_2_0。4.2 自定义宏的典型应用场景场景一管理功能模块开关项目中有一些实验性功能或可插拔模块不希望直接删除代码可以用宏来控制。// 在Player Settings中定义 ENABLE_EXPERIMENTAL_AI public class GameManager : MonoBehaviour { void Update() { // ... 核心游戏逻辑 ... #if ENABLE_EXPERIMENTAL_AI UpdateExperimentalAI(); // 只有定义了该宏这行代码才会被编译 #endif } #if ENABLE_EXPERIMENTAL_AI void UpdateExperimentalAI() { // 实验性AI逻辑 } #endif }这样在给测试团队打包时开启这个宏给玩家发布时关闭非常灵活。场景二渠道/区域差异化这是国内移动游戏开发的常态。通过宏来切换不同的SDK初始化逻辑。public class SDKManager : MonoBehaviour { void Awake() { #if CHANNEL_XIAOMI InitializeXiaomiSDK(); #elif CHANNEL_HUAWEI InitializeHuaweiSDK(); #elif CHANNEL_GOOGLE_PLAY InitializeGooglePlaySDK(); #else InitializeDefaultSDK(); #endif } }在打包脚本中根据渠道参数动态设置对应的CHANNEL_*宏即可。场景三调试与开发工具开发阶段需要很多辅助工具但绝不能带到线上。public class DebugCheatMenu : MonoBehaviour { #if DEVELOPMENT_BUILD || UNITY_EDITOR void OnGUI() { if (GUI.Button(new Rect(10, 10, 150, 50), Add 1000 Gold)) { PlayerData.Gold 1000; } // ... 其他作弊按钮 ... } #endif }这里用了||逻辑表示在开发构建版或编辑器环境下都会显示这个作弊菜单。而正式的发布构建Development Build未勾选则不会包含此代码。4.3 架构层面的设计模式当自定义宏变得很多逻辑复杂时就需要更好的架构来管理避免#if指令散落在代码的各个角落。1. 接口与平台实现类模式这是最优雅的方式之一。定义一个公共接口然后为不同的宏条件提供不同的实现类。// 1. 定义接口 public interface INativeShareService { void ShareText(string text); void ShareImage(string imagePath); } // 2. 为不同平台/配置提供实现 #if UNITY_ANDROID CHANNEL_CN public class AndroidCNShareService : INativeShareService { // 调用国内安卓渠道的分享SDK public void ShareText(string text) { /* ... */ } public void ShareImage(string imagePath) { /* ... */ } } #elif UNITY_IOS public class iOSShareService : INativeShareService { // 调用iOS的UIActivityViewController public void ShareText(string text) { /* ... */ } public void ShareImage(string imagePath) { /* ... */ } } #else public class DefaultShareService : INativeShareService { // 一个简单的、跨平台的回退实现如复制到剪贴板 public void ShareText(string text) { GUIUtility.systemCopyBuffer text; } public void ShareImage(string imagePath) { Debug.Log(Image sharing not supported.); } } #endif // 3. 一个简单的工厂或依赖注入容器来创建实例 public class ServiceLocator { public static INativeShareService GetShareService() { // 这里可以根据宏或配置返回不同的实例 // 实际项目中可能用更复杂的IoC容器 #if UNITY_ANDROID CHANNEL_CN return new AndroidCNShareService(); #elif UNITY_IOS return new iOSShareService(); #else return new DefaultShareService(); #endif } }这样你的业务逻辑代码中完全看不到#if它只依赖于INativeShareService接口实现了良好的解耦。2. 部分类Partial Class配合宏对于平台差异较大的MonoBehaviour可以使用partial关键字将不同平台的实现拆分到不同文件。// PlatformInput.cs (通用部分) public partial class PlatformInput : MonoBehaviour { public partial float GetHorizontalAxis(); } // PlatformInput.Android.cs (Android实现) #if UNITY_ANDROID public partial class PlatformInput { public partial float GetHorizontalAxis() { // 安卓特有的输入逻辑 return Input.GetAxis(Horizontal) GetGyroTilt(); } } #endif // PlatformInput.PC.cs (PC实现) #if UNITY_STANDALONE public partial class PlatformInput { public partial float GetHorizontalAxis() { // PC标准的输入逻辑 return Input.GetAxis(Horizontal); } } #endif在构建时只有符合当前平台条件的文件会被编译进去。这保持了单个类的概念但物理上分离了代码非常清晰。5. 高级技巧与实战中的疑难杂症掌握了基础用法我们来看看一些能提升效率和稳定性的高级技巧以及那些容易让人栽跟头的坑。5.1 利用Conditional属性进行条件编译除了#ifC# 还提供了[Conditional]属性它更适合用于控制整个方法的调用是否被编译。public class DebugLogger { [System.Diagnostics.Conditional(DEVELOPMENT_BUILD)] public static void LogDetailed(object message) { // 这个方法体在 DEVELOPMENT_BUILD 未定义时不会被编译 // 更重要的是所有调用它的地方整行调用代码也会被移除 Debug.Log($[Detail] {message} at {Time.frameCount}); } [System.Diagnostics.Conditional(UNITY_EDITOR)] public static void LogEditorOnly(string message) { Debug.Log($[Editor] {message}); } } // 在业务代码中 public class SomeSystem : MonoBehaviour { void Update() { DebugLogger.LogDetailed($Update called. Pos: {transform.position}); // 这行代码在非开发版中会完全消失 // ... 其他逻辑 ... } }优势调用处的代码也会被移除不仅减小了代码体积还避免了因为方法参数计算可能带来的运行时开销哪怕方法不执行。劣势只能用于返回类型为void的方法。5.2 宏与预制体Prefab、ScriptableObject的兼容性问题这是一个巨坑条件编译是基于C#编译器的它只处理脚本代码。Unity的序列化系统负责保存Prefab、Scene、ScriptableObject中的字段引用和值完全不知道宏的存在。问题场景你有一个MonoBehaviour脚本MyBehaviour。在UNITY_EDITOR宏下你定义了一个公共字段public EditorOnlyData editorData;。你在编辑器中将这个脚本挂到GameObject上并设置了editorData的值然后做成了Prefab。当你为Android平台构建时UNITY_EDITOR宏未定义EditorOnlyData这个类甚至都不存在了。Unity在反序列化Prefab时找不到这个类型就会丢失对这个字段的引用通常表现为字段值变空或显示“(Missing)”。解决方案方案A使用#if UNITY_EDITOR包裹整个类定义。如果某个类或结构体只在编辑器下使用直接把它放在#if UNITY_EDITOR块里。这样在非编辑器构建时这个类型根本不存在Unity不会尝试去反序列化它。但要注意任何引用它的地方在其他平台也会出错。方案B将平台相关数据抽象化。不要直接序列化平台特定的类。可以序列化一个字符串ID或一个通用数据容器然后在运行时根据宏条件来解析它。方案C使用ScriptableObject作为数据容器并通过构建脚本动态切换。为不同平台创建不同的ScriptableObject资产在打包时通过脚本将正确的资产链接到Prefab中。这更复杂但更健壮。最实用的建议尽量避免在会被序列化public或[SerializeField]的字段中使用平台特定的类型。如果非用不可做好心理准备并确保在切换构建平台后重新检查并设置所有受影响的Prefab和场景。5.3 自动化构建与宏的动态管理在大型项目或团队中手动去Player Settings里修改宏定义容易出错。通常我们会借助自动化构建脚本。using UnityEditor; using System.Linq; public static class BuildScript { public static void BuildAndroidWithChannel(string channel) { // 1. 获取当前Android平台的宏定义 BuildTargetGroup buildTargetGroup BuildTargetGroup.Android; string originalDefines PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup); var defineList originalDefines.Split(;).ToList(); // 2. 移除可能存在的旧渠道宏 defineList.RemoveAll(d d.StartsWith(CHANNEL_)); // 3. 添加新渠道宏 defineList.Add($CHANNEL_{channel.ToUpper()}); // 4. 如果是开发构建加入调试宏 if (EditorUserBuildSettings.development) { if (!defineList.Contains(DEVELOPMENT_BUILD)) defineList.Add(DEVELOPMENT_BUILD); } else { defineList.Remove(DEVELOPMENT_BUILD); } // 5. 应用新的宏定义 string newDefines string.Join(;, defineList.Distinct()); PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, newDefines); Debug.Log($Set Android Defines to: {newDefines}); // 6. 执行构建... BuildPipeline.BuildPlayer(...); } }这个脚本可以在命令行调用集成到Jenkins、GitLab CI等工具中实现一键打出不同渠道的包。5.4 调试与日志输出策略在条件编译中调试信息也需要差异化处理。你肯定不希望调试日志在发布版本中输出也不希望安卓的日志逻辑跑到iOS上。public static class PlatformDebug { // 一个更完善的平台感知日志方法 public static void Log(object message, UnityEngine.Object context null) { #if DEVELOPMENT_BUILD || UNITY_EDITOR // 开发阶段输出完整堆栈和上下文 Debug.Log($[Frame:{Time.frameCount}] {message}, context); #elif UNITY_ANDROID // Android发布版可能使用更轻量或定向到Logcat的日志 // 例如使用AndroidJavaClass调用android.util.Log Debug.Log(message); // 简单处理实际可优化 #else // 其他平台发布版完全禁用日志以减少开销 // 什么都不做方法调用会被编译器优化掉吗不会但空方法开销极小。 // 如果追求极致可以配合[Conditional]属性但这里演示差异化逻辑。 #endif } // 专门用于性能关键代码的“仅编辑器”日志 [System.Diagnostics.Conditional(UNITY_EDITOR)] public static void LogEditorPerformance(string marker) { Debug.Log($Perf: {marker} - {Time.realtimeSinceStartup}); } }6. 实战构建一个跨平台工具类——DeviceInfo让我们综合运用所学创建一个实战例子一个DeviceInfo工具类它需要在不同平台上获取不同的设备信息。目标在安卓上获取设备型号和安卓版本。在iOS上获取设备型号和系统版本。在WebGL上获取浏览器User-Agent。在编辑器和其他平台上返回模拟信息。6.1 定义接口与数据结构首先我们定义统一的数据结构和接口这样业务代码就不需要关心平台细节。// DeviceInfoData.cs - 可序列化的数据容器避免平台特定类型 [System.Serializable] public class DeviceInfoData { public string Platform; public string DeviceModel; public string OperatingSystem; public string UniqueIdentifier; // 注意隐私政策 public string AdditionalInfo; // 用于存放平台特定信息如WebGL的UserAgent } // IDeviceInfoProvider.cs - 平台信息提供者接口 public interface IDeviceInfoProvider { DeviceInfoData GetDeviceInfo(); }6.2 实现各平台的具体提供者使用不同的C#文件来实现利用条件编译和部分类。DeviceInfoProvider.Android.cs#if UNITY_ANDROID using UnityEngine; public partial class DeviceInfoProvider : IDeviceInfoProvider { public DeviceInfoData GetDeviceInfo() { var data new DeviceInfoData { Platform Android, DeviceModel SystemInfo.deviceModel, OperatingSystem SystemInfo.operatingSystem, UniqueIdentifier SystemInfo.deviceUniqueIdentifier }; // 尝试获取更多安卓特有信息需动态权限 try { using (AndroidJavaClass version new AndroidJavaClass(android.os.Build$VERSION)) { data.AdditionalInfo $SDK Level: {version.GetStaticint(SDK_INT)}; } } catch (System.Exception e) { data.AdditionalInfo $Android API info unavailable: {e.Message}; } return data; } } #endifDeviceInfoProvider.iOS.cs#if UNITY_IOS using UnityEngine; public partial class DeviceInfoProvider : IDeviceInfoProvider { // 声明iOS原生代码函数 [System.Runtime.InteropServices.DllImport(__Internal)] private static extern string GetIOSDeviceModelIdentifier(); public DeviceInfoData GetDeviceInfo() { var data new DeviceInfoData { Platform iOS, DeviceModel SystemInfo.deviceModel, OperatingSystem SystemInfo.operatingSystem, UniqueIdentifier SystemInfo.deviceUniqueIdentifier }; // 调用原生代码获取更精确的型号标识符如 iPhone14,2 #if !UNITY_EDITOR try { data.AdditionalInfo $ModelID: {GetIOSDeviceModelIdentifier()}; } catch { data.AdditionalInfo ModelID fetch failed; } #else data.AdditionalInfo Running in Editor; #endif return data; } } #endif注意这里的GetIOSDeviceModelIdentifier需要在Xcode项目中有一个对应的C/Objective-C函数实现。DeviceInfoProvider.WebGL.cs#if UNITY_WEBGL using UnityEngine; using System.Runtime.InteropServices; public partial class DeviceInfoProvider : IDeviceInfoProvider { // 通过JS交互获取UserAgent [DllImport(__Internal)] private static extern string GetUserAgent(); public DeviceInfoData GetDeviceInfo() { var data new DeviceInfoData { Platform WebGL, DeviceModel SystemInfo.deviceModel, OperatingSystem SystemInfo.operatingSystem, UniqueIdentifier WebGL- SystemInfo.deviceUniqueIdentifier // 注意WebGL的deviceUniqueIdentifier可能不稳定 }; #if !UNITY_EDITOR try { data.AdditionalInfo $UA: {GetUserAgent()}; } catch { data.AdditionalInfo Failed to get UA; } #else data.AdditionalInfo WebGL in Editor; #endif return data; } } #endif同样GetUserAgent需要在你的HTML模板或JS插件中定义。DeviceInfoProvider.Default.cs// 回退实现用于编辑器或不支持的平台 public partial class DeviceInfoProvider : IDeviceInfoProvider { public DeviceInfoData GetDeviceInfo() { return new DeviceInfoData { Platform Application.platform.ToString(), DeviceModel SystemInfo.deviceModel, OperatingSystem SystemInfo.operatingSystem, UniqueIdentifier SystemInfo.deviceUniqueIdentifier, AdditionalInfo Default implementation }; } }6.3 创建工厂类与使用示例DeviceInfoFactory.cspublic static class DeviceInfoFactory { private static IDeviceInfoProvider _provider; private static readonly object _lock new object(); public static IDeviceInfoProvider GetProvider() { if (_provider null) { lock (_lock) { if (_provider null) { // 根据平台创建对应的提供者实例 // 这里直接new大型项目可用IoC容器 _provider new DeviceInfoProvider(); } } } return _provider; } public static DeviceInfoData GetCurrentDeviceInfo() { return GetProvider().GetDeviceInfo(); } }使用示例public class ExampleUsage : MonoBehaviour { void Start() { DeviceInfoData info DeviceInfoFactory.GetCurrentDeviceInfo(); Debug.Log($Platform: {info.Platform}); Debug.Log($Model: {info.DeviceModel}); Debug.Log($OS: {info.OperatingSystem}); Debug.Log($Additional: {info.AdditionalInfo}); // 业务逻辑可以根据info.Platform或具体字段进行差异化处理 if (info.Platform.Contains(Android) info.AdditionalInfo.Contains(SDK Level)) { // 针对特定安卓版本的处理 } } }6.4 项目文件组织建议为了清晰建议在项目中这样组织文件/Scripts ├── Runtime/ │ ├── DeviceInfo/ │ │ ├── Interfaces/ │ │ │ └── IDeviceInfoProvider.cs │ │ ├── Data/ │ │ │ └── DeviceInfoData.cs │ │ ├── Providers/ (或 Platforms/) │ │ │ ├── DeviceInfoProvider.Android.cs │ │ │ ├── DeviceInfoProvider.iOS.cs │ │ │ ├── DeviceInfoProvider.WebGL.cs │ │ │ └── DeviceInfoProvider.Default.cs │ │ └── Factory/ │ │ └── DeviceInfoFactory.cs │ └── OtherSystems... └── Editor/ (如果有编辑器工具)通过这样的架构平台相关的代码被物理隔离通过接口与工厂与主程序交互极大地提升了代码的可维护性和可扩展性。当需要新增一个平台如Switch时你只需要添加一个DeviceInfoProvider.Switch.cs文件并实现接口即可其他所有代码都无需改动。7. 常见问题排查与性能考量即使方案设计得再完美在实际开发和构建过程中还是会遇到各种问题。这里记录一些我踩过的坑和对应的排查思路。7.1 宏未生效编译符号排查清单当你写了#if MY_CUSTOM_MACRO但代码块依然灰显未激活或者应该被编译的代码却被跳过了请按以下顺序检查检查当前活动构建目标Unity编辑器右上角。你为Android平台定义的宏在iOS作为活动平台时是不生效的。代码高亮和编译都依赖于当前选择的平台。确认宏定义位置如果是全局宏去Player Settings里确认是否已添加并注意分号分隔。如果是文件内#define确认它写在文件最顶部using之前并且只在本文件有效。如果是通过脚本动态添加检查构建脚本的逻辑是否正确执行是否有权限修改PlayerSettings。检查宏名称拼写大小写敏感DEVELOPMENT_BUILD和Development_Build是两个不同的宏。清理并重新编译有时Unity的编译缓存会出问题。尝试Edit - Preferences - External Tools - Regenerate project files或者直接删除项目下的Library和obj文件夹风险较高需备份然后重新打开项目。查看编译日志在Console窗口点击右侧下拉菜单选择Open Editor Log。搜索你的宏名称有时会有相关提示。7.2 条件编译对运行时性能的影响这是一个常见的误解条件编译#if会影响游戏运行时的性能吗答案是几乎不会。因为#if/#endif是编译预处理指令。在代码被编译成DLL或执行文件之前预处理器就会根据宏定义像剪刀一样把不符合条件的代码完全剪掉。最终生成的程序集中根本不包含那些被排除的代码。因此运行时没有任何额外的if判断开销。与之对比的是运行时判断if (Application.platform ...)这个if判断是实实在在的代码每次执行都会进行条件评估虽然开销极小但在极端性能敏感的循环中累积起来也可能有影响。更重要的是它无法移除无用平台的代码导致包体膨胀。所以对于确定性的、基于平台的代码选择应优先使用编译时宏。7.3 代码冗余与维护难题过度或不当使用宏会导致代码难以阅读和维护。反面教材void Awake() { #if UNITY_ANDROID InitAndroid(); #elif UNITY_IOS InitIOS(); #elif UNITY_WEBGL InitWebGL(); #endif #if DEVELOPMENT_BUILD EnableDebugCanvas(); #endif #if CHANNEL_HUAWEI InitHuaweiSDK(); #elif CHANNEL_XIAOMI InitXiaomiSDK(); #endif }各种宏交织在一起逻辑分散难以理解。优化建议分层抽象如前文所述使用接口和工厂模式将平台差异抽象到独立的类中。配置文件驱动对于一些简单的开关可以考虑使用ScriptableObject或JSON配置文件在运行时读取而不是用宏写死。这样不需要重新编译就能切换配置。宏的范围最小化尽量将#if限制在最小的代码块内或者集中在几个专门的“平台适配层”文件中避免业务核心代码中遍布宏指令。7.4 第三方插件与宏的冲突当你导入Asset Store的插件时它们可能自带了自己的宏定义。有时这些宏会和你项目中的宏发生命名冲突或者插件内部的宏逻辑不符合你的项目需求。应对策略检查插件文档好的插件会说明它定义了哪些宏。查看插件脚本可以搜索插件目录下的.cs文件看是否有#define或#if使用了特定符号。隔离与包装如果插件代码因为宏问题无法直接使用可以考虑为它写一个包装层Wrapper。在你的包装层代码里用正确的宏条件去调用插件的API将差异消化在包装层内部。谨慎修改插件直接修改第三方插件代码不是好主意因为更新插件时会覆盖你的修改。如果必须改考虑使用partial class扩展或者将修改后的脚本移到插件目录之外。掌握Unity的条件编译尤其是结合自定义宏来管理代码差异化是迈向高级Unity开发者的必经之路。它不仅仅是语法技巧更是一种架构思维。开始可能会觉得繁琐但一旦建立起规范你会发现它在管理复杂多变的项目需求时带来的整洁和高效是无可替代的。最关键的是多写、多重构、多思考如何将平台相关的“脏活”封装起来让主逻辑保持干净这才是用好这门技术的精髓。