每个人都有自己的知识体系。
Toggle navigation
Home
随笔
C#/.Net
树莓派 / Raspberry
皓月汉化组
Beego
Golang
OxideMod
apache
haproxy
windows
Java
Objective-C
日语/罗马音歌词/日语常识
MongoDB
python
电学
公告
Minecraft服务器-公告
NanoPi
C4D (CINEMA 4D)
生活
推流/m3u8/rtmp/rtsp
Unity3d
ffmpeg
数据结构
区块链
tarui
UnityForPSVita
About Me
Archives
Tags
Unity下Switch开发文件存储踩坑,以及解决方案范例
2025-04-08 01:01:05
17
0
0
akiragatsu
*本文仅作技术研究,因为合规原因,本文不提供任何法律敏感和版权相关的诸如SDK的文件。 ## 意义? 本文主要探讨Unity下的Nintendo Switch平台 存档或存储相关, 有朋友就问了,Unity开发的文件存储,不就是Syatem.IO 下直接操作么?并非如此,他就是不一样,可以说是**传统方式完全不可用** ~ **我先介绍,再告诉你代码怎么写,安心先看** 我们先来看看在Switch平台下的,情况: Application.persistentDataPath 不可用 调用就崩溃,也不用在任何地方动态或静态初始化 Application.DataPath 可用 输出 rom:/Data Application.streamingAssetsPath 可用 输出 rom:/Data/StreamingAssets Application.temporaryCachePath 可用 输出 host:/Temp 像Application.DataPath,Application.streamingAssetsPath这种只读的路径,也不能通过System.IO去访问,调用就会卡死掉。 而要存储,如Application.persistentDataPath 在Switch下是直接不可用的,类似我PSVita文章讲到的,但比PSVita更严格,甚至不能取值。 那么,比如我们要开发模拟器亦或者需要可读写的目录怎么办呢? 他这里有一个 挂载点的概念,列个表吧。 | 挂载点 | 描述 | 用途 | 是否可读写 | 大小限制 | 注意事项 | |-----------------|-------------------------------------------|-----------------------------------|--------------------|------------------------------------|--------------------------------------------------------------------------| | `rom:/` | 只读文件系统,存储游戏资源 | 加载静态资源(如模型、纹理等) | 只读 | 受NSP包大小限制(通常几GB) | - 构建时打包<br>- 需优化资源以减少加载时间<br>- 不可动态修改 | | `host:/` | 开发环境中的主机文件系统 | 调试时访问开发机文件 | 可读写(开发时) | 受开发机存储限制 | - 仅限开发套件<br>- 零售版不可用<br>- 需USB或网络连接开发机 | | `save:/` | 保存数据挂载点,存储存档 | 保存游戏进度、玩家设置等 | 可读写 | 几MB(由游戏注册信息决定) | - 通过SDK API访问<br>- 数据隔离且可能加密<br>- 避免频繁小文件操作 | | `sd:/` | SD卡文件系统(若插入SD卡) | 存储DLC或调试数据 | 可读写(需权限) | 受SD卡容量限制(最大2TB) | - 默认不可直接访问<br>- 需特殊权限和API<br>- SD卡未插入时需处理异常 | | `content:/` | 游戏内容挂载点,通常与NSP相关 | 访问已安装的游戏内容 | 只读 | 受NSP或存储设备限制 | - 仅系统或特定API可用<br>- 不直接暴露给开发者<br>- 用于内容分发 | | `user:/` | 用户数据挂载点,存储用户相关数据 | 保存用户特定的临时或缓存数据 | 可读写 | 几MB至几十MB(视系统分配) | - 按用户账号隔离<br>- 通过SDK管理<br>- 注意多用户场景下的数据兼容性 | | `system:/` | 系统分区,存储OS相关文件 | 系统固件或预装内容访问 | 只读(系统级) | 内置存储容量(约32GB) | - 开发者无法访问<br>- 系统专用<br>- 尝试访问会导致权限错误 | | `bis:/` | 内置存储(Business Internal Storage) | 系统和用户数据的底层存储 | 只读(系统级) | 内置存储容量(约32GB) | - 底层分区<br>- 普通开发者无权限<br>- 系统级操作专用 | | `safe:/` | 安全模式下的文件系统 | 调试或恢复模式下的数据访问 | 可读写(特殊模式)| 极小(临时分配) | - 仅限安全模式或维修<br>- 普通游戏无法使用<br>- 调试专用 | | `tmp:/` | 临时文件系统 | 存储运行时的临时文件 | 可读写 | 极小(几MB以内) | - 数据不持久,重启后清空<br>- 适合临时缓存<br>- 避免存储关键数据 | | `cache:/` | 缓存文件系统 | 存储缓存数据(如网络下载内容) | 可读写 | 几十MB(视系统分配) | - 不保证持久性<br>- 通过SDK管理<br>- 适合非关键数据缓存 | 我们发现可以适合拿来做存储的,只有save:/ 或者 sd:/,而且save:/ 还有不少限制。其他的要么只读,要么只适合开发机,到玩家手上的零售机,并不可使用。 此外,save节点,还需要你自己手动挂载,比如save:/ ## 前置条件: 1. 环境相关 若不了解开发环境相关,请先了解,这是我提供的NSP打包库:http://git.axibug.com/sin365/AxibugNSPTools/ 2. NintendoSDKPlugin 官方给Switch提供给Unity的SDK调用封装的插件,请导入到您的项目中。 ## 存档模式 存档模式,即主机掌机常见的游戏存档的方式。 存档模式,需要在ProjectSettings中或者NMeta中将存档大小设置一下,默认为0,否则无法保存文件(实测,如果不设置,你只能存几十字节的东西,超过就崩溃,一定要设置) 而设置呢,**要设置 SaveDataSize和SaveDataJournalSize,后者一定要不小于前者,且都是16KiB的倍数,不是16KiB的倍数,你会打包失败** 我projectsetting中设置了一个4M+的范例,直接用文本给你们看对应设置吧,按需修改。 switchUserAccountSaveDataSize: 49152 switchUserAccountSaveDataJournalSize: 49152 switchApplicationAttribute: 0 switchCardSpecSize: 4 switchCardSpecClock: 25 在NintendoSDK中对应的c++库,是诸如nn::fs等库文件,NintendoSDKPlugin中有直接的C#调用封装。 好,咱们一一展开: 1.首先存档是基于Switch用户的,就是你在Switch游戏机上创建的用户。通过这个挂载存储节点,那么在此之前,你的Unity项目,就需要配置 PlayerSetting > Publishing Settings >Startup User Acount 设置为 Required 确保在启动游戏时就选择了账户,这样无论如何都会有用户。 然后再代码中初始化: //必须先初始化NS的Account 不然调用即崩 nn.account.Account.Initialize(); 然后可以通过一系列方式获取User nn.Result result; mUserHandle = new nn.account.UserHandle(); if (!nn.account.Account.TryOpenPreselectedUser(ref mUserHandle)) { UnityEngine.Debug.LogError("打开预选的用户失败."); return; } UnityEngine.Debug.Log("打开预选用户成功."); result = nn.account.Account.GetUserId(ref m_UserId, mUserHandle); result.abortUnlessSuccess(); if (m_UserId == Uid.Invalid) { UnityEngine.Debug.LogError("无法获取用户 ID"); return; } UnityEngine.Debug.Log($"获取用户 ID:{m_UserId.ToString()}"); result = nn.account.Account.GetNickname(ref m_NickName, m_UserId); result.abortUnlessSuccess(); if (m_UserId == Uid.Invalid) { UnityEngine.Debug.LogError("无法获取用户 ID"); return; } UnityEngine.Debug.Log($"获取用户 NickName ID:{m_NickName.ToString()}"); 2.挂载 save的挂载,是调用nn.fs.SaveData.Mount去挂载存档节点 使用刚才获取到的Uid,作为挂载的参数之一去挂载, 第二个参数,则是你给挂载其一个别名 注意:不要重复挂载同一个节点,记得判断一下, 重复调用会导致崩溃 范例: public bool MountSave(Uid userId, string mountName = "save") { //.... 你的一些逻辑,比如判断是否重复挂载,避免崩溃 nn.Result result; result = nn.fs.SaveData.Mount(mountName, userId); result.abortUnlessSuccess(); if (!result.IsSuccess()) { UnityEngine.Debug.LogError($"MountSave->挂载{mountName}:/ 失败: " + result.ToString()); return false; } UnityEngine.Debug.Log($"MountSave->挂载{mountName}:/ 成功 "); return true; } 关于卸载,所有节点的卸载入口都统一是:nn.fs.FileSystem.Unmount(string name) 如果你有必要卸载,请调用 nn.fs.FileSystem.Unmount("save"); //比如你挂载时起名叫save 3.读写 没错,既然不能用System.IO去访问,那只能用nn.fs下的东西, 当你移植Switch上,首先必须要做的事情,就是读写改成如下方式(最好封装一下) 值得一提的是,写入之后,得调用Commit提交给Switch存储。否则不生效 请一定挂载成功之后,在读写,否则也是崩溃。 如下代码是写入: nn.Result result = File.Open(ref fileHandle, filePath, OpenFileMode.Write); //result.abortUnlessSuccess();//这一句非必要一定不要调用,即Result if (!result.IsSuccess()) { UnityEngine.Debug.LogError($"失败 File.Open(ref filehandle, {filePath}, OpenFileMode.Write): " + result.GetErrorInfo()); return false; } UnityEngine.Debug.Log($"成功 File.Open(ref filehandle, {filePath}, OpenFileMode.Write)"); //nn.fs.WriteOption.Flush 应该就是覆盖写入 result = nn.fs.File.Write(fileHandle, 0, data, data.Length, nn.fs.WriteOption.Flush); // Writes and flushes the write at the same time //result.abortUnlessSuccess(); if (!result.IsSuccess()) { UnityEngine.Debug.LogError("写入文件失败: " + result.GetErrorInfo()); return false; } UnityEngine.Debug.Log("写入文件成功: " + filePath); nn.fs.File.Close(fileHandle); //必须得提交,否则没有真实写入 result = FileSystem.Commit(save_name); //result.abortUnlessSuccess(); if (!result.IsSuccess()) { UnityEngine.Debug.LogError($"FileSystem.Commit({save_name}) 失败: " + result.GetErrorInfo()); return false; } 然后读取: nn.Result result; result = nn.fs.File.Open(ref fileHandle, filename, nn.fs.OpenFileMode.Read); if (result.IsSuccess() == false) { UnityEngine.Debug.LogError($"nn.fs.File.Open 失败 {filename} : result=>{result.GetErrorInfo()}"); return false; } UnityEngine.Debug.Log($"nn.fs.File.Open 成功 {filename}"); long iFileSize = 0; result = nn.fs.File.GetSize(ref iFileSize, fileHandle); if (result.IsSuccess() == false) { UnityEngine.Debug.LogError($"nn.fs.File.GetSize 失败 {filename} : result=>{result.GetErrorInfo()}"); return false; } UnityEngine.Debug.Log($"nn.fs.File.GetSize 成功 {filename},size=>{iFileSize}"); byte[] loadedData = new byte[iFileSize]; result = nn.fs.File.Read(fileHandle, 0, loadedData, iFileSize); if (result.IsSuccess() == false) { UnityEngine.Debug.LogError($"nn.fs.File.Read 失败 {filename} : result=>{result.GetErrorInfo()}"); return false; } UnityEngine.Debug.Log($"nn.fs.File.Read 成功 {filename}"); nn.fs.File.Close(fileHandle); 以上是存储的范例,更完整的代码,在我开源仓库中。 ## SDCard模式 这个要求更高,如果你游戏的存档不大,请直接使用存档模式,即save:/ 如果你是程序的资源包,建议StreamingAssets,走Bundle流程,挂载节点是content:/ 如果真的有大量网络下载需求,缓存需求,或者比如我这边正在开发的模拟器会自动下载Rom,则使用存档模式,就不够了。那就大概率需要使用SDCard模式 在授权开发Switch正式流程中,往往是没有权限的,需要向老任单独申请。否则无效。 而Unity的C# SDK中,并没有明文提供sd卡挂载的dllimport,只提供了MountSDcardForDebug,这个对于我们来说,意义不大,本身是给开发机才有的Debug SDCard做Dump或者调试用的。零售机根本用不了。 那么为了挂载SD卡,Unity Switch中官方范例中没有,怎么办呢。我看了一下SDK c++库中的头文件,是由非debug的SD卡挂载的,nn::fs::MountSdCard,没错 就是这个。 咱们自己手动引入DllImport就好了:(我的AxiNSApi中已经包含) #if UNITY_SWITCH using nn.account; #endif public class AxiNSSDCard { #if UNITY_SWITCH #if DEVELOPMENT_BUILD || NN_FS_SD_CARD_FOR_DEBUG_ENABLE [DllImport(Nn.DllName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "nn_fs_MountSdCard")] public static extern nn.Result Mount(string name); #else public static nn.Result Mount(string name) { return new nn.Result(); } #endif #endif } 调用AxiNSSDCard.Mount(指定一个挂载名)即可挂载。 实测,捣鼓机某大气层是可以挂在成功的。 关于SDCard权限,在NAC中,这个我会尽快研究,更新到本文中,待续……
Pre:
Unity Switch 开发/打包/移植教程 - 皓月
Next:
NS错误码:SwitchErrCode
0
likes
17
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Submit
Sign in
to leave a comment.
No Leanote account?
Sign up now.
0
comments
More...
Table of content
No Leanote account? Sign up now.