unity遊戲框架學習-AssetBundle

AssetBundle官網連接:https://docs.unity3d.com/Manual/AssetBundles-Workflow.htmlhtml

1、爲何要使用AssetBundleandroid

AssetBundle是Unity推薦的資源管理方式,熱更新必須使用此方式。ios

2、AssetBundle是什麼?api

First is the actual file on disk. This we call the AssetBundle archive, or just archive for short in this document. 
The archive can be thought of as a container, like a folder, that holds additional files inside of it.
These additional files consist of two types; the serialized file and resource files. The serialized file contains your assets broken out into their individual objects and written out to this single file.
The resource files are just chunks of binary data stored separately for certain assets (textures and audio) to allow us to load them from disk on another thread efficiently. Second is the actual AssetBundle object you interact with via code to load assets from a specific archive.
This object contains a map of all the file paths of the assets you added to this archive to the objects that belong to that asset that need to be loaded when you ask for it.

1.如上,第一部分是咱們在unity裏看到的,生成出來的ab包文件,你能夠理解成一種特殊的文件夾。咱們將chariot打成ab包」animation_2d_chariot.unity3d「,而後用UnityStudio解壓」animation_2d_chariot.unity3d「後的結構大概是下面這樣子的緩存

他所包含的資源列表以下:網絡

你也能夠經過.mainfest文件查看app

ManifestFileVersion: 0
CRC: 2785811640
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: 05555bf8d49a3c8fc690e4913454de28
  TypeTreeHash:
    serializedVersion: 2
    Hash: 0317c6c2e1c00c8e914e7d09d8b3e9b0
HashAppended: 0
ClassTypes:
- Class: 1
  Script: {instanceID: 0}
- Class: 4
  Script: {instanceID: 0}
- Class: 21
  Script: {instanceID: 0}
- Class: 28
  Script: {instanceID: 0}
- Class: 48
  Script: {instanceID: 0}
- Class: 114
  Script: {fileID: 11500000, guid: 24fd26203f8ea48f1b25f24fc3663d1c, type: 3}
- Class: 114
  Script: {fileID: 11500000, guid: c93168c4c5e9f49bfa80fc75bd465a40, type: 3}
- Class: 114
  Script: {fileID: 11500000, guid: a6791178c999f426a8618ef42eac4275, type: 3}
- Class: 115
  Script: {instanceID: 0}
- Class: 212
  Script: {instanceID: 0}
- Class: 213
  Script: {instanceID: 0}
Assets:
- Assets/Data/animation/2d/chariot/chariot_5.prefab
- Assets/Data/animation/2d/chariot/chariot_6.prefab
Dependencies:
- F:/ALClient/Assets/Temp/data/shader.unity3d

2.第二部分是腳本中使用的,例如經過unity api AssetBundle.LoadFromFile能夠從指定路徑加載一個AssetBundle 對象,這個要加載的對象就是上面咱們說的unity裏面的Asset(你能夠理解成AssetBundle是一種特殊的資源,如prefabs),經過這個腳本的AssetBundle,咱們能夠加載出unity的AssetBundle所包含的文件less

3、如何生成AssetBundle文件?異步

生成AssetBundle文件分兩步,第一步標記你要生成AssetBundle的文件,你能夠在unity面板直接指定AB包名字,以下ide

To assign a given Asset to an AssetBundle, follow these steps:

Select the asset you want to assign to a bundle from your Project View
Examine the object in the inspector
At the bottom of the inspector
, you should see a section to assign AssetBundles and Variants:
The left-hand drop down assigns the AssetBundle while the right-hand drop down assigns the variant
Click the left-hand drop down where it says 「None」 to reveal the currently registered AssetBundle names
Click 「New…」 to create a new AssetBundle
Type in the desired AssetBundle name. Note that AssetBundle names do support a type of folder structure depending on what you type. To add sub folders, separate folder names by a 「/」. For example: AssetBundle name 「environment/forest」 will create a bundle named forest under an environment sub folder
Once you’ve selected or created an AssetBundle name, you can repeat this process for the right hand drop down to assign or create a Variant name, if you desire. Variant names are not required to build the AssetBundles

