Unity3D Android构建后如何从文件夹动态加载图片与音频
兄弟,我之前做类似的严肃游戏项目时也踩过这个坑!编辑器里能直接读项目文件夹的资源,但打包成Android APK后,资源的存储路径和访问逻辑完全变了——你说的WWW失效大概率是路径没写对,或者没处理Android的异步加载要求。下面给你几个针对性的方案,按你的需求选:
1. 直接用StreamingAssets打包资源(适合不需要用户修改资源的场景)
StreamingAssets里的内容会原封不动打包进APK,但Android上不能用File类直接读取,必须用UnityWebRequest(WWW已经被Unity弃用了,别用了)异步加载。
第一步:整理你的资源结构
把所有需要动态加载的文件都放到Assets/StreamingAssets目录下,比如:
StreamingAssets/ ├─ 你的场景文件夹/ │ ├─ letters.txt │ ├─ 配置.csv │ ├─ Sprites/ │ │ ├─ Tile_A.png │ │ └─ Tile_B.png │ └─ Audios/ │ ├─ Sound_A.mp3 │ └─ Sound_B.mp3
注意:Sprite图片要设置成Texture Type为Sprite (2D and UI),不然加载后转不成Sprite。
第二步:读取txt/CSV文本文件
用UnityWebRequest异步读取,兼容编辑器和Android:
IEnumerator LoadTextFile(string relativePath, Action<string> onLoaded) { string fullPath; // 区分Android和编辑器的路径 #if UNITY_ANDROID && !UNITY_EDITOR fullPath = $"jar:file://{Application.dataPath}!/assets/{relativePath}"; #else fullPath = Path.Combine(Application.streamingAssetsPath, relativePath); #endif using (UnityWebRequest www = UnityWebRequest.Get(fullPath)) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.LogError($"加载文件失败:{www.error}"); onLoaded?.Invoke(null); } else { // 返回读取到的文本内容 onLoaded?.Invoke(www.downloadHandler.text); } } }
调用示例:StartCoroutine(LoadTextFile("你的场景文件夹/letters.txt", (content) => { /* 这里写解析逻辑 */ }));
第三步:加载Sprite资源
Android上不能直接读StreamingAssets里的Sprite,得先加载Texture2D再转成Sprite:
IEnumerator LoadSprite(string spriteRelativePath, Action<Sprite> onLoaded) { string fullPath; #if UNITY_ANDROID && !UNITY_EDITOR fullPath = $"jar:file://{Application.dataPath}!/assets/{spriteRelativePath}"; #else fullPath = Path.Combine(Application.streamingAssetsPath, spriteRelativePath); #endif using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(fullPath)) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.LogError($"加载Sprite失败:{www.error}"); onLoaded?.Invoke(null); } else { Texture2D texture = DownloadHandlerTexture.GetContent(www); Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f)); onLoaded?.Invoke(sprite); } } }
调用后把Sprite赋值给预制体的SpriteRenderer就行。
第四步:加载音频资源
用UnityWebRequestMultimedia加载AudioClip:
IEnumerator LoadAudio(string audioRelativePath, Action<AudioClip> onLoaded) { string fullPath; #if UNITY_ANDROID && !UNITY_EDITOR fullPath = $"jar:file://{Application.dataPath}!/assets/{audioRelativePath}"; #else fullPath = Path.Combine(Application.streamingAssetsPath, audioRelativePath); #endif using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip(fullPath, AudioType.MPEG)) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Debug.LogError($"加载音频失败:{www.error}"); onLoaded?.Invoke(null); } else { AudioClip clip = DownloadHandlerAudioClip.GetContent(www); onLoaded?.Invoke(clip); } } }
2. 支持用户修改资源:用PersistentDataPath
如果需要让不懂Unity的人直接复制文件夹修改内容,StreamingAssets就不行了(因为它打包在APK里,用户没法改)。这时候可以把初始资源放到StreamingAssets,第一次启动游戏时复制到Application.persistentDataPath——这个目录是Android的外部存储,用户可以访问和修改。
核心思路:
- 第一次启动时,把StreamingAssets里的场景资源全部复制到PersistentDataPath
- 之后所有资源读取都从PersistentDataPath走,用
File类直接读写(因为这个目录是可读写的)
复制资源的示例代码
IEnumerator CopyInitialResources() { string sourceRoot = Path.Combine(Application.streamingAssetsPath, "你的场景文件夹"); string targetRoot = Path.Combine(Application.persistentDataPath, "你的场景文件夹"); // 先检查目标目录是否存在,不存在就创建 if (!Directory.Exists(targetRoot)) { Directory.CreateDirectory(targetRoot); } // 注意:Android上没法直接获取StreamingAssets的文件夹列表,所以你需要提前在StreamingAssets里放一个file_list.txt,记录所有需要复制的文件相对路径 yield return LoadTextFile("file_list.txt", (content) => { string[] allFiles = content.Split(new[] {'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries); foreach (string filePath in allFiles) { StartCoroutine(CopySingleFile(filePath)); } }); } IEnumerator CopySingleFile(string relativePath) { string sourcePath; #if UNITY_ANDROID && !UNITY_EDITOR sourcePath = $"jar:file://{Application.dataPath}!/assets/{relativePath}"; #else sourcePath = Path.Combine(Application.streamingAssetsPath, relativePath); #endif string targetPath = Path.Combine(Application.persistentDataPath, relativePath); using (UnityWebRequest www = UnityWebRequest.Get(sourcePath)) { yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.Success) { // 写入到PersistentDataPath File.WriteAllBytes(targetPath, www.downloadHandler.data); } } }
之后读取资源时,直接用Path.Combine(Application.persistentDataPath, "你的资源路径")就行,和编辑器里的逻辑一致。
3. 进阶方案:Addressables(推荐复杂场景)
如果你的游戏资源量大,或者需要热更新,Unity的Addressables系统是最优解。它会自动处理平台差异,编辑器和移动端的加载逻辑完全一致,还支持远程资源更新。
快速上手:
- 在Package Manager里安装Addressables包
- 右键你要动态加载的资源(Sprite、音频、文本),选择
Addressable -> Add to Addressables - 用代码加载资源(支持异步/await):
async void LoadTileSprite(string spriteAddress) { var loadHandle = Addressables.LoadAssetAsync<Sprite>(spriteAddress); await loadHandle.Task; if (loadHandle.Status == AsyncOperationStatus.Succeeded) { Sprite targetSprite = loadHandle.Result; // 赋值给预制体的SpriteRenderer } // 记得释放资源,避免内存泄漏 Addressables.Release(loadHandle); }
Addressables的好处是你不用关心平台路径,直接用资源的Addressable名称加载就行,后续要更新资源也不用重新打包APK。
避坑提醒
- Android必须异步加载:同步读取StreamingAssets会直接失败,一定要用Coroutine或者async/await
- 路径区分大小写:Android是Linux内核,路径大小写不匹配会加载失败,比如
Tile_a.png和Tile_A.png是两个不同的文件 - 权限问题:如果用PersistentDataPath,Android 10及以上需要申请读写权限,Unity在打包时会自动处理大部分,但最好在代码里加权限申请逻辑
- 音频格式:尽量用MP3或WAV,避免用OGG(部分低端设备可能不支持)
内容的提问来源于stack exchange,提问作者Nin4ikP

