
1. 项目概述与核心价值在Unity3D项目开发中尤其是涉及到持续迭代更新的游戏或应用版本管理是一个看似微小却至关重要的环节。你有没有遇到过这样的场景测试同事反馈了一个Bug你修复后打了个新包发过去对方却问“这是哪个版本和我刚才测的是同一个包吗”。或者线上版本出现了一个问题你需要快速确认用户当前运行的究竟是哪个具体的构建版本以便在代码仓库中精准定位对应的提交记录。如果只是在打包时手动修改文件名不仅效率低下而且极易出错。这个功能的核心价值就是将项目的构建版本号动态、自动地显示在游戏运行时界面中。它解决的远不止是“显示一串文字”这么简单而是打通了“开发构建”与“运行时状态”之间的信息壁垒。对于开发团队内部它能避免版本混淆提升测试和沟通效率对于线上产品它是问题排查和版本追踪的关键依据。实现这个功能我们通常会利用Unity提供的PlayerSettings.bundleVersion或从Application类中获取版本信息并结合UI系统进行展示。下面我将从一个资深开发者的角度拆解几种主流、稳定且可扩展的实现方案并分享在实际项目中容易踩到的“坑”和最佳实践。2. 版本号信息源与获取方式解析在动手写代码之前我们必须清楚Unity为我们提供了哪些版本信息来源以及它们各自的含义和适用场景。选对信息源是功能稳定可靠的前提。2.1 Application.version最直接的运行时版本号Application.version是最常用、最直接的属性它返回的值就是在Player Settings中设置的Version字段。原理与操作路径在Unity Editor中你可以通过菜单栏Edit - Project Settings - Player打开Player设置面板。在Other Settings部分找到Version输入框。你在这里填写的字符串就是打包后Application.version所读取的内容。这个值会直接写入到构建出的应用程序的配置信息中例如在Windows上是.exe文件的属性详情在Android上是AndroidManifest.xml里的versionName。重要注意事项直接使用Application.version虽然方便但它有一个关键限制它在Editor模式下运行游戏时获取到的是Project Settings里配置的版本号而不是最后一次打包的版本号。这意味着如果你在Editor里按Play按钮测试显示的版本号可能并不是你最终发布包的那个版本。对于需要严格区分“开发中版本”和“已发布版本”的场景这一点需要特别留意。2.2 使用自定义脚本动态生成版本号为了获得更精确、包含更多构建信息的版本号例如集成Git提交哈希、构建时间等我们通常会编写一个构建后处理脚本在打包过程中动态生成并写入版本信息。核心思路利用Unity的IPostprocessBuildWithReport接口在构建完成后自动生成一个包含版本信息的数据文件如JSON、Text或ScriptableObject并将其包含在构建资源中。游戏运行时再读取这个文件来显示版本号。一个基础的构建后处理脚本示例using UnityEngine; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using System.IO; using System; public class BuildVersionWriter : IPostprocessBuildWithReport { public int callbackOrder 0; public void OnPostprocessBuild(BuildReport report) { // 定义版本信息 var versionInfo new VersionInfo { // 使用Player Settings中的版本作为基础 baseVersion Application.version, // 获取当前时间作为构建时间 buildTime DateTime.UtcNow.ToString(yyyy-MM-dd HH:mm:ss UTC), // 可以在这里调用命令获取Git提交哈希需要系统支持 // gitCommitHash GetGitCommitHash(), // 构建类型如Development, Release buildType EditorUserBuildSettings.development ? Development : Release, // Unity版本 unityVersion Application.unityVersion }; // 将版本信息序列化为JSON string json JsonUtility.ToJson(versionInfo, true); // 确定输出路径放在Resources文件夹下以便运行时使用Resources.Load加载 string resourcesPath Path.Combine(Application.dataPath, Resources); if (!Directory.Exists(resourcesPath)) { Directory.CreateDirectory(resourcesPath); } string filePath Path.Combine(resourcesPath, BuildVersion.json); File.WriteAllText(filePath, json); AssetDatabase.Refresh(); // 刷新AssetDatabase让Unity识别新文件 Debug.Log($构建版本信息已写入: {filePath}); } // 示例方法获取Git提交哈希需确保系统环境支持git命令 private string GetGitCommitHash() { // 这是一个简化示例实际使用中需要考虑跨平台和错误处理 try { var process new System.Diagnostics.Process(); process.StartInfo.FileName git; process.StartInfo.Arguments rev-parse --short HEAD; process.StartInfo.UseShellExecute false; process.StartInfo.RedirectStandardOutput true; process.StartInfo.CreateNoWindow true; process.Start(); string output process.StandardOutput.ReadToEnd(); process.WaitForExit(); return output.Trim(); } catch (Exception) { return Unknown; } } } // 用于存储版本信息的可序列化类 [System.Serializable] public class VersionInfo { public string baseVersion; public string buildTime; public string gitCommitHash; public string buildType; public string unityVersion; }实操要点脚本放置这个脚本需要放在Editor文件夹下例如Assets/Editor/BuildVersionWriter.cs。只有放在Editor文件夹中的脚本才会在Unity编辑器中编译执行而不会被打包到运行时。接口说明IPostprocessBuildWithReport是Unity提供的构建后处理接口callbackOrder属性决定了执行顺序数字越小越先执行OnPostprocessBuild方法会在每次构建成功完成后被调用。文件路径我们将生成的BuildVersion.json文件放在Assets/Resources目录下。这是因为Resources文件夹内的资源会被Unity特殊处理打包时会包含进去并且可以通过Resources.Load接口在运行时无需路径直接加载非常方便。安全与兼容性获取Git信息的代码 (GetGitCommitHash) 在实际项目中需要更健壮的错误处理并且要考虑到团队中并非所有成员的开发环境都配置了Git命令行工具。一种更稳妥的做法是将其设为可选功能或者通过CI/CD流水线在打包时传入这些信息。2.3 读取自定义版本信息文件构建时生成了信息文件运行时就需要读取它。我们创建一个通用的版本号管理器。using UnityEngine; using System.IO; public class VersionManager : MonoBehaviour { // 单例模式方便全局访问 private static VersionManager _instance; public static VersionManager Instance { get { if (_instance null) { GameObject go new GameObject(VersionManager); _instance go.AddComponentVersionManager(); DontDestroyOnLoad(go); // 跨场景不销毁 _instance.Initialize(); } return _instance; } } // 公开的属性供其他脚本访问 public string FullVersionString { get; private set; } public string BuildTime { get; private set; } public string CommitHash { get; private set; } private void Initialize() { LoadVersionInfo(); } private void LoadVersionInfo() { // 首先尝试加载自定义的构建信息文件 TextAsset versionFile Resources.LoadTextAsset(BuildVersion); if (versionFile ! null) { VersionInfo info JsonUtility.FromJsonVersionInfo(versionFile.text); if (info ! null) { // 组合完整的版本字符串格式可按需定制 FullVersionString ${info.baseVersion} ({info.buildType}); if (!string.IsNullOrEmpty(info.gitCommitHash) info.gitCommitHash ! Unknown) { FullVersionString $ - Git: {info.gitCommitHash}; } BuildTime info.buildTime; CommitHash info.gitCommitHash; Debug.Log($已加载构建版本信息: {FullVersionString}); return; } } // 如果自定义文件不存在或加载失败则回退到Application.version Debug.LogWarning(未找到自定义构建版本文件将使用Application.version。); FullVersionString $Ver: {Application.version} (Editor/Unknown Build); BuildTime N/A; CommitHash N/A; } // 提供一个快速获取显示用字符串的方法 public string GetDisplayString() { return $Version: {FullVersionString}\nBuild: {BuildTime}; } }这个管理器做了几件事首先它尝试从Resources/BuildVersion.json加载我们打包时生成的信息。如果成功就解析并使用这些丰富的信息组合成完整的版本字符串。如果加载失败比如在Editor中直接运行则优雅地降级到使用Application.version并标记为“Editor/Unknown Build”这样在开发阶段也能清晰地区分。3. 在游戏UI中动态显示版本号获取到版本信息后下一步就是将其展示给玩家或测试人员。这里我们结合Unity的UGUI系统实现一个灵活、可配置的版本号显示组件。3.1 创建UI显示组件我们创建一个VersionDisplay组件它可以挂载在任何包含Text或TextMeshPro - Text组件的UI对象上。using UnityEngine; using UnityEngine.UI; // 如果是UGUI Text // 如果使用TextMeshPro需要引入对应的命名空间 // using TMPro; public class VersionDisplay : MonoBehaviour { public enum DisplayMode { FullInfo, // 显示完整信息版本号构建时间 VersionOnly, // 只显示版本号 CustomFormat // 自定义格式 } [Header(显示设置)] public DisplayMode displayMode DisplayMode.FullInfo; [Header(自定义格式)] [Tooltip(使用 {0} 代表版本号{1} 代表构建时间{2} 代表提交哈希)] [SerializeField] private string customFormat v{0} | {1}; [Header(UI组件引用)] [SerializeField] private Text versionText; // UGUI Text引用 // [SerializeField] private TMP_Text versionTextTMP; // TextMeshPro引用二选一 private void Start() { // 自动获取Text组件如果未手动赋值 if (versionText null) { versionText GetComponentText(); } // 对于TextMeshPro取消下方注释 // if (versionTextTMP null) // { // versionTextTMP GetComponentTMP_Text(); // } UpdateVersionDisplay(); } public void UpdateVersionDisplay() { string displayString ; switch (displayMode) { case DisplayMode.FullInfo: displayString VersionManager.Instance.GetDisplayString(); break; case DisplayMode.VersionOnly: displayString $Version: {VersionManager.Instance.FullVersionString}; break; case DisplayMode.CustomFormat: displayString string.Format(customFormat, VersionManager.Instance.FullVersionString, VersionManager.Instance.BuildTime, VersionManager.Instance.CommitHash); break; } // 更新UI文本 if (versionText ! null) { versionText.text displayString; } // 如果使用TextMeshPro // if (versionTextTMP ! null) // { // versionTextTMP.text displayString; // } } // 提供一个编辑器按钮方便预览 #if UNITY_EDITOR [ContextMenu(预览版本显示)] private void PreviewInEditor() { // 在编辑器中模拟初始化VersionManager并更新显示 if (VersionManager.Instance ! null) { UpdateVersionDisplay(); UnityEditor.EditorUtility.SetDirty(this); // 标记对象已修改确保更改被保存 } } #endif }组件使用步骤在UI Canvas下创建一个Text对象例如在屏幕右上角创建一个空GameObject添加Text组件。将VersionDisplay脚本挂载到这个GameObject上。在Inspector窗口中将自身的Text组件拖拽到VersionDisplay脚本的Version Text字段上。选择你想要的Display Mode。如果选择CustomFormat可以在输入框中按提示定义格式。设计考量为什么提供多种显示模式因为在不同场景下需求不同。在游戏的“设置”或“关于”页面你可能希望显示完整的构建信息而在主界面角落的常驻水印可能只需要简洁的版本号。自定义格式则提供了最大的灵活性比如你可以格式化为v1.2.3.4567 | 2023-10-27这样的样式。3.2 实现屏幕角落的常驻水印对于需要始终显示的版本水印我们还需要考虑UI适配和干扰问题。public class VersionWatermark : MonoBehaviour { public Text versionText; // 关联的Text组件 public Vector2 screenMargin new Vector2(10, 10); // 距离屏幕边缘的像素偏移 public TextAnchor alignment TextAnchor.UpperRight; // 默认在右上角 private RectTransform rectTransform; private void Start() { rectTransform versionText.GetComponentRectTransform(); UpdateWatermark(); } private void UpdateWatermark() { if (versionText null) return; // 设置文本内容 versionText.text $v{VersionManager.Instance.FullVersionString}; // 根据对齐方式设置锚点和位置 rectTransform.pivot GetPivotFromAnchor(alignment); rectTransform.anchorMin rectTransform.anchorMax GetAnchorFromAnchor(alignment); // 根据锚点和边距计算位置 Vector2 anchoredPosition Vector2.zero; switch (alignment) { case TextAnchor.UpperLeft: anchoredPosition new Vector2(screenMargin.x, -screenMargin.y); break; case TextAnchor.UpperRight: anchoredPosition new Vector2(-screenMargin.x, -screenMargin.y); break; case TextAnchor.LowerLeft: anchoredPosition new Vector2(screenMargin.x, screenMargin.y); break; case TextAnchor.LowerRight: anchoredPosition new Vector2(-screenMargin.x, screenMargin.y); break; // 可以扩展其他对齐方式... } rectTransform.anchoredPosition anchoredPosition; // 可选降低水印的视觉干扰 Color textColor versionText.color; textColor.a 0.7f; // 设置一定的透明度 versionText.color textColor; versionText.fontStyle FontStyle.Italic; // 使用斜体区分 } // 工具方法将TextAnchor转换为对应的锚点和中心点 private Vector2 GetAnchorFromAnchor(TextAnchor anchor) { switch (anchor) { case TextAnchor.UpperLeft: return new Vector2(0, 1); case TextAnchor.UpperRight: return new Vector2(1, 1); case TextAnchor.LowerLeft: return new Vector2(0, 0); case TextAnchor.LowerRight: return new Vector2(1, 0); default: return new Vector2(1, 1); } } private Vector2 GetPivotFromAnchor(TextAnchor anchor) { // Pivot通常与Anchor对应使文本向期望的方向扩展 return GetAnchorFromAnchor(anchor); } }这个水印脚本自动将文本定位到屏幕角落并设置了半透明和斜体使其既能提供信息又不会过度干扰游戏主视觉。screenMargin参数让你可以微调水印与屏幕边缘的距离。4. 高级应用与自动化集成基础功能实现后我们可以进一步探索如何将其融入现代游戏开发工作流实现真正的自动化。4.1 与CI/CD流水线集成以Jenkins为例在团队协作和自动化构建环境中版本号通常由CI/CD工具生成和管理。下面以Jenkins为例展示如何将构建信息从Jenkins传递到Unity构建过程中。思路Jenkins在构建时可以生成包含版本号、构建编号、Git分支、提交ID等信息的文件如JSON或者直接设置为环境变量。然后我们通过Unity命令行构建参数将这些信息传递给Unity并在构建后处理脚本中读取和使用。步骤一在Jenkins中定义构建参数和环境变量在Jenkins的构建配置中你可以使用插件如Environment Injector Plugin或直接通过Pipeline脚本定义变量pipeline { agent any environment { // 从Git获取短提交哈希 GIT_COMMIT_SHORT sh(script: git rev-parse --short HEAD, returnStdout: true).trim() // 使用Jenkins的构建编号作为内部版本号 BUILD_NUMBER ${env.BUILD_NUMBER} // 组合成完整的版本字符串例如 1.0.0.125 BUNDLE_VERSION 1.0.0.${BUILD_NUMBER} // 构建时间 BUILD_TIMESTAMP sh(script: date -u %Y-%m-%dT%H:%M:%SZ, returnStdout: true).trim() } stages { stage(Build Unity) { steps { // 将环境变量作为参数传递给Unity命令行 bat C:\\Program Files\\Unity\\Hub\\Editor\\2022.3.XXf1\\Editor\\Unity.exe ^ -projectPath . ^ -quit -batchmode -nographics ^ -executeMethod BuildScript.PerformBuild ^ -buildVersion ${BUNDLE_VERSION} ^ -buildNumber ${BUILD_NUMBER} ^ -gitCommit ${GIT_COMMIT_SHORT} ^ -buildTime ${BUILD_TIMESTAMP} } } } }步骤二修改Unity构建脚本以接收参数我们需要修改之前的构建脚本使其能够从命令行参数读取信息。// BuildScript.cs - 放置于Assets/Editor目录下 using UnityEditor; using System; using System.Linq; public static class BuildScript { public static void PerformBuild() { // 从命令行参数获取版本信息 string buildVersion GetArgument(-buildVersion); string buildNumber GetArgument(-buildNumber); string gitCommit GetArgument(-gitCommit); string buildTime GetArgument(-buildTime); // 如果传入了自定义版本则覆盖Player Settings if (!string.IsNullOrEmpty(buildVersion)) { PlayerSettings.bundleVersion buildVersion; PlayerSettings.Android.bundleVersionCode int.Parse(buildNumber); // Android版本码 // iOS的build number设置类似略 } // 准备构建选项 BuildPlayerOptions buildOptions new BuildPlayerOptions(); buildOptions.scenes EditorBuildSettings.scenes.Where(s s.enabled).Select(s s.path).ToArray(); buildOptions.locationPathName Builds/MyGame.exe; // 输出路径 buildOptions.target BuildTarget.StandaloneWindows64; buildOptions.options BuildOptions.None; // 执行构建 BuildPipeline.BuildPlayer(buildOptions); // 构建完成后可以调用之前的BuildVersionWriter来生成包含详细信息的文件 // 这里需要将获取到的gitCommit, buildTime等信息传递给VersionInfo GenerateBuildInfoFile(buildVersion, buildNumber, gitCommit, buildTime); } private static string GetArgument(string name) { string[] args Environment.GetCommandLineArgs(); for (int i 0; i args.Length; i) { if (args[i] name i 1 args.Length) { return args[i 1]; } } return null; } private static void GenerateBuildInfoFile(string version, string buildNum, string commit, string time) { // 创建VersionInfo对象并写入Resources目录的逻辑与之前类似 // 但此时使用从命令行传入的参数 VersionInfo info new VersionInfo { baseVersion version, buildNumber buildNum, gitCommitHash commit, buildTime time, buildType EditorUserBuildSettings.development ? Development : Release, unityVersion Application.unityVersion }; // ... 写入文件的代码 ... } }通过这种方式版本号的生成和注入完全由自动化流水线控制确保了每次构建版本信息的唯一性和可追溯性。4.2 扩展在日志、错误报告和Analytics中集成版本号版本号不应该只显示在UI上将其集成到系统的其他部分能极大提升运维和调试效率。集成到日志系统 修改你的日志工具类在每一条日志的开头或结尾自动附加当前版本号。public static class GameLogger { public static void Log(string message, LogType type LogType.Log) { string versionPrefix $[{VersionManager.Instance.FullVersionString}] ; string formattedMessage versionPrefix message; switch (type) { case LogType.Log: Debug.Log(formattedMessage); break; case LogType.Warning: Debug.LogWarning(formattedMessage); break; case LogType.Error: Debug.LogError(formattedMessage); break; } // 同时可以写入本地文件或发送到远程服务器 WriteToFile(formattedMessage); } }集成到错误上报 当游戏发生未处理的异常时在将错误信息发送到后端服务器如Sentry, Bugsnag时务必包含版本号。void OnEnable() { Application.logMessageReceived HandleLog; } void HandleLog(string logString, string stackTrace, LogType type) { if (type LogType.Exception || type LogType.Error) { CrashReport report new CrashReport { version VersionManager.Instance.FullVersionString, buildTime VersionManager.Instance.BuildTime, logMessage logString, stackTrace stackTrace, deviceInfo SystemInfo.deviceModel, // ... 其他信息 }; SendReportToServer(report); } }集成到Analytics事件 向游戏数据分析平台如Unity Analytics, Firebase发送自定义事件时将版本号作为一个固定参数。public void SendAnalyticsEvent(string eventName, Dictionarystring, object parameters null) { if (parameters null) parameters new Dictionarystring, object(); // 确保每个事件都带有版本信息 parameters[game_version] VersionManager.Instance.FullVersionString; parameters[build_type] VersionManager.Instance.BuildType; // 需要在VersionInfo中增加此字段 // 调用Analytics SDK // Analytics.CustomEvent(eventName, parameters); }这样做的好处是当你在分析后台查看数据或错误报告时可以立即根据版本号进行筛选快速定位某个特定版本的问题分析不同版本的用户行为差异。5. 常见问题、调试技巧与最佳实践即使功能看似简单在实际开发和团队协作中也会遇到各种问题。这里总结了一些典型场景和解决方案。5.1 版本号不更新或显示错误这是最常见的问题通常由以下几个原因导致Player Settings中的Version未修改Application.version直接读取此值。确保在打包前检查并更新它。构建后处理脚本未执行或执行失败检查脚本是否放在Assets/Editor目录下并且实现了正确的接口如IPostprocessBuildWithReport。查看Unity构建日志确认是否有相关错误。Resources.Load 路径或文件名错误确保构建脚本生成的文件路径是Assets/Resources/BuildVersion.json并且运行时加载的路径是Resources.LoadTextAsset(BuildVersion)注意没有后缀名。版本管理器单例未初始化确保在显示版本号的UI脚本Start()或Awake()方法被调用之前VersionManager.Instance已经被访问过以触发初始化。一个稳妥的做法是在游戏启动的第一个场景中就通过一个启动脚本访问一次VersionManager.Instance。调试技巧在VersionManager的LoadVersionInfo方法中加入详细的Debug.Log打印出它尝试加载的路径、加载结果和最终决定的版本字符串。在Editor中播放和打真机包后分别测试对比日志输出。5.2 多平台构建的兼容性处理不同平台对文件系统的访问权限和路径有差异我们的方案需要具备跨平台兼容性。文件路径构建脚本中使用Application.dataPath组合路径是安全的它在Editor中指向Assets文件夹在构建后指向只读的数据目录。而使用Resources.Load是Unity跨平台资源加载的标准方式无需担心路径问题。Android/iOS平台注意事项在这些移动平台上Resources文件夹内的资源在构建后会被压缩并打包到特定格式中如.assets文件。我们的JSON文件作为TextAsset被打包进去Resources.Load可以正确读取这没有问题。但要绝对避免在运行时尝试用System.IO.File去读写Application.dataPath下的原始文件这在移动平台上是不可行的。平台特定版本号除了通用的bundleVersion各平台还有自己的版本号设置Android:PlayerSettings.Android.bundleVersionCode(整数每次提交市场必须递增)。iOS:PlayerSettings.iOS.buildNumber(字符串)。 我们的构建脚本在接收命令行参数时可以同时更新这些平台特定的版本号确保应用商店后台的版本信息与游戏内显示的一致。5.3 版本号命名规范建议一个清晰、自动化的版本号命名规则能省去很多沟通成本。推荐使用语义化版本控制Semantic Versioning的变体并结合构建号。格式主版本号.次版本号.修订号.构建号(例如1.2.3.4567)主版本号重大更新通常包含不兼容的API变更。次版本号功能性更新向下兼容。修订号问题修复和小幅优化向下兼容。构建号由CI/CD系统自动生成的单调递增数字如Jenkins的BUILD_NUMBER用于唯一标识每次构建。在CI/CD中实现在Jenkins等工具中主版本号.次版本号.修订号可以作为一个配置项或从某个配置文件如project.version中读取然后与BUILD_NUMBER拼接成完整版本。游戏内显示在UI上你可能只显示主版本号.次版本号.修订号而在“关于”页面或日志中显示完整的带构建号的版本。VersionManager可以轻松提供这两种格式。5.4 性能与内存考量对于这样一个基础功能性能开销通常可以忽略不计但好的实践仍值得遵循懒加载与缓存我们的VersionManager在首次访问时加载并解析JSON文件然后将结果缓存到属性中。之后多次访问FullVersionString等属性都是直接读取内存没有重复的IO或解析操作效率很高。避免每帧更新版本号在运行时是固定的所以显示它的UI组件只在Start()时更新一次即可千万不要放在Update()里。Resources文件夹的使用我们将版本文件放在Resources文件夹内这会使它在打包时被包含进一个统一的资源包。对于这种极小的文本文件来说完全可以接受。如果你的项目严格规定不使用Resources系统出于资源管理策略可以考虑使用Addressables或AssetBundle但复杂度会大大增加对于版本文件这种必备资源来说有些“杀鸡用牛刀”。5.5 在团队中的协作规范为了让这个功能在团队中顺畅运行需要建立简单的规范脚本和预制体归档将VersionManager、VersionDisplay、BuildVersionWriter等脚本以及设置好的版本显示UI预制体放在项目版本控制中一个公认的目录下如Assets/_Core/Versioning/。构建流程文档化在团队的Wiki或README中说明版本号的生成规则、如何通过命令行参数传递版本信息以及开发者在本地测试时需要注意什么例如理解Editor模式与真机模式的版本号差异。UI预设提供几个常用的版本显示UI预制体比如“右上角半透明水印”、“关于页面完整信息框”、“调试菜单中的版本行”方便团队成员直接拖拽使用保持UI风格统一。经过以上从原理到实践从基础到进阶的详细拆解这个“显示版本号”的小功能已经演变成一个健壮、可扩展、与自动化流程深度集成的开发基础设施。它不再是一个简单的文本显示而是连接开发、构建、测试、运维各个环节的信息枢纽。实现它可能只需要一两个小时但它为项目带来的长期秩序和效率提升会远超你的投入。