Bekerja dengan sumber daya eksternal di Unity3D

Pendahuluan


Halo pembaca yang budiman, hari ini kita akan berbicara tentang bekerja dengan sumber daya eksternal di lingkungan Unity 3d.

Secara tradisi, untuk memulai, kita akan menentukan apa itu dan mengapa kita membutuhkannya. Jadi, apa sebenarnya sumber daya eksternal ini. Sebagai bagian dari pengembangan game, sumber daya tersebut dapat berupa segala sesuatu yang diperlukan agar aplikasi berfungsi dan tidak boleh disimpan dalam pembangunan akhir proyek. Sumber daya eksternal dapat ditemukan di hard disk komputer pengguna dan di server web eksternal. Dalam kasus umum, sumber daya tersebut adalah setiap file atau kumpulan data yang kami muat ke dalam aplikasi kami yang sudah berjalan. Berbicara dalam kerangka Unity 3d, maka mereka dapat:

  • File teks
  • File tekstur
  • File audio
  • Array byte
  • AssetBundle (arsipkan dengan aset proyek 3d Unity)

Di bawah ini, kami akan memeriksa secara lebih terperinci mekanisme bawaan untuk bekerja dengan sumber daya ini yang ada di Unity 3d, serta menulis manajer sederhana untuk berinteraksi dengan server web dan memuat sumber daya ke dalam aplikasi.

Catatan : sisa artikel ini menggunakan kode menggunakan C # 7+ dan dirancang untuk kompiler Roslyn yang digunakan di Unity3d dalam versi 2018.3+.

Fitur Unity 3d


Sebelum versi Unity 2017, satu mekanisme (tidak termasuk yang dijelaskan sendiri) digunakan untuk bekerja dengan data server dan sumber daya eksternal, yang termasuk dalam mesin - ini adalah kelas WWW. Kelas ini memungkinkan penggunaan berbagai perintah http (get, post, put, dll.) Dalam bentuk sinkron atau asinkron (via Coroutine). Bekerja dengan kelas ini cukup sederhana dan mudah.

IEnumerator LoadFromServer(string url) { var www = new WWW(url); yield return www; Debug.Log(www.text); } 

Demikian pula, Anda bisa mendapatkan tidak hanya data teks, tetapi juga yang lain:


Namun, mulai dari versi 2017, Unity memiliki sistem server baru yang diperkenalkan oleh kelas UnityWebRequest , yang terletak di namespace Jaringan. Sampai Unity 2018, itu ada bersama dengan WWW , tetapi dalam versi terbaru dari mesin WWW itu menjadi tidak direkomendasikan, dan di masa depan itu akan sepenuhnya dihapus. Karena itu, lebih jauh kita akan fokus hanya pada UnityWebRequest (selanjutnya disebut UWR).

Bekerja dengan UWR secara keseluruhan mirip dengan WWW pada intinya, tetapi ada perbedaan, yang akan dibahas nanti. Di bawah ini adalah contoh serupa dari memuat teks.

 IEnumerator LoadFromServer(string url) { var request = new UnityWebRequest(url); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); request.Dispose(); } 

Perubahan utama yang diperkenalkan sistem UWR baru (selain mengubah prinsip bekerja di dalam) adalah kemampuan untuk menetapkan penangan untuk mengunggah dan mengunduh data dari server itu sendiri, lebih jelasnya dapat dibaca di sini . Secara default, ini adalah kelas UploadHandler dan DownloadHandler . Unity sendiri menyediakan satu set ekstensi dari kelas-kelas ini untuk bekerja dengan berbagai data, seperti audio, tekstur, aset, dll. Mari pertimbangkan bekerja dengan mereka secara lebih rinci.

Bekerja dengan sumber daya


Teks


Bekerja dengan teks adalah salah satu opsi termudah. Metode untuk mengunduhnya sudah dijelaskan di atas. Kami menulis ulang sedikit dengan menggunakan pembuatan http langsung. Dapatkan permintaan.

 IEnumerator LoadTextFromServer(string url, Action<string> response) { var request = UnityWebRequest.Get(url); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(uwr.downloadHandler.text); } else { Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error); response(null); } request.Dispose(); } 