1.選中該Project中要導出ab包的文件。2在Inspect底部設置AssetBundle

可是通常不推薦這種手擼的方法(大項目動輒幾萬個文件。。。),通常使用腳本動態進行設置,以下,主要就是針對你要導出的文件、文件夾調用AssetImporter.SetAssetBundleNameAndVariant方法動態設置ab包,你能夠進一步封裝,例如文件夾xxx目錄下的全部文件都設置成單獨的ab包或者只有子目錄設置ab包等等,這種方法會比手擼效率高不少,只須要設置一次須要導出abbb包的文件,而後在每次打包都調用指定的方法進行ab包設置就行了

// 設置單個文件(或目錄)的ABName
    private static void ImportSingleFile(string Path, string abName)
    {
        AssetImporter importer = AssetImporter.GetAtPath(Path);

        if (importer == null)
        {
            Debugger.LogError("[路徑錯誤] path:{0}", Path);
            return;
        }
        abName = abName.Replace ('\\', '_').Replace ('/','_');
        importer.SetAssetBundleNameAndVariant(abName, BaseDef.AB_SUFFIX);
    }

第二步,調用unity的生成ab包的接口(依賴第一步設置完的ab包名字和屬性)

BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

outputPath:輸出路徑
assetBundleOptions:壓縮模式等,unity提供三種壓縮模式,官方說明:https://docs.unity3d.com/Manual/AssetBundles-Building.html

BuildAssetBundleOptions.None:LZMA格式壓縮,使用的時候要整包解壓到內存,最大的壓縮比(意味着壓縮後的文件最小),但第一次的加載須要更長的時間,unity會將解壓後的LZMA從新壓縮成LZ4格式並保存在硬盤裏,意味着第二次加載將會擁有和LZ4壓縮相同的加速度,unity推薦在下載時使用(如熱更、高清資源),這樣能夠節省用戶流量,加快下載速度

BuildAssetBundleOptions.UncompressedAssetBundle:不壓縮,最大的文件,最快的加載速度
BuildAssetBundleOptions.ChunkBasedCompression:LZ4壓縮,使用的時候不須要整包解壓,即只解壓當前須要的塊,這是unity推薦的壓縮方式(母包)

Using ChunkBasedCompression has comparable loading times to uncompressed bundles with the added benefit of reduced size on disk.

若是以爲lz4的壓縮格式致使包體過大,能夠將一部分ab包在壓縮成lzma(將要壓縮的ab包放在文件夾Temp,再將Temp壓縮成LZMA格式,只在用戶初次進入遊戲時整個解壓就行了),至關於壓縮了兩次

targetPlatform:目標平臺android/ios等

4、AssetBundle如何分組?

AssetBundle 數量太少:
  會增長運行時內存使用,由於可能加載了當前功能不須要使用的資源
  會增長加載時間,雖然lz4壓縮格式不須要整包解壓,但仍是會把文件頭加載進來的
  須要下載大量數據,包體太大,致使細分度不夠,可能其中一個對象更新了會致使其餘對象也更新,對熱更不友好。

有太多的 AssetBundle:
  會增長構建的時間
  會加大開發的複雜性
  會增長總的加載時間:一個大文件的解壓時間和多個小文件的解壓時間 ,文件總大小一致的話,確定是大文件快

官方說明:https://docs.unity3d.com/Manual/AssetBundles-Preparing.html

1.按邏輯實體(功能)分組,例如英雄界面相關的預知體一個包,副本界面相關的預製體打一個包,對熱更支持最高

2.按類型分組,例如音效、shader、本地化文件等都單獨打1-n個包,對熱更版本不友好,由於包體相對會被比較大

