背景
因公司项目需要,开始接触Unity相关开发,在此记录自己开发过程中学习和实践笔记。一方面督促自己弄明白技术细节,记录自己成长;一方面感觉国内Unity开发资料很少,且Unity官方文档多有错误,希望可以帮助到大家。
1、Unity使用PlayerPrefs
我不太清楚Unity客户端程序员叫它什么,我一般称它为玩家偏好设置。
1.1 使用
1.1.1 存储数据:
1.1.2 获取数据:
1.1.3 删除数据:
详情见:[2020.3 - Unity Documentation](该文档描述跟实际测试不符)(docs.unity3d.com/2020.3/Docu…)
1.2 注意事项:
-
操作系统不同,PlayerPrefs存储路径也不同。
-
Editor直接运行:存储路径为 ~/Library/Preferences/unity.CompanyName.ProductName.plist;
在 UnityEditor -> File -> Build Setting -> Player 路径下可设置CompanyName和ProductName:
-
Mac平台:直接Build出的未签名包,存储路径为~/Library/Preferences/BundleId.plist;签名后从Test Flight下载的App,存储路径为~/Library/Containers/ProductName/Data/Library/Preferences/BundleId.plist。
- 注意~/Library/Container/ProductName/Data/Library/Preferences/BundleId.plist 是在Mac 电脑上显示的文件夹结构路径,当我们将~/Library/Container/ProductName路径下的文件夹拖到终端,显示路径为~/Library/Container/BundleId。
-
-
在Windows和Mac平台PlayerPrefs数据独立于应用存储,且不会进行任何加密;
-
当应用被覆盖安装、重新加载、甚至卸载重装,使用PlayerPrefs存储的数据都不会丢失。
-
PlayerPrefs只能存储String、Float、Int三种数据类型。
1.3 总结
虽然PlayerPrefs 存储数据的方法足够简单,但仍旧不推荐大家在实际开发中使用。原因如下:
1、存储的数据类型的局限性,不够灵活,很难满足大型数据和对象数据的存储。
2、用户可以直接操作Windows和Mac平台上PlayerPrefs对应的文件路径:
- 如果使用PlayerPrefs存储用户敏感数据和应用程序敏感数据,很容易泄漏,也有可能被操作系统意外删除,更有可能被玩家修改,利用游戏漏洞获益。
- 由于Mac OS的特殊性,PlayerPrefs存储路径在不同开发模式下并不相同,这给开发自测以及QA带来很大理解困扰和工作量的增加。(一开始我们Mac App中使用PlayerPrefs保存用户的行为,比如是否同意用户写协议等。但QA每次进行相关测试时,都必须将PlayerPrefs对应的plist删除,才能重新测试,非常麻烦也容易出现沟通上的问题。)
1.4 补充
1、一般情况下移动端是可以使用PlayerPrefs持久化非敏感数据,因为普通用户在移动设备上根本打不开对应的文件。以下是Android和iOS存储PlayerPrefs的路径:
- **Android:** /data/data/pkg-name/shared_prefs/pkg-name.v2.playerprefs.xml
- **iOS:** /Library/Preferences/BundleIdentifier.plist
2、我在想Unity API大都是跨平台的,虽然这带来极大的好处,但也导致调用具体API在不同平台上的实际效果有些差异,我们作为开发者在使用过程中还是要仔细调研和甄别,避免被官方文档误导。
2 Unity使用 Application.persistentDataPath()
2.1 使用
无论是在移动端还是在Window和Mac平台,我们应用创建的用户操作日志文件都存储在Application.persistentDataPath路径下。
// Path.Combine(),将多个字符串拼接成一个路径。主要应用于第一个参数为绝对路径,第二个参数为相对路径
// Path.Combine()自动处理路径分隔符,这样可以确保生成的路径是与当前操作系统兼容的。
// 这里如果使用"/logs"拼接,生成的路径为 “/logs”
// 官方示例推荐写法:
string[] paths = {Application.persistentDataPath, "logs"};
var ntConfigLogRootFolder = Path.Combine(paths);
2.2 注意事项(以下均为在Mac平台下的路径)
1、使用Unity Editor直接运行后的路径为:/Users/用户名/Library/Application Support/CompanyName/ProductName/自定义日志文件夹
2、在Mac直接Build 后的路径为:/Users/用户名/Library/Application Support/BundleId/自定义日志文件夹
- 并且当以上 1和2 两个路径同时存在时,写入的日志文件会一起进行变化。
3、从Mac App Store 下载后的路径为:/Users/用户名/Library/Containers/ProductName/Data/Library/Application Support/BundleId/自定义日志文件夹
2.3 总结
在实际开发中,建议将当前平台和具体路径信息进行打印,因为Unity官方文档并不正确。
3 使用VuplexWebViewMac插件无法上传到App Store
Mac Unity项目使用 3D WebView - Vuplex 是无法上传到App Store的。首先在上传过程中会报错:
Asset validation failed (90284)
Invalid Code Signing. The executable 'XXX.pkg/Payload/XXX.app/Contents/PlugIns/VuplexWebViewMac.bundle/Contents/Frameworks/Vuplex WebView.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libswiftshader_libGLESv2.dylib' must be signed with the certificate that is contained in the provisioning profile.
Asset validation failed (90284)
Invalid Code Signing. The executable 'XXX.pkg/Payload/XXX.app/Contents/PlugIns/VuplexWebViewMac.bundle/Contents/Frameworks/Vuplex WebView.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib' must be signed with the certificate that is contained in the provisioning profile.
Asset validation failed (90284)
Invalid Code Signing. The executable 'XXX.pkg/Payload/XXX.app/Contents/PlugIns/VuplexWebViewMac.bundle/Contents/Frameworks/Vuplex WebView.app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib' must be signed with the certificate that is contained in the provisioning profile.
我们在遇到这个报错后,立即对VuplexWebViewMac.bundle 进行了不包含entitlements文件的签名,但仍旧无法在mac上使用该浏览器。最终在 Vuplex的官方文档中看到这样一篇文章:macOS: Can't submit to the Mac App Store / How to notarize for distribution?。这篇文章说明了3D WebView不支持在Mac App Store构建的原因:3D WebView中嵌入了Chromium。Chromium 是一个由Google发起的开源的Web浏览器项目,由于Chromium需要访问底层系统功能来实现高性能的浏览器引擎和功能,而这些功能违反了Apple 沙盒环境限制。所以一般使用Chromium的 MacApp,都会选用除Mac App Store外的其他方式分发。
4 使用VuplexWebView 无法输入中文
4.1 原因
1)Vuplex支持3D WebView 和 2D WebViw。
2)Vuplex的3D WebView 和 2D WebViw 均不支持输入非拉丁字母字符,比如中文、韩文、日本等;
3)Vuplex不支持中文输入的原因是由于Unity的imeCompositionMode的限制,详见Unity官方文档 docs.unity3d.com/ScriptRefer…。
- IME 是输入法编辑器(Input Method Editor)的缩写。它是一种软件工具,用于在计算机上输入非拉丁字符集(如中文、日文、韩文等)或其他特殊字符,以及进行符号输入和表情符号等。在 Unity 中,
imeCompositionMode属性主要用于控制当用户正在输入文本时,是否显示输入法编辑器的组合窗口。在 Windows 操作系统中,输入法编辑器的组合窗口通常用于显示用户正在输入的非拉丁字符(如中文、日文、韩文等)的候选词或部分组合。当用户使用输入法编辑器输入文本时,通常会先输入一部分内容,然后输入法编辑器会根据这部分内容提供一组可能的候选词或部分组合,用户可以从中选择合适的字符或词语来完成输入。
4.2 解决
在初始化Vuplex 的 CanvasWebViewPrefab.cs 文件中,找到_initCanvasPrefab函数,添加代码:Input.imeCompositionMode = IMECompositionMode.On;
4.3 总结
上面提供的解决方法只针对Windows,不适用MacOS。
官方教程:developer.vuplex.com/webview/ove…
Vuplex WebViw 的键盘如何工作:support.vuplex.com/articles/ke…;
5 Mac App Store 沙盒环境下, 使用Process. Start()无法打开外部浏览器
5.1 原因
1、我们将 Unity Editor -> Build Setting -> Player Setting -> Player -> Other S etting -> Configuration 路径下的 “Scipting Backend ” 设置为 “ IL2CPP”。
- 该设置项,在Unity Editor直接运行出来的包,是可以使用
Process. Start()打开外部浏览器。 - 但使用Unity Editor 直接Build出来的应用和从Mac App Store 下载的应用都无法使用Process. Start()打开外部浏览器,并且报错:can not open web browser: mono-io-layer-error (0) 。
当我们将" Scipting Backend" 为 "Mono"时,以下三种情况使用Process. Start() 都可以打开外部浏览器:
- Unity Editor直接运行出来的包
- Unity Editor 直接Build出来的应用
- 从Mac App Store 下载的应用
2、在编写Unity应用时,我们通常会使用C#语言。Unity游戏引擎最初是基于Mono运行时来解释和执行C#代码的。但随着游戏复杂度的提升,性能要求的提高,就引入了IL2CPP(Intermediate Language to C++)技术。该技术能将C#代码转换为C++代码,实现Unity引擎的高性能以及跨平台的兼容性。
- 我请教了我们游戏客户端,游戏客户端告诉我IL2CPP技术主要为了兼容iOS平台,Apple自2022年6月6日就不再提供32位的App支持。
- M系列芯片的Mac采用ARM64架构,Inter芯片的Mac采用X86_64架构。
3、Unity技术人员在官方论坛中回复: Process类没有在IL2CPP中实现,也就是说Process. Start()转成的C++代码不可用。
5.2 解决
使用Application.OpenURL()跳转至默认浏览器的网页即可。
6 Path.GetDirectoryName() 和 Path.Combine()
1、Path.GetDirectoryName() :返回一个路径字符串,路径不包含当前文件层级。例如,将路径“C:\Directory\SubDirectory\test.txt”传递到GetDirectoryName将返回“C:\Directory\SubDirectory”。将该路径“C:\Directory\SubDirectory”传递到GetDirectoryName将返回“C:\Directory”。
2、Path.Combine():将多个字符串数组组合成路径。
// Path.Combine(),将多个字符串拼接成一个路径。主要应用于第一个参数为绝对路径,第二个参数为相对路径
// Path.Combine()自动处理路径分隔符,这样可以确保生成的路径是与当前操作系统兼容的。
// 这里如果使用"/logs"拼接,生成的路径为 “/logs”
// 官方示例推荐写法
string[] paths = {Application.persistentDataPath, "logs"};
var ntConfigLogRootFolder = Path.Combine(paths);
7 上传日志文件时报错System.IO.IOException: Sharing violation on path
7.1 原因
我们调用接口上传日志文件的过程中,使用如下代码:
MultipartFormDataContent content = new MultipartFormDataContent();
...
var stream = new FileStream(value, FileMode.Open, FileAccess.Read);
byte[] bytes = new byte[stream.Length];
// 读取文件内容并放入字节数组
stream.Read(bytes, 0, bytes.Length);
// 不关闭会报错
stream.Close();
content.Add(new ByteArrayContent(bytes), keyValuePair.Key, Path.GetFileName(value));
从代码看本身是没有问题的,因为我们在读取操作后面,立即调用了Close()关闭了文件流。但我们上传的游戏文件会读取所有日志文件,其中包括游戏客户端在程序启动时打开,在程序退出时才会关闭的文件流。这就导致上传过程中总会访问到游戏客户端未关闭的文件流,所以产生该问题。
7.2 解决
使用try...catch...语法,捕捉并忽略IOException错误,避免该问题导致之前写入的文件上传不成功。
try
{
// public FileStream(string path, FileMode mode, FileAccess access);
// FileStream:用于读取和写入文件的类
// path: 可以是绝对路径也可以是相对路径
// FileMode:指定如何打开或创建文件,FileMode.Open:打开现有文件,
// FileAccess:FileStream访问对象的方式,FileAccess.Read:只用于读文件
var stream = new FileStream(value, FileMode.Open, FileAccess.Read);
byte[] bytes = new byte[stream.Length];
// 读取文件内容并放入字节数组
stream.Read(bytes, 0, bytes.Length);
// 不关闭会报错
stream.Close();
content.Add(new ByteArrayContent(bytes), keyValuePair.Key, Path.GetFileName(value));
}
catch (IOException e)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, $"upload file IO Exception: {e.Message}");
}
catch (Exception e)
{
NtLog.Log(NtLog.NtLogLevel.Error, Tag, $"upload file Other Exception: {e.Message}");
}
8 System.Globalization.CultureInfo.InstalledUICulture.Name Or Application.systemLanguage
8.1 区别和选择
在 Mac 和 Windows平台上分别可以使用这两个接口来获取“用户设置的操作系统语言”,区别在于:Application.systemLanguage返回的是”系统语言“;而System.Globalization.CultureInfo.InstalledUICulture.Name返回的“系统语言和国家/地区”。
1)Application.systemLanguage返回的是一个字符串枚举值。
- 当前设备设置的语言为简体中文,无论设置什么地区,
Application.systemLanguage返回的都是**SystemLanguage.ChineseSimplified**;
2)System.Globalization.CultureInfo.InstalledUICulture.Name返回的是一个字符串。
- 当前设备设置的语言为简体中文,地区设置为香港,
System.Globalization.CultureInfo.InstalledUICulture.Name返回"zh-HK","zh" 表示中文,"HK" 表示香港。 - 当前设备设置的语言为简体中文,地区设置为中国大陆,
System.Globalization.CultureInfo.InstalledUICulture.Name返回"zh-CN","zh" 代表中文,"CN" 代表中国大陆。
由此,我们可以更清楚的知道两者的区别。根据应用类型和业务需求选择API即可,我在开发中使用的是 Application.systemLanguage。
8.2 mac OS上设置语言与地区的路径
【系统设置】-【通用】-【语言与地区】,见下图
8.3 测试表现
【信息】mac OS 14.3.1 , Unity Editor 2022.3.28f1
【测试内容】修改系统语言后,根据Application.systemLanguage返回的系统语言来设置Unity App内的语言。检查Application.systemLanguage和Unity App显示语言是否一致。
1)直接使用Unity Editor运行测试时,必须重启电脑,设置的系统语言才会在App内生效。另:
- 简体中文和繁体中文都必须切换到对应的地区才能正确生效。也就是简体中文必须对应地区为中国大陆,繁体中文必须对应地区为香港或台湾这样使用繁体文字作为主要语言的地区。
- 切换其他语言,则忽略地区设置。
2)使用Unity Editor 导出的项目和下载的项目 ,无需重启电脑即生效。
- 体中文和繁体中文都必须切换到对应的地区才能正确生效。也就是简体中文必须对应地区为中国大陆,繁体中文必须对应地区为香港或台湾这样使用繁体文字作为主要语言的地区。
- 切换其他语言,则忽略地区设置。
9 error CS0103: The name 'XRSettings' does not exist in the current context
升级Unity Editor 2022.3.28f1后,Vuplex库BaseWebViewPrefab.cs文件报错,报错内容为 “Vuplex/WebView/Core/Scripts/BaseWebViewPrefab.cs(644,73): error CS0103: The name 'XRSettings' does not exist in the current context”
9.1 排查原因
1、这个错误的意思是:“当前上下文中不存在名称‘XRSettings’”表示在当前脚本中未能识别XRSettings类或命名空间。这通常发生在必要的XR插件未安装、未使用正确的命名空间,或者Unity版本不支持该API的情况下。
2、XRSettings是UnityEngin.XR中的类,而UnityEngin.XR属于UnityEngin.VRModule。VR这个词大家都不陌生,很容易就知道UnityEngin.VRModule是Unity中对虚拟现实的支持。
3、Vuplex库在BaseWebViewPrefab.cs和CanvasWebViewPrefab.cs中使用了 XRSettings类,我们项目本身并未使用VR相关的功能。
9.2 解决方案:
方案一:安装XR Plugin Management
安装位置:
PS:我们项目本身并未使用VR相关的功能,所以没有选择改方案。
方案二:修改Vuplex库
// CanvasWebViewPrefab.cs中:
#if ENABLE_XR
if (XRSettings.enabled) {
if (logWarnings) {
_logNative2DModeWarning("CanvasWebViewPrefab.Native2DModeEnabled is enabled but XR is enabled, so Native 2D Mode will not be enabled.");
}
return false;
}
#endif
// BaseWebViewPrefab.cs中:
bool isXREnabled = false;
#if ENABLE_XR
isXREnabled = XRSettings.enabled;
#endif
if (webViewWithCursorType != null && CursorIconsEnabled && !isXREnabled) {
webViewWithCursorType.CursorTypeChanged += (sender, eventArgs) => {
Internal.CursorHelper.SetCursorIcon(eventArgs.Value);
_hasOverriddenCursorIcon = eventArgs.Value != "default";
};
}