Seperti yang dapat Anda lihat dari kode, DownloadHandler default digunakan di sini. Properti teks adalah pengambil yang mengubah array byte ke teks yang disandikan UTF8. Penggunaan utama memuat teks dari server adalah untuk menerima file json (representasi serial data dalam bentuk teks). Anda bisa mendapatkan data ini menggunakan kelas Unity JsonUtility .

 var data = JsonUtility.FromJson<T>(value); // T  ,    . 

Audio


Untuk bekerja dengan audio, Anda harus menggunakan metode khusus untuk membuat permintaan UnityWebRequestMultimedia.GetAudioClip , dan untuk mendapatkan representasi data dalam bentuk yang diperlukan untuk bekerja di Unity, Anda harus menggunakan DownloadHandlerAudioClip . Selain itu, saat membuat permintaan, Anda harus menentukan jenis data audio yang diwakili oleh enumerasi AudioType , yang menetapkan format (wav, aiff, oggvorbis, dll.).

 IEnumerator LoadAudioFromServer(string url, AudioType audioType, Action<AudioClip> response) { var request = UnityWebRequestMultimedia.GetAudioClip(url, audioType); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(DownloadHandlerAudioClip.GetContent(request)); } else { Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error); response(null); } request.Dispose(); } 

Tekstur


Mengunduh tekstur mirip dengan untuk file audio. Permintaan dibuat menggunakan UnityWebRequestTexture.GetTexture . Untuk mendapatkan data dalam bentuk yang diperlukan untuk Unity, DownloadHandlerTexture digunakan.

 IEnumerator LoadTextureFromServer(string url, Action<Texture2D> response) { var request = UnityWebRequestTexture.GetTexture(url); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(DownloadHandlerTexture.GetContent(request)); } else { Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error); response(null); } request.Dispose(); } 

Bundel aset


Seperti yang disebutkan sebelumnya, bundel ini, pada kenyataannya, adalah arsip dengan sumber daya Unity yang dapat digunakan dalam game yang sudah berjalan. Sumber daya ini dapat berupa aset proyek apa pun, termasuk adegan. Pengecualiannya adalah skrip C #, skrip tersebut tidak dapat dikirimkan. Untuk memuat AssetBundle , digunakan kueri yang dibuat menggunakan UnityWebRequestAssetBundle.GetAssetBundle. DownloadHandlerAssetBundle digunakan untuk mendapatkan data dalam bentuk yang diperlukan untuk Unity.

 IEnumerator LoadBundleFromServer(string url, Action<AssetBundle> response) { var request = UnityWebRequestAssetBundle.GetAssetBundle(url); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(DownloadHandlerAssetBundle.GetContent(request)); } else { Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error); response(null); } request.Dispose(); } 

Masalah utama dan solusi saat bekerja dengan server web dan data eksternal


Metode interaksi sederhana antara aplikasi dan server dalam hal memuat berbagai sumber daya telah dijelaskan di atas. Namun, dalam praktiknya, banyak hal yang jauh lebih rumit. Pertimbangkan masalah utama yang menyertai pengembang dan memikirkan cara untuk menyelesaikannya.

Ruang kosong tidak cukup


Salah satu masalah pertama saat mengunduh data dari server adalah kemungkinan kurangnya ruang kosong pada perangkat. Sering terjadi bahwa pengguna menggunakan perangkat lama untuk permainan (terutama pada Android), serta ukuran file yang diunduh itu sendiri bisa cukup besar (halo PC). Bagaimanapun, situasi ini harus diproses dengan benar dan pemain harus diberitahu sebelumnya bahwa tidak ada cukup ruang dan berapa banyak. Bagaimana cara melakukannya? Hal pertama yang perlu Anda ketahui adalah ukuran file yang diunduh, ini dilakukan melalui permintaan UnityWebRequest.Head () . Di bawah ini adalah kode untuk mendapatkan ukuran.

 IEnumerator GetConntentLength(string url, Action<int> response) { var request = UnityWebRequest.Head(url); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { var contentLength = request.GetResponseHeader("Content-Length"); if (int.TryParse(contentLength, out int returnValue)) { response(returnValue); } else { response(-1); } } else { Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error); response(-1); } } 

Penting untuk mencatat satu hal di sini, agar permintaan berfungsi dengan benar, server harus dapat mengembalikan ukuran konten, jika tidak (karena, sebenarnya, untuk menampilkan kemajuan), nilai yang salah akan dikembalikan.