3.不相干(concurrent)內容分組,將須要同時加載和使用內容分組到同一個 AssetBundle 的策略。這種策略最經常使用在強本地相關屬性的內容上,也就是說內容不多或者基本不可能在應用特定的位置或者時間以外出現,例如某個副本關卡用到的獨特的資源、模型等

官方的分組建議以下:

Regardless of the strategy you follow, here are some additional tips that are good to keep in mind across the board:

1.Split frequently updated objects into AssetBundles separate from objects that rarely change
2.Group objects that are likely to be loaded simultaneously. Such as a model, its textures, and its animations
3.If you notice multiple objects across multiple AssetBundles are dependant on a single asset from a completely different AssetBundle, move the dependency to a separate AssetBundle. 
If several AssetBundles are referencing the same group of assets in other AssetBundles, it may be worth pulling those dependencies into a shared AssetBundle to reduce duplication. 4.If two sets of objects are unlikely to ever be loaded at the same time, such as Standard and High Definition assets, be sure they are in their own AssetBundles. 6.Consider splitting apart an AssetBundle if less that 50% of that bundle is ever frequently loaded at the same time 7.Consider combining AssetBundles that are small (less that 5 to 10 assets) but whose content is frequently loaded simultaneously 8.If a group of objects are simply different versions of the same object, consider AssetBundle Variants

1.把常常更新的資源放在一個單獨的包裏面,跟不常常更新的包分離
2.把須要同時加載的資源放在一個包裏面,如同一個功能模塊。若是兩個對象不太可能同時加載,好比一個紋理的高清和標清版本,能夠將他們分配到不一樣的 AssetBundle 中
3.能夠把其餘包共享的資源放在一個單獨的包裏面,例如UI界面裏面會有不少按鈕、彈窗,而這些資源通常是全部界面通用的,那就能夠把它們打1-3個圖集
4.控制ab包體的大小,太大了,熱更的話,要更新很大的文件,過小的話,io次數會很高,對性能很差

5、如何加載AssetBundle?

1.從包含AssetBundle數據的bytes 裏讀取

2.本地加載最快的接口,若是是lzma的壓縮格式,會先解壓到內存裏(佔用內存)

3.從網絡加載(也能夠從本地加載)

IEnumerator InstantiateObject()

    {
        string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;        
     UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0); yield return request.Send(); AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request); GameObject cube = bundle.LoadAsset<GameObject>("Cube"); GameObject sprite = bundle.LoadAsset<GameObject>("Sprite"); Instantiate(cube); Instantiate(sprite); }

6、如何從AssetBundle中加載文件?

同步加載單個對象:T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

異步加載單個對象:

AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;

同步加載ab包裏的全部對象:Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

異步加載ab包裏的全部對象:

AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync(); 
yield return request;
var loadedAssets = request.allAssets;

7、如何使用AssetBundleManifest,什麼叫作依賴包?

AssetBundles can become dependent on other AssetBundles if one or more of the UnityEngine.Objects contains a reference to a UnityEngine.Object located in another bundle. A dependency does not occur if the UnityEngine.Object contains a reference to a UnityEngine.Object that is not contained in any AssetBundle. In this case, a copy of the object that the bundle would be dependent on is copied into the bundle when you build the AssetBundles. If multiple objects in multiple bundles contain a reference to the same object that isn’t assigned to a bundle, every bundle that would have a dependency on that object will make its own copy of the object and package it into the built AssetBundle.

Should an AssetBundle contain a dependency, it is important that the bundles that contain those dependencies are loaded before the object you’re attempting to instantiate is loaded. Unity will not attempt to automatically load dependencies.

Consider the following example, a Material in Bundle 1 references a Texture in Bundle 2:

In this example, before loading the Material from Bundle 1, you would need to load Bundle 2 into memory. It does not matter which order you load Bundle 1 and Bundle 2, the important takeaway is that Bundle 2 is loaded before loading the Material from Bundle 1. In the next section, we’ll discuss how you can use the AssetBundleManifest objects we touched on in the previous section to determine, and load, dependencies at runtime.

ab包依賴狀況有如下兩種:

1.ab包引用到另外一個ab包裏面的資源,即ab包依賴了另外一個ab包,例如Bundle1材質A引用了Bundle2的貼圖B,那麼Bundle1就是依賴Bundle2的,在加載材質A前,你必須先加載Bundle2到內存,unity不會自動加載依賴項。也就是說在你使用某個ab包時,必須先加載他依賴的ab包。

2.ab包引用到另外一個再也不任何ab包裏的資源,例如Bundle1材質A引用了貼圖B,而貼圖B沒有打進任何ab包裏,那麼最終打ab包時,unity會拷貝一份貼圖B到Bundle1,若是有n個Bundle都引用了貼圖B,那麼這n個Bundle裏都會有貼圖的拷貝,會形成資源冗餘。

AssetBundleManifest文件包含了全部ab包的依賴關係,在使用ab包前,你須要先加載AssetBundleManifest文件,在經過AssetBundleManifest獲取ab包的依賴ab包,AssetBundleManifest的加載:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

獲取依賴包:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies)
{
    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}

7、如何判斷AssetBundle是否還被引用?

ab包的引用主要做用有兩點:1.緩存ab包,卸載無用的ab包,避免內存泄漏 2.判斷ab包的依賴包是否已經被加載

核心點是引用技術,每一個AssetBundle都會維護一個引用計數,當該ab包被加載、被依賴時引用計數加1,當依賴包被卸載、加載的資源被卸載時,引用計數減1,當引用計數爲0超過一段時間(通常爲幾分鐘)時,認爲該ab包已經無用了,卸載該ab包。

如下是一個簡單的示例(不涉及到從ab包里加載資源),簡單說明引用計數的用法,這個例子分兩部分,一個是緩存的ab包實體類AssetBundleCache,該類維護一個本身的引用計數,一個是緩存的控制類ABCachePool,該類用於維護ab包的引用、卸載。

using UnityEngine;

// AssetBundle緩存
public class AssetBundleCache
{
    string m_name;          // AssetBundle name
    int m_referencedCount;  // 引用計數
    float m_unloadTime;     // 釋放時間

    public AssetBundleCache(string name, AssetBundle ab, int refCount)
    {
        m_name = name;
        Bundle = ab;
        ReferencedCount = refCount;
    }

    // AssetBundle
    public AssetBundle Bundle
    {
        get;
        private set;
    }

    // 是否常駐,通用資源的ab包不卸載
    public bool Persistent
    {
        get;
        set;
    }

    public string BundleName
    {
        get
        {
            return m_name;
        }
    }

    // 引用計數
    public int ReferencedCount
    {
        get
        {
            return m_referencedCount;
        }

        set
        {
            m_referencedCount = value;
            if (m_referencedCount <= 0)
            {
                m_unloadTime = Time.realtimeSinceStartup;
            }
            else
            {
                m_unloadTime = 0;
            }
            if (m_referencedCount < 0)
            {
                Debug.LogWarningFormat("AssetBundleCache reference count < 0, name:{0}, referencecount:{1}", m_name, m_referencedCount);
            }
        }
    }

    // 是否能夠刪除
    public bool IsCanRemove
    {
        get
        {
            // 常駐資源
            if (Persistent) return false;

            // 很是駐,而且引用計數爲0
            if (!Persistent && ReferencedCount <= 0)
            {
                return true;
            }

            return false;
        }
    }

    // 緩存時間到
    public bool IsTimeOut
    {
        get
        {
            return Time.realtimeSinceStartup - m_unloadTime >= Config.Instance.AssetCacheTime;//這個時間本身定義
        }
    }

    //卸載ab包
    public void Unload()
    {
        if (Bundle != null)
        {
            Bundle.Unload(false);
            Bundle = null;
        }
    }
}
public class ABCachePool
{
    #region Instance
    private static ABCachePool m_Instance;
    public static ABCachePool Instance
    {
        get { return m_Instance ?? (m_Instance = new ABCachePool()); }
    }
    #endregion
    Dictionary<string, AssetBundleCache> m_AssetBundleCaches = new Dictionary<string, AssetBundleCache>();  // 緩存隊列
    HashSet<string> m_persistentABs = new HashSet<string>();