Setelah kami mendapatkan ukuran data yang diunduh, kami dapat membandingkannya dengan ukuran ruang disk kosong. Untuk mendapatkan yang terakhir, saya menggunakan plugin gratis dari Asset Store .

Catatan : Anda bisa menggunakan kelas Cache di Unity3d, ini bisa menunjukkan ruang cache yang bebas dan bekas. Namun, perlu dipertimbangkan bahwa data ini relatif. Mereka dihitung berdasarkan ukuran cache itu sendiri, secara default adalah 4GB. Jika pengguna memiliki lebih banyak ruang kosong daripada ukuran cache, maka tidak akan ada masalah, namun jika tidak demikian, maka nilai dapat mengambil nilai yang tidak benar relatif terhadap keadaan sebenarnya.

Pemeriksaan akses internet


Sangat sering, sebelum mengunduh apa pun dari server, perlu untuk menangani situasi kurangnya akses Internet. Ada beberapa cara untuk melakukan ini: mulai dari ping alamat, ke permintaan GET ke google.ru. Namun, menurut saya, hasil yang paling benar dan memberikan hasil yang cepat dan stabil adalah mengunduh dari server Anda sendiri (yang sama dari tempat file akan diunduh) file kecil. Cara melakukannya dijelaskan di atas pada bagian bekerja dengan teks.
Selain memeriksa fakta memiliki akses Internet, perlu juga untuk menentukan jenisnya (seluler atau WiFi), karena kecil kemungkinan seorang pemain ingin mengunduh beberapa ratus megabyte pada lalu lintas seluler. Ini dapat dilakukan melalui properti Application.internetReachability .

Caching


Selanjutnya, dan salah satu masalah paling penting, adalah caching file yang diunduh. Untuk apa caching ini?

  1. Menghemat lalu lintas (jangan mengunduh data yang sudah diunduh)
  2. Memastikan pekerjaan tanpa adanya Internet (Anda dapat menampilkan data dari cache).

Apa yang perlu di-cache? Jawaban atas pertanyaan ini adalah segalanya, semua file yang Anda unduh harus di-cache. Cara melakukannya, pertimbangkan di bawah, dan mulai dengan file teks sederhana.
Sayangnya, Unity tidak memiliki mekanisme bawaan untuk caching teks, serta tekstur dan file audio. Oleh karena itu, untuk sumber daya ini perlu untuk menulis sistem Anda, atau tidak menulis, tergantung pada kebutuhan proyek. Dalam kasus yang paling sederhana, kita cukup menulis file ke cache dan, dengan tidak adanya Internet, kita mengambil file darinya. Dalam versi yang sedikit lebih kompleks (saya menggunakannya dalam proyek-proyek), kami mengirim permintaan ke server, yang mengembalikan json yang menunjukkan versi file yang disimpan di server. Anda dapat menulis dan membaca file dari cache menggunakan kelas C # dari kelas File atau dengan cara lain yang nyaman dan diterima oleh tim Anda.

 private void CacheText(string fileName, string data) { var cacheFilePath = Path.Combine("CachePath", "{0}.text".Fmt(fileName)); File.WriteAllText(cacheFilePath, data); } private void CacheTexture(string fileName, byte[] data) { var cacheFilePath = Path.Combine("CachePath", "{0}.texture".Fmt(fileName)); File.WriteAllBytes(cacheFilePath, data); } 

Demikian pula, mendapatkan data dari cache.

 private string GetTextFromCache(string fileName) { var cacheFilePath = Path.Combine(Utils.Path.Cache, "{0}.text".Fmt(fileName)); if (File.Exists(cacheFilePath)) { return File.ReadAllText(cacheFilePath); } return null; } private Texture2D GetTextureFromCache(string fileName) { var cacheFilePath = Path.Combine(Utils.Path.Cache, "{0}.texture".Fmt(fileName)); Texture2D texture = null; if (File.Exists(cacheFilePath)) { var data = File.ReadAllBytes(cacheFilePath); texture = new Texture2D(1, 1); texture.LoadImage(data, true); } return texture; } 

Catatan : mengapa UWR yang sama dengan url dari file formulir: // tidak digunakan untuk memuat tekstur. Saat ini, ada masalah dengan ini, file tidak dapat dimuat, jadi saya harus mencari solusinya.