    public Dictionary<string, AssetBundleCache> AssetBundleCaches
    {
        get
        {
            return m_AssetBundleCaches;
        }
    }

  //只有在退出遊戲時會調用這個接口
public void ClearAllCache() { foreach(KeyValuePair<string, AssetBundleCache> keyval in m_AssetBundleCaches) { keyval.Value.Unload(); } m_AssetBundleCaches.Clear(); }
  //是否存在ab包緩存
public bool IsExistCache(string abName) { return m_AssetBundleCaches.ContainsKey (abName); } // 引用這個bundle public AssetBundleCache ReferenceCacheByName(string abName) { AssetBundleCache cache = null; m_AssetBundleCaches.TryGetValue (abName, out cache); if(cache!=null) { ++cache.ReferencedCount; } return cache; } // 獲取ABCache 不增長引用 public AssetBundleCache GetABCacheByName(string abName) { AssetBundleCache cache = null; m_AssetBundleCaches.TryGetValue (abName, out cache); return cache; } public AssetBundleCache AddCache(string abName, AssetBundle bundle, int refCount) { if(m_AssetBundleCaches.ContainsKey (abName)) { Debugger.LogWarning ("AssetBundleCache already contains key:{0}, it will be cover by new value.", abName); } AssetBundleCache cache = new AssetBundleCache (abName, bundle, refCount); m_AssetBundleCaches [abName] = cache; if(m_persistentABs.Contains (abName)) { cache.Persistent = true; } return cache; } // immediate 只有場景是馬上卸載 public AssetBundleCache UnReferenceCache(string abName, bool immediate = false) { AssetBundleCache cache = null; if (!m_AssetBundleCaches.TryGetValue(abName, out cache)) { return null; } if(cache.Persistent) { return null; } --cache.ReferencedCount; if (immediate && cache.IsCanRemove) { RemoveCache (abName); } return cache; }
public void RemoveCache(string abName) { AssetBundleCache cache = m_AssetBundleCaches [abName]; cache.Unload (); m_AssetBundleCaches.Remove(abName); } private List<string> m_lstRm = new List<string>(); // 清除無引用的AssetBundle緩存 public void ClearNoneRefCache(bool mustTimeout) { foreach(KeyValuePair<string, AssetBundleCache> keyval in m_AssetBundleCaches) { AssetBundleCache item = keyval.Value; // 只清除引用計數爲0的 if (item.IsCanRemove && (!mustTimeout || item.IsTimeOut)) { m_lstRm.Add(keyval.Key); } } for(int i=0; i<m_lstRm.Count; i++) { RemoveCache(m_lstRm[i]); } m_lstRm.Clear(); } /// <summary> /// 常駐ab包設置 /// </summary> /// <param name="arrAB"></param> public void SetPersistentABs(string[] arrAB) { m_persistentABs.Clear(); for (int i = 0; i < arrAB.Length; i++) { string strAB = FileHelper.GenBundlePath (arrAB[i]); m_persistentABs.Add(strAB); AssetBundleCache abCache; m_AssetBundleCaches.TryGetValue(strAB, out abCache); if (abCache!=null) { abCache.Persistent = true; } } } }

8、如何卸載AssetBundle?

Most projects should use AssetBundle.Unload(true) and adopt a method to ensure that Objects are not duplicated. Two common methods are:

Having well-defined points during the application’s lifetime at which transient AssetBundles are unloaded, such as between levels or during a loading screen.

Maintaining reference-counts for individual Objects and unload AssetBundles only when all of their constituent Objects are unused. This permits an application to unload & reload individual Objects without duplicating memory.

If an application must use AssetBundle.Unload(false), then individual Objects can only be unloaded in two ways:

Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.

Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.

AssetBundle.Unload能夠卸載一個AssetBundle,下面會說到這個方法

Resources.UnloadUnusedAssets會卸載全部不被引用的資源,具體以下圖所示

 