Catatan : Saya tidak menggunakan pemuatan langsung AudioClip dalam proyek, saya menyimpan semua data tersebut di AssetBundle. Namun, jika perlu, ini dapat dengan mudah dilakukan menggunakan fungsi kelas AudioClip GetData dan SetData.

Sumber daya Unity yang sederhana untuk AssetBundle , Unity memiliki mekanisme caching bawaan . Mari kita pertimbangkan lebih terinci.

Pada dasarnya, mekanisme ini dapat menggunakan dua pendekatan:

  1. Menggunakan CRC dan nomor versi
  2. Menggunakan Nilai Hash

Pada prinsipnya, Anda dapat menggunakan salah satu dari mereka, tetapi saya memutuskan sendiri bahwa Hash adalah yang paling dapat diterima, karena saya memiliki sistem versi saya sendiri dan tidak hanya memperhitungkan versi AssetBundle , tetapi juga versi aplikasi, karena seringkali bundel mungkin tidak kompatibel dengan versi tersebut, disajikan di toko-toko.

Jadi, bagaimana caching dilakukan:

  1. Kami meminta file bundel dari server manifes (file ini dibuat secara otomatis saat dibuat dan berisi deskripsi aset yang dikandungnya, serta hash, crc, ukuran, dll.). File tersebut memiliki nama yang sama dengan bundel plus ekstensi .manifest.
  2. Dapatkan nilai hash128 dari manifes
  3. Kami membuat permintaan ke server untuk mendapatkan AssetBundle, di mana selain url, tentukan nilai hash128 yang diterima

Kode untuk algoritma yang dijelaskan di atas:
 IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response) { // ,    while (!Caching.ready) { yield return null; } //     var request = UnityWebRequest.Get(url + ".manifest"); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { Hash128 hash = default; // hash var hashRow = request.downloadHandler.text.ToString().Split("\n".ToCharArray())[5]; hash = Hash128.Parse(hashRow.Split(':')[1].Trim()); if (hash.isValid == true) { request.Dispose(); request = UnityWebRequestAssetBundle.GetAssetBundle(url, hash, 0); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(DownloadHandlerAssetBundle.GetContent(request)); } else { response(null); } } else { response(null); } } else { response(null); } request.Dispose(); } 


Dalam contoh di atas, Unity, atas permintaan ke server, pertama-tama terlihat apakah ada file dalam cache dengan nilai hash128 yang ditentukan, jika ya, akan dikembalikan, jika tidak, file yang diperbarui akan diunduh. Untuk mengelola semua file cache di Unity, ada kelas Caching , yang dengannya kita bisa mengetahui apakah ada file di cache, dapatkan semua versi yang di-cache, serta hapus yang tidak perlu, atau hapus sepenuhnya.

Catatan : mengapa cara aneh mendapatkan nilai hash? Hal ini disebabkan oleh fakta bahwa mendapatkan hash128 dengan cara yang dijelaskan dalam dokumentasi memerlukan pemuatan seluruh bundel, dan kemudian menerima aset AssetBundleManifest dari itu dan dari sana sudah memiliki nilai hash. Kelemahan dari pendekatan ini adalah bahwa seluruh AssetBundle berayun, tetapi kita hanya perlu melakukannya. Oleh karena itu, pertama-tama kita unduh hanya file manifes dari server, ambil hash128 darinya, dan hanya kemudian, jika perlu, unduh file bundle, dan nilai hash128 harus ditarik melalui interpretasi baris.

Bekerja dengan sumber daya dalam mode editor


Masalah terakhir, atau lebih tepatnya masalah debugging dan kenyamanan pengembangan, bekerja dengan sumber daya yang dapat diunduh dalam mode editor, jika tidak ada masalah dengan file biasa, maka hal-hal yang tidak begitu sederhana dengan bundel. Tentu saja, Anda dapat membangunnya setiap waktu, mengunggahnya ke server dan meluncurkan aplikasi di editor Unity dan melihat bagaimana semuanya bekerja, tetapi bahkan deskripsi ini terdengar seperti "penopang". Sesuatu harus dilakukan dengan ini dan untuk ini kelas AssetDatabase akan membantu kami.

Untuk menyatukan pekerjaan dengan bundel, saya membuat pembungkus khusus:

 public class AssetBundleWrapper { private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } } 

Sekarang kita perlu menambahkan dua mode bekerja dengan aset, tergantung pada apakah kita berada di editor atau di build. Untuk membangun, kami menggunakan pembungkus atas fungsi-fungsi kelas AssetBundle , dan untuk editor kami menggunakan kelas AssetDatabase yang disebutkan di atas.

Dengan demikian, kami memperoleh kode berikut:
 public class AssetBundleWrapper { #if UNITY_EDITOR private readonly List<string> _assets; public AssetBundleWrapper(string url) { var uri = new Uri(url); var bundleName = Path.GetFileNameWithoutExtension(uri.LocalPath); _assets = new List<string>(AssetDatabase.GetAssetPathsFromAssetBundle(bundleName)); } public T LoadAsset<T>(string name) where T : UnityEngine.Object { var assetPath = _assets.Find(item => { var assetName = Path.GetFileNameWithoutExtension(item); return string.CompareOrdinal(name, assetName) == 0; }); if (!string.IsNullOrEmpty(assetPath)) { return AssetDatabase.LoadAssetAtPath<T>(assetPath); } else { return default; } } public T[] LoadAssets<T>() where T : UnityEngine.Object { var returnedValues = new List<T>(); foreach(var assetPath in _assets) { returnedValues.Add(AssetDatabase.LoadAssetAtPath<T>(assetPath)); } return returnedValues.ToArray(); } public void LoadAssetAsync<T>(string name, Action<T> result) where T : UnityEngine.Object { result(LoadAsset<T>(name)); } public void LoadAssetsAsync<T>(Action<T[]> result) where T : UnityEngine.Object { result(LoadAssets<T>()); } public string[] GetAllScenePaths() { return _assets.ToArray(); } public void Unload(bool includeAllLoadedAssets = false) { _assets.Clear(); } #else private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } public T LoadAsset<T>(string name) where T : UnityEngine.Object { return _assetBundle.LoadAsset<T>(name); } public T[] LoadAssets<T>() where T : UnityEngine.Object { return _assetBundle.LoadAllAssets<T>(); } public void LoadAssetAsync<T>(string name, Action<T> result) where T : UnityEngine.Object { var request = _assetBundle.LoadAssetAsync<T>(name); TaskManager.Task.Create(request) .Subscribe(() => { result(request.asset as T); Unload(false); }) .Start(); } public void LoadAssetsAsync<T>(Action<T[]> result) where T : UnityEngine.Object { var request = _assetBundle.LoadAllAssetsAsync<T>(); TaskManager.Task.Create(request) .Subscribe(() => { var assets = new T[request.allAssets.Length]; for (var i = 0; i < request.allAssets.Length; i++) { assets[i] = request.allAssets[i] as T; } result(assets); Unload(false); }) .Start(); } public string[] GetAllScenePaths() { return _assetBundle.GetAllScenePaths(); } public void Unload(bool includeAllLoadedAssets = false) { _assetBundle.Unload(includeAllLoadedAssets); } #endif } 


Catatan : kode menggunakan kelas TaskManager , akan dibahas di bawah ini, singkatnya, ini adalah pembungkus untuk bekerja dengan Coroutine .

Selain hal di atas, juga berguna selama pengembangan untuk melihat apa yang kami unduh dan apa yang saat ini ada dalam cache. Untuk tujuan ini, Anda dapat memanfaatkan kemampuan untuk mengatur folder Anda sendiri, yang akan digunakan untuk caching (Anda juga dapat menulis teks yang diunduh dan file lainnya ke folder yang sama):

 #if UNITY_EDITOR var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "_EditorCache"); #else var path = Path.Combine(Application.persistentDataPath, "_AppCache"); #endif Caching.currentCacheForWriting = Caching.AddCache(path); 

Kami menulis manajer permintaan jaringan atau bekerja dengan server web


Di atas kami memeriksa aspek-aspek utama bekerja dengan sumber daya eksternal di Unity, sekarang saya ingin membahas implementasi API, yang menggeneralisasikan dan menyatukan semua hal di atas. Dan pertama, mari kita memikirkan manajer kueri jaringan.

Catatan : selanjutnya, kami menggunakan pembungkus Coroutine dalam bentuk kelas TaskManager . Saya menulis tentang bungkus ini di artikel lain .

Mari kita dapatkan kelas yang sesuai:

 public class Network { public enum NetworkTypeEnum { None, Mobile, WiFi } public static NetworkTypeEnum NetworkType; private readonly TaskManager _taskManager = new TaskManager(); } 

Bidang statis NetworkType diperlukan untuk aplikasi untuk menerima informasi tentang jenis koneksi Internet. Pada prinsipnya, nilai ini dapat disimpan di mana saja, saya memutuskan bahwa ini adalah tempat di kelas Jaringan .

Tambahkan fungsi dasar mengirim permintaan ke server:
 private IEnumerator WebRequest(UnityWebRequest request, Action<float> progress, Action<UnityWebRequest> response) { while (!Caching.ready) { yield return null; } if (progress != null) { request.SendWebRequest(); _currentRequests.Add(request); while (!request.isDone) { progress(request.downloadProgress); yield return null; } progress(1f); } else { yield return request.SendWebRequest(); } response(request); if (_currentRequests.Contains(request)) { _currentRequests.Remove(request); } request.Dispose(); } 


Seperti yang dapat Anda lihat dari kode, metode untuk memproses penyelesaian permintaan telah berubah dibandingkan dengan kode di bagian sebelumnya. Ini untuk menunjukkan kemajuan pemuatan data. Juga, semua permintaan yang dikirim disimpan dalam daftar sehingga, jika perlu, mereka dapat dibatalkan.

Tambahkan fungsi pembuatan kueri berbasis tautan untuk AssetBundle:
 private IEnumerator WebRequestBundle(string url, Hash128 hash, Action<float> progress, Action<UnityWebRequest> response) { var request = UnityWebRequestAssetBundle.GetAssetBundle(url, hash, 0); return WebRequest(request, progress, response); } 


Demikian pula, fungsi dibuat untuk tekstur, audio, teks, byte array.

Sekarang Anda perlu memastikan bahwa server mengirim data melalui perintah Post. Seringkali Anda perlu mengirimkan sesuatu ke server, dan tergantung pada apa tepatnya, dapatkan jawabannya. Tambahkan fungsi yang sesuai.

Mengirim data dalam bentuk set nilai kunci:
 private IEnumerator WebRequestPost(string url, Dictionary<string, string> formFields, Action<float> progress, Action<UnityWebRequest> response) { var request = UnityWebRequest.Post(url, formFields); return WebRequest(request, progress, response); } 


Mengirim data sebagai json:
 private IEnumerator WebRequestPost(string url, string data, Action<float> progress, Action<UnityWebRequest> response) { var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST) { uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(data)), downloadHandler = new DownloadHandlerBuffer() }; request.uploadHandler.contentType = "application/json"; return WebRequest(request, progress, response); } 