 9、注意事項

1.AssetBundle.Unload(bool unloadAllLoadedObjects)

unloadAllLoadedObjects爲true時會卸載全部從這個ab包里加載的對象(不包括instantiation對象),例如材質M加載自Bundle1,當Bundle1調用Unload(true)時,材質M也會被刪除,對象會在場景中顯示紅色(缺失)

unloadAllLoadedObjects爲false時不會卸載從這個ab包里加載的對象,但會斷開和這個對象的聯繫,例如材質M加載自Bundle1,當Bundle1調用Unload(false)時,材質M不會被刪除,當用戶再次加載Bundle1的時候不會從新創建和材質M的聯繫,而是會從新建立一份引用,形成材質M的冗餘,以下圖,內存裏會存在兩個材質M,冗餘了一份

 

 關於true跟false,unity官方時間以使用true的,這樣不會形成冗餘,可是你必須清楚的知道何時能夠卸載這個ab包

Most projects should use AssetBundle.Unload(true) and adopt a method to ensure that Objects are not duplicated. Two common methods are:

Having well-defined points during the application’s lifetime at which transient AssetBundles are unloaded, such as between levels or during a loading screen.

Maintaining reference-counts for individual Objects and unload AssetBundles only when all of their constituent Objects are unused. This permits an application to unload & reload individual Objects without duplicating memory.

If an application must use AssetBundle.Unload(false), then individual Objects can only be unloaded in two ways:

Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.

Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.

2.一個沒有被分配到任何ab包中的資源A,任何引用資源A的ab包都會產生一份資源A的拷貝,這會致使遊戲的ab包大小變大(資源A冗餘了),若是這兩個ab包都被加載到內存,那麼還會致使內存裏存在兩份徹底同樣資源A。

Any Object that is not explicitly assigned in an AssetBundle will be included in all AssetBundles that contain 1 or more Objects that reference the untagged Object.

If two different Objects are assigned to two different AssetBundles, but both have references to a common dependency Object, then that dependency Object will be copied into both AssetBundles. 
The duplicated dependency will also be instanced, meaning that the two copies of the dependency Object will be considered different Objects with a different identifiers.
This will increase the total size of the application’s AssetBundles. This will also cause two different copies of the Object to be loaded into memory
if the application loads both of its parents.

解決這個問題的最優解是:把全部ab包引用的資源都打到ab包裏,即ab包不引用任何再也不ab包裏的資源,可是這樣作須要程序在加載ab包時,確認該ab包的全部依賴包都已經加載完成了(ab包緩存、ab依賴包加載)

3.圖集:首先咱們須要大概知道圖集在AssetBundle裏是以什麼形式存在的。以下圖所示,一個圖集的ab包裏包含了這個圖集的圖片資源以及圖集的信息(下圖的SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12)

須要注意的是:

1.若是一個圖集包含的Sprite資源被包含在多個AssetBundle裏,那麼全部包含該圖集的Sprite的ab包都會有一份圖集信息(SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12),從上圖咱們看到,這個文件仍是很大的

2.若是一個圖集包含的Sprite資源再也不任何ab包裏,那麼圖集信息(SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12)也不會在任何的ab包裏

綜上,若是圖集分散到多個ab包,會形成資源冗餘,會增大包體大小,運行時也會浪費內存,若是不分配到ab包裏又沒法熱更,那麼惟一的作法就是一個圖集打一個ab包(把相同圖集的Sprite放在同一個文件夾,這個文件夾只包含該圖集的sprite,在把這個文件夾打成ab包)

4.減小同時加載的AB數量(這個是純邏輯控制),使用AssetBundle.LoadFromFile接口。使用WWW加載會生成一個新的線程,在移動平臺線程多了會致使遊戲崩潰,儘可能使用UnityWebRequest

相關文章
相關標籤/搜索