Sekarang kita akan menambahkan metode publik yang dengannya kita akan memuat data, khususnya AssetBundle
 public void Request(string url, Hash128 hash, Action<float> progress, Action<AssetBundle> response, TaskManager.TaskPriorityEnum priority = TaskManager.TaskPriorityEnum.Default) { _taskManager.AddTask(WebRequestBundle(url, hash, progress, (uwr) => { if (!uwr.isHttpError && !uwr.isNetworkError) { response(DownloadHandlerAssetBundle.GetContent(uwr)); } else { Debug.LogWarningFormat("[Netowrk]: error request [{0}]", uwr.error); response(null); } }), priority); } 


Demikian pula, metode ditambahkan untuk tekstur, file audio, teks, dll.

Dan akhirnya, kami menambahkan fungsi untuk mendapatkan ukuran file yang diunduh dan fungsi pembersihan untuk menghentikan semua permintaan yang dibuat.
 public void Request(string url, Action<int> response, TaskManager.TaskPriorityEnum priority = TaskManager.TaskPriorityEnum.Default) { var request = UnityWebRequest.Head(url); _taskManager.AddTask(WebRequest(request, null, uwr => { var contentLength = uwr.GetResponseHeader("Content-Length"); if (int.TryParse(contentLength, out int returnValue)) { response(returnValue); } else { response(-1); } }), priority); } public void Clear() { _taskManager.Clear(); foreach (var request in _currentRequests) { request.Abort(); request.Dispose(); } _currentRequests.Clear(); } 


Pada ini, manajer kami untuk bekerja dengan permintaan jaringan selesai. Jika perlu, setiap subsistem permainan yang mengharuskan bekerja dengan server dapat membuat instance kelasnya sendiri.

Menulis manajer pemuatan sumber daya eksternal


Selain kelas yang dijelaskan di atas, untuk sepenuhnya bekerja dengan data eksternal, kami membutuhkan manajer terpisah yang tidak hanya akan mengunduh data, tetapi juga memberi tahu aplikasi tentang mulai mengunduh, menyelesaikan, kemajuan, kurangnya ruang kosong, dan juga menangani masalah caching.

Kami memulai kelas yang sesuai, yang dalam kasus saya adalah singleton
 public class ExternalResourceManager { public enum ResourceEnumType { Text, Texture, AssetBundle } private readonly Network _network = new Network(); public void ExternalResourceManager() { #if UNITY_EDITOR var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "_EditorCache"); #else var path = Path.Combine(Application.persistentDataPath, "_AppCache"); #endif if (!System.IO.Directory.Exists(path)) { System.IO.Directory.CreateDirectory(path); #if UNITY_IOS UnityEngine.iOS.Device.SetNoBackupFlag(path); #endif } Caching.currentCacheForWriting = Caching.AddCache(path); } } 


Seperti yang Anda lihat, perancang menetapkan folder untuk caching, tergantung pada apakah kita ada di editor atau tidak. Kami juga menyiapkan bidang pribadi untuk turunan kelas Jaringan, yang telah kami jelaskan sebelumnya.

Sekarang kita akan menambahkan fungsi bantu untuk bekerja dengan cache, serta menentukan ukuran file yang diunduh dan memeriksa ruang kosong untuk itu. Lebih lanjut dan di bawah, kode diberikan pada contoh bekerja dengan AssetBundle, untuk sisa sumber daya semuanya dilakukan dengan analogi.

Kode Fungsi Pembantu
 public void ClearAssetBundleCache(string url) { var fileName = GetFileNameFromUrl(url); Caching.ClearAllCachedVersions(fileName); } public void ClearAllRequest() { _network.Clear(); } public void AssetBundleIsCached(string url, Action<bool> result) { var manifestFileUrl = "{0}.manifest".Fmt(url); _network.Request(manifestFileUrl, null, (string manifest) => { var hash = string.IsNullOrEmpty(manifest) ? default : GetHashFromManifest(manifest); result(Caching.IsVersionCached(url, hash)); } , TaskManager.TaskPriorityEnum.RunOutQueue); } public void CheckFreeSpace(string url, Action<bool, float> result) { GetSize(url, lengthInMb => { #if UNITY_EDITOR_WIN var logicalDrive = Path.GetPathRoot(Utils.Path.Cache); var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(logicalDrive); #elif UNITY_EDITOR_OSX var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(); #elif UNITY_IOS var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(); #elif UNITY_ANDROID var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(true); #endif result(availableSpace > lengthInMb, lengthInMb); }); } public void GetSize(string url, Action<float> result) { _network.Request(url, length => result(length / 1048576f)); } private string GetFileNameFromUrl(string url) { var uri = new Uri(url); var fileName = Path.GetFileNameWithoutExtension(uri.LocalPath); return fileName; } private Hash128 GetHashFromManifest(string manifest) { var hashRow = manifest.Split("\n".ToCharArray())[5]; var hash = Hash128.Parse(hashRow.Split(':')[1].Trim()); return hash; } 


Sekarang mari kita tambahkan fungsi pemuatan data menggunakan contoh AssetBundle.
 public void GetAssetBundle(string url, Action start, Action<float> progress, Action stop, Action<AssetBundleWrapper> result, TaskManager.TaskPriorityEnum taskPriority = TaskManager.TaskPriorityEnum.Default) { #if DONT_USE_SERVER_IN_EDITOR start?.Invoke(); result(new AssetBundleWrapper(url)); stop?.Invoke(); #else void loadAssetBundle(Hash128 bundleHash) { start?.Invoke(); _network.Request(url, bundleHash, progress, (AssetBundle value) => { if(value != null) { _externalResourcesStorage.SetCachedHash(url, bundleHash); } result(new AssetBundleWrapper(value)); stop?.Invoke(); }, taskPriority); }; var manifestFileUrl = "{0}.manifest".Fmt(url); _network.Request(manifestFileUrl, null, (string manifest) => { var hash = string.IsNullOrEmpty(manifest) ? default : GetHashFromManifest(manifest); if (!hash.isValid || hash == default) { hash = _externalResourcesStorage.GetCachedHash(url); if (!hash.isValid || hash == default) { result(new AssetBundleWrapper(null)); } else { loadAssetBundle(hash); } } else { if (Caching.IsVersionCached(url, hash)) { loadAssetBundle(hash); } else { CheckFreeSpace(url, (spaceAvailable, length) => { if (spaceAvailable) { loadAssetBundle(hash); } else { result(new AssetBundleWrapper(null)); NotEnoughDiskSpace.Call(); } }); } } #endif } 


Jadi apa yang terjadi dalam fungsi ini:

  • Arahan precompilation DONT_USE_SERVER_IN_EDITOR digunakan untuk menonaktifkan pemuatan bundel yang sebenarnya dari server.
  • Langkah pertama adalah membuat permintaan ke server untuk mendapatkan file manifes untuk bundel
  • - , , - ( _externalResourcesStorage ) , , ( , ), , null
  • , Caching , , ( )
  • , , , , - ( ). , ( )

Catatan : Penting untuk memahami mengapa nilai hash disimpan secara terpisah. Ini diperlukan untuk kasus ketika tidak ada Internet, atau koneksi tidak stabil, atau ada beberapa jenis kesalahan jaringan dan kami tidak dapat mengunduh bundel dari server, dalam hal ini kami menjamin untuk mengunduh bundel dari cache jika hadir di sana .

Demikian pula dengan metode yang dijelaskan di atas, di manajer, Anda dapat / perlu mendapatkan fungsi lain untuk bekerja dengan data: GetJson, GetTexture, GetText, GetAudio, dll.

Dan akhirnya, Anda perlu mendapatkan metode yang memungkinkan Anda mengunduh set sumber daya. Metode ini akan berguna jika kita perlu mengunduh atau memperbarui sesuatu di awal aplikasi.
 public void GetPack(Dictionary<string, ResourceEnumType> urls, Action start, Action<float> progress, Action stop, Action<string, object, bool> result) { var commonProgress = (float)urls.Count; var currentProgress = 0f; var completeCounter = 0; void progressHandler(float value) { currentProgress += value; progress?.Invoke(currentProgress / commonProgress); }; void completeHandler() { completeCounter++; if (completeCounter == urls.Count) { stop?.Invoke(); } }; start?.Invoke(); foreach (var url in urls.Keys) { var resourceType = urls[url]; switch (resourceType) { case ResourceEnumType.Text: { GetText(url, null, progressHandler, completeHandler, (value, isCached) => { result(url, value, isCached); }); } break; case ResourceEnumType.Texture: { GetTexture(url, null, progressHandler, completeHandler, (value, isCached) => { result(url, value, isCached); }); } break; case ResourceEnumType.AssetBundle: { GetAssetBundle(url, null, progressHandler, completeHandler, (value) => { result(url, value, false); }); } break; } } } 


Di sini perlu dipahami keanehan dari TaskManager , yang digunakan dalam manajer permintaan jaringan, secara default berfungsi, melakukan semua tugas secara bergantian. Karenanya, pengunduhan file akan terjadi.

Catatan : bagi mereka yang tidak suka Coroutine , semuanya dapat dengan mudah diterjemahkan ke dalam async / menunggu , tetapi dalam kasus ini, dalam artikel, saya memutuskan untuk menggunakan opsi yang lebih dimengerti untuk pemula (seperti yang menurut saya).

Kesimpulan


Pada artikel ini, saya mencoba menggambarkan bekerja dengan sumber daya eksternal dari aplikasi game sekompleks mungkin. Pendekatan dan kode ini digunakan dalam proyek-proyek yang telah dirilis dan sedang dikembangkan dengan partisipasi saya. Ini cukup sederhana dan dapat diterapkan dalam game sederhana di mana tidak ada komunikasi yang konstan dengan server (MMO dan game f2p kompleks lainnya), tetapi sangat memudahkan pekerjaan jika kita perlu mengunduh materi tambahan, bahasa, mengimplementasikan validasi pembelian di sisi server dan data lain yang satu kali atau tidak terlalu sering digunakan dalam aplikasi.

Tautan yang disediakan dalam artikel :
assetstore.unity.com/packages/tools/simple-disk-utils-59382
habr.com/post/352296
habr.com/post/282524

Source: https://habr.com/ru/post/id433366/


All Articles