مقدمة
مرحبًا أيها القراء الأعزاء ، سنتحدث اليوم عن العمل مع الموارد الخارجية في بيئة الوحدة ثلاثية الأبعاد.
حسب التقاليد ، في البداية ، سنحدد ما هو ولماذا نحتاجه. لذلك ، ما هي بالضبط هذه الموارد الخارجية. كجزء من تطوير اللعبة ، يمكن أن تكون هذه الموارد هي كل ما هو مطلوب حتى يعمل التطبيق ويجب ألا يتم تخزينه في الإنشاء النهائي للمشروع. يمكن العثور على الموارد الخارجية على القرص الثابت لجهاز الكمبيوتر الخاص بالمستخدم وعلى خادم ويب خارجي. في الحالة العامة ، هذه الموارد هي أي ملف أو مجموعة بيانات نقوم بتحميلها في تطبيقنا قيد التشغيل بالفعل. عند التحدث في إطار Unity 3d ، يمكن أن يكونوا:
- ملف نصي
- ملف الملمس
- ملف الصوت
- مجموعة بايت
- AssetBundle (الأرشيف مع أصول مشروع الوحدة ثلاثية الأبعاد)
أدناه ، سوف ندرس بمزيد من التفصيل الآليات المضمنة للعمل مع هذه الموارد الموجودة في Unity 3D ، وكذلك كتابة مديرين بسيطين للتفاعل مع خادم الويب وتحميل الموارد في التطبيق.
ملاحظة : تستخدم
بقية هذه المقالة تعليمة برمجية باستخدام C # 7+ وهي مصممة لمترجم Roslyn المستخدم في Unity3d في الإصدارات 2018.3+.ملامح الوحدة 3D
قبل Unity 2017 ، تم استخدام آلية واحدة (باستثناء الوصف الذاتي) للعمل مع بيانات الخادم والموارد الخارجية ، والتي تم تضمينها في المحرك - هذه هي فئة WWW. سمحت هذه الفئة باستخدام أوامر http المختلفة (get ، post ، put ، إلخ.) بشكل متزامن أو غير متزامن (عبر Coroutine). كان العمل مع هذه الفئة بسيطًا ومباشرًا.
IEnumerator LoadFromServer(string url) { var www = new WWW(url); yield return www; Debug.Log(www.text); }
وبالمثل ، لا يمكنك الحصول على البيانات النصية فقط ، ولكن أيضًا البيانات الأخرى:
ومع ذلك ، بدءًا من الإصدار 2017 ، لدى Unity نظام خادم جديد مقدم من فئة
UnityWebRequest ، والذي يقع في مساحة أسماء الشبكات. حتى Unity 2018 ، كانت موجودة إلى جانب
WWW ، ولكن في الإصدار الأخير من محرك
WWW أصبح غير مستحسن ، وسيتم إزالته بالكامل في المستقبل. لذلك ، سوف نركز أيضًا على
UnityWebRequest (المشار إليها فيما يلي بـ UWR).
العمل مع UWR ككل يشبه WWW في جوهره ، ولكن هناك اختلافات ، والتي سيتم مناقشتها لاحقًا. يوجد أدناه مثال مشابه لتحميل النص.
IEnumerator LoadFromServer(string url) { var request = new UnityWebRequest(url); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); request.Dispose(); }
التغييرات الرئيسية التي أدخلها نظام UWR الجديد (بالإضافة إلى تغيير مبدأ العمل من الداخل) هي القدرة على تعيين معالجات لتحميل وتنزيل البيانات من الخادم نفسه ، يمكن قراءة المزيد من التفاصيل
هنا . بشكل افتراضي ، هذه هي فئات
UploadHandler و
DownloadHandler . توفر الوحدة نفسها مجموعة من امتدادات هذه الفئات للعمل مع البيانات المختلفة ، مثل الصوت ، والقوام ، والأصول ، إلخ. دعونا نفكر في العمل معهم بمزيد من التفاصيل.
العمل مع الموارد
النص
العمل مع النص هو واحد من أسهل الخيارات. تم بالفعل وصف طريقة تنزيله أعلاه. نعيد كتابته قليلاً باستخدام إنشاء طلب الحصول على http مباشرة.
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(); }
كما ترى من الكود ، يتم استخدام
DownloadHandler الافتراضي هنا. خاصية النص عبارة عن getter يحول صفيف بايت إلى نص مشفر UTF8. الاستخدام الرئيسي لتحميل النص من الخادم هو تلقي ملف json (التمثيل المتسلسل للبيانات في نموذج نصي). يمكنك الحصول على هذه البيانات باستخدام فئة Unity
JsonUtility .
var data = JsonUtility.FromJson<T>(value);
الصوت
للعمل مع الصوت ، يجب أن تستخدم الطريقة الخاصة لإنشاء طلب
UnityWebRequestMultimedia.GetAudioClip ،
وللحصول على تمثيل البيانات في النموذج الضروري للعمل في Unity ، يجب عليك استخدام
DownloadHandlerAudioClip . بالإضافة إلى ذلك ، عند إنشاء طلب ، يجب عليك تحديد نوع بيانات الصوت الممثلة في تعداد
AudioType ، والذي يحدد التنسيق (wav ، aiff ، oggvorbis ، إلخ).
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(); }
الملمس
يشبه تنزيل مواد تشبه تلك الخاصة بالملفات الصوتية. يتم إنشاء الطلب باستخدام
UnityWebRequestTexture.GetTexture . للحصول على البيانات في النموذج الضروري للوحدة ،
يتم استخدام
DownloadHandlerTexture .
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(); }
حزمة الأصول
كما ذكرنا سابقًا ، فإن الحزمة هي في الواقع أرشيف به موارد Unity يمكن استخدامها في لعبة قيد التشغيل بالفعل. يمكن أن تكون هذه الموارد أي أصول للمشروع ، بما في ذلك المشاهد. الاستثناء هو البرامج النصية C # ؛ لا يمكن تمريرها. لتحميل
AssetBundle ، يتم استخدام استعلام يتم إنشاؤه باستخدام
UnityWebRequestAssetBundle.GetAssetBundle. يستخدم
DownloadHandlerAssetBundle للحصول على البيانات بالشكل الضروري للوحدة.
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(); }
المشاكل والحلول الرئيسية عند العمل مع خادم الويب والبيانات الخارجية
تم وصف الطرق البسيطة للتفاعل بين التطبيق والخادم من حيث تحميل الموارد المختلفة أعلاه. ومع ذلك ، في الممارسة العملية ، الأمور أكثر تعقيدًا بكثير. فكر في المشكلات الرئيسية التي تصاحب المطورين واستكشف طرق حلها.
لا مساحة حرة كافية
تتمثل إحدى المشكلات الأولى عند تنزيل البيانات من الخادم في احتمال وجود مساحة خالية على الجهاز. غالبًا ما يحدث أن يستخدم المستخدم أجهزة قديمة للألعاب (خاصة على Android) ، كما أن حجم الملفات التي تم تنزيلها بحد ذاته كبير جدًا (مرحبًا بجهاز الكمبيوتر الشخصي). في أي حال ، يجب معالجة هذا الموقف بشكل صحيح ويجب إبلاغ اللاعب مقدمًا بأنه لا توجد مساحة كافية ومقدار المساحة. كيف نفعل ذلك؟ أول ما تحتاج إلى معرفته هو حجم الملف الذي تم تنزيله ، ويتم ذلك عن طريق طلب
UnityWebRequest.Head () . أدناه هو رمز للحصول على الحجم.
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); } }
من المهم الإشارة إلى شيء واحد هنا ، لكي يعمل الطلب بشكل صحيح ، يجب أن يكون الخادم قادراً على إرجاع حجم المحتوى ، وإلا (كما في الواقع ، لعرض التقدم) ، سيتم إرجاع القيمة الخطأ.
بعد أن نحصل على حجم البيانات التي تم تنزيلها ، يمكننا مقارنتها بحجم مساحة القرص الحرة. للحصول على الأخير ، أستخدم
المكوّن الإضافي المجاني من Asset Store .
ملاحظة :
يمكنك استخدام فئة ذاكرة التخزين المؤقت في Unity3d ، ويمكن أن تظهر مساحة تخزين حرة ومستخدمة. ومع ذلك ، تجدر الإشارة إلى أن هذه البيانات نسبية. يتم حسابها بناءً على حجم ذاكرة التخزين المؤقت نفسها ، افتراضيًا فهي 4 جيجابايت. إذا كان لدى المستخدم مساحة خالية أكبر من حجم ذاكرة التخزين المؤقت ، فلن تكون هناك مشاكل ، ولكن إذا لم يكن الأمر كذلك ، فقد تأخذ القيم قيمًا غير صحيحة بالنسبة للحالة الحقيقية للأمور.الاختيار الوصول إلى الإنترنت
في كثير من الأحيان ، قبل تنزيل أي شيء من الخادم ، من الضروري معالجة وضع عدم الوصول إلى الإنترنت. هناك عدة طرق للقيام بذلك: من اختبار الاتصال عبر عنوان ، إلى طلب GET إلى google.ru. ومع ذلك ، في رأيي ، فإن النتيجة الأكثر دقة وإعطاء سرعة وثبات هي تنزيل ملف صغير من الخادم الخاص بك (نفس الملف الذي سيتم تنزيل الملفات منه). تم شرح كيفية القيام بذلك أعلاه في القسم الخاص بالتعامل مع النص.
بالإضافة إلى التحقق من حقيقة وجود إمكانية الوصول إلى الإنترنت ، من الضروري أيضًا تحديد نوعه (جوال أو WiFi) ، لأنه من غير المحتمل أن يرغب المشغل في تنزيل عدة مئات من الميجابايت على حركة مرور الهاتف المحمول. يمكن القيام بذلك من خلال خاصية
Application.internetReachability .
التخزين المؤقت
التالي ، وأحد أهم المشكلات ، هو التخزين المؤقت للملفات التي تم تنزيلها. ما هو هذا التخزين المؤقت ل؟
- حفظ حركة المرور (لا تقم بتنزيل البيانات التي تم تنزيلها بالفعل)
- ضمان العمل في غياب الإنترنت (يمكنك عرض البيانات من ذاكرة التخزين المؤقت).
ما يجب أن يكون مؤقتا؟ الإجابة على هذا السؤال هي كل شيء ، يجب تخزين جميع الملفات التي قمت بتنزيلها مؤقتًا. كيفية القيام بذلك ، فكر في ما يلي ، وابدأ بملفات نصية بسيطة.
لسوء الحظ ، ليس لدى Unity آلية مضمنة للتخزين المؤقت للنص ، وكذلك القوام وملفات الصوت. لذلك ، من الضروري لهذه الموارد أن تكتب نظامك ، أو لا تكتب ، وفقًا لاحتياجات المشروع. في أبسط الحالات ، نكتب ببساطة الملف إلى ذاكرة التخزين المؤقت ، وفي غياب الإنترنت ، نأخذ الملف منه. في إصدار أكثر تعقيدًا قليلاً (استخدمه في المشاريع) ، نرسل طلبًا إلى الخادم ، والذي يعرض json للإشارة إلى إصدارات الملفات المخزنة على الخادم. يمكنك كتابة وقراءة الملفات من ذاكرة التخزين المؤقت باستخدام فئة C # لفئة
الملف أو بأي طريقة أخرى مريحة ومقبولة من قبل فريقك.
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); }
وبالمثل ، الحصول على البيانات من ذاكرة التخزين المؤقت.
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; }
ملاحظة :
لماذا لا يتم استخدام نفس UWR مع عنوان url لملف النموذج: // لتحميل القوام. في الوقت الحالي ، هناك مشاكل في هذا ، الملف ببساطة لا يتم تحميله ، لذلك اضطررت إلى إيجاد حل بديل.ملاحظة :
لا أستخدم التحميل المباشر لبرنامج AudioClip في المشروعات ، فأنا أخزن كل هذه البيانات في AssetBundle. ومع ذلك ، إذا لزم الأمر ، يمكن القيام بذلك بسهولة باستخدام وظائف الفئة AudioClip GetData و SetData.
موارد Unity البسيطة لـ
AssetBundle ، لدى Unity آلية تخزين مدمجة. لننظر في الأمر بمزيد من التفصيل.
في الأساس ، يمكن لهذه الآلية استخدام طريقتين:
- باستخدام اتفاقية حقوق الطفل ورقم الإصدار
- باستخدام قيم التجزئة
من حيث المبدأ ، يمكنك استخدام أي منها ، لكنني قررت بنفسي أن هاش هو الأكثر قبولا ، لأن لدي نظام الإصدار الخاص بي ويأخذ في الاعتبار ليس فقط إصدار
AssetBundle ، ولكن أيضا إصدار التطبيق ، لأنه في كثير من الأحيان قد لا تكون الحزمة متوافقة مع الإصدار ، المقدمة في المتاجر.
لذلك ، كيف يتم التخزين المؤقت القيام به:
- نطلب ملف حزمة من خادم البيان (يتم إنشاء هذا الملف تلقائيًا عند إنشائه ويحتوي على وصف للأصول التي يحتوي عليها ، بالإضافة إلى التجزئة ، و crc ، والحجم ، إلخ). الملف له نفس اسم الحزمة بالإضافة إلى ملحق البيان.
- الحصول على قيمة hash128 من البيان
- نقوم بإنشاء طلب إلى الخادم للحصول على AssetBundle ، حيث بالإضافة إلى عنوان url ، حدد قيمة hash128 المستلمة
رمز الخوارزمية الموضحة أعلاه: IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response) {
في المثال أعلاه ، يبحث Unity ، بناءً على طلب إلى الخادم ، أولاً لمعرفة ما إذا كان هناك ملف في ذاكرة التخزين المؤقت بقيمة hash128 المحددة ، إذا كان كذلك ، فسيتم إرجاعه ؛ وإذا لم يكن الأمر كذلك ، فسيتم تنزيل الملف الذي تم تحديثه. لإدارة جميع ملفات ذاكرة التخزين المؤقت في الوحدة ، توجد فئة ذاكرة
التخزين المؤقت ، والتي يمكننا من خلالها معرفة ما إذا كان هناك ملف في ذاكرة التخزين المؤقت ، أو الحصول على جميع الإصدارات المخزنة مؤقتًا ، وكذلك حذف النسخ غير الضرورية ، أو محوها تمامًا.
ملاحظة :
لماذا هذه الطريقة الغريبة للحصول على قيم التجزئة؟ هذا يرجع إلى حقيقة أن الحصول على hash128 بالطريقة الموضحة في الوثائق يتطلب تحميل الحزمة بالكامل ، ثم استلام أصل AssetBundleManifest منه ومن هناك بالفعل قيم hash. الجانب السلبي لهذا النهج هو أن AssetBundle بأكمله يتأرجح ، لكننا نحتاج فقط أن لا يكون. لذلك ، نقوم أولاً بتنزيل ملف البيان فقط من الخادم ، ونأخذ منه هاش 128 ، وبعد ذلك فقط ، إذا لزم الأمر ، نزّل ملف الحزمة ، ويجب سحب قيمة hash128 من خلال تفسير الخطوط.العمل مع الموارد في وضع المحرر
المشكلة الأخيرة ، أو بالأحرى مسألة تصحيح الأخطاء وراحة التطوير ، هي العمل مع الموارد القابلة للتنزيل في وضع المحرر ، إذا لم تكن هناك مشاكل مع الملفات العادية ، فإن الأمور ليست بهذه البساطة في الحزم. بالطبع ، يمكنك إنشاء بنائها في كل مرة ، وتحميله على الخادم وإطلاق التطبيق في محرر Unity ومشاهدة كيف يعمل كل شيء ، ولكن حتى هذا الوصف يبدو وكأنه "عكاز". هناك حاجة إلى القيام بشيء ما لهذا
الغرض ، وسوف تساعدنا فئة
AssetDatabase .
من أجل توحيد العمل مع الحزم ، صنعت غلافًا خاصًا:
public class AssetBundleWrapper { private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } }
نحتاج الآن إلى إضافة وضعين للعمل مع الأصول ، اعتمادًا على ما إذا كنا في المحرر أو في البنية. بالنسبة للبناء ، نستخدم الأغلفة على وظائف فئة
AssetBundle ، وبالنسبة للمحرر نستخدم فئة
AssetDatabase المذكورة أعلاه.
وبالتالي ، نحصل على الكود التالي: 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 }
ملاحظة : يستخدم
الكود فئة
TaskManager ، وسيتم مناقشته أدناه ، باختصار ، هذا ملف
مغلف للعمل مع
Coroutine .
بالإضافة إلى ما سبق ، من المفيد أيضًا أثناء التطوير النظر إلى ما قمنا بتنزيله وما يوجد حاليًا في ذاكرة التخزين المؤقت. لهذا الغرض ، يمكنك الاستفادة من القدرة على تعيين المجلد الخاص بك ، والذي سيتم استخدامه للتخزين المؤقت (يمكنك أيضًا كتابة النص الذي تم تنزيله والملفات الأخرى إلى نفس المجلد):
#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);
نكتب مدير طلب الشبكة أو نعمل مع خادم ويب
أعلاه درسنا الجوانب الرئيسية للعمل مع الموارد الخارجية في الوحدة ، والآن أود أن أتناول تنفيذ API ، الذي يعمم ويوحد كل ما سبق. أولاً ، دعنا نتحدث عن مدير استعلام الشبكة.
ملاحظة :
فيما يلي ، نستخدم المجمع على Coroutine كفئة من TaskManager . كتبت عن هذا المجمع في مقال آخر .دعنا نحصل على الفصل المقابل:
public class Network { public enum NetworkTypeEnum { None, Mobile, WiFi } public static NetworkTypeEnum NetworkType; private readonly TaskManager _taskManager = new TaskManager(); }
حقل ثابت
NetworkType مطلوب للتطبيق لتلقي معلومات حول نوع اتصال الإنترنت. من حيث المبدأ ، يمكن تخزين هذه القيمة في أي مكان ، قررت أنه هو المكان في فئة
الشبكة .
أضف الوظيفة الأساسية لإرسال طلب إلى الخادم: 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(); }
كما ترى من الكود ، فإن طريقة معالجة إتمام الطلب قد تغيرت مقارنةً بالكود في الأقسام السابقة. هذا هو إظهار التقدم في تحميل البيانات. أيضًا ، يتم تخزين جميع الطلبات المرسلة في قائمة بحيث ، إذا لزم الأمر ، يمكن إلغاؤها.
إضافة وظيفة إنشاء استعلام يستند إلى الارتباط لـ 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); }
وبالمثل ، يتم إنشاء وظائف للنسيج ، والصوت ، والنص ، ومجموعة البايت.
أنت الآن بحاجة إلى التأكد من أن الخادم يرسل البيانات من خلال أمر النشر. غالبًا ما تحتاج إلى نقل شيء إلى الخادم ، واعتمادًا على ما هو بالضبط ، احصل على إجابة. أضف الوظائف المناسبة.
إرسال البيانات في شكل مجموعة قيمة مفتاح: 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); }
إرسال البيانات كـ 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); }
سنضيف الآن الأساليب العامة التي سنقوم بتحميل البيانات بها ، وخاصة 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); }
وبالمثل ، يتم إضافة طرق للملمس ، وملف الصوت ، والنص ، إلخ.
وأخيرًا ، نضيف وظيفة الحصول على حجم الملف الذي تم تنزيله ووظيفة التنظيف لإيقاف جميع الطلبات التي تم إنشاؤها. 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(); }
على هذا ، اكتمال مديرنا للعمل مع طلبات الشبكة. إذا لزم الأمر ، يمكن لكل نظام فرعي للعبة يتطلب العمل مع الخادم إنشاء مثيلات خاصة به من الفصل.
نكتب مدير تحميل الموارد الخارجية
بالإضافة إلى الفصل الموضح أعلاه ، للعمل بشكل كامل مع البيانات الخارجية ، نحتاج إلى مدير منفصل لن يقوم بتنزيل البيانات فقط ، ولكن أيضًا يخطر التطبيق ببدء التحميل والإكمال والتقدم ونقص المساحة الحرة وأيضًا التعامل مع مشكلات التخزين المؤقت.
نبدأ الفصل المقابل ، وهو في حالتي مفردة 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); } }
كما ترى ، يقوم المصمم بتعيين المجلد للتخزين المؤقت ، اعتمادًا على ما إذا كنا في المحرر أم لا. أيضًا ، أنشأنا حقلًا خاصًا لمثيل فئة الشبكة ، والذي وصفناه سابقًا.سنقوم الآن بإضافة وظائف مساعدة للعمل مع ذاكرة التخزين المؤقت ، وكذلك تحديد حجم الملف الذي تم تنزيله والتحقق من المساحة الخالية له. علاوة على ذلك ، أدناه ، يتم تقديم الكود على مثال للعمل مع AssetBundle ، بالنسبة لبقية الموارد ، يتم كل شيء عن طريق القياس.مساعد رمز الوظيفة 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; }
الآن ، دعونا نضيف وظائف تحميل البيانات باستخدام مثال 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 }
إذن ما يحدث في هذه الوظيفة:- يتم استخدام توجيه التجميع المسبق DONT_USE_SERVER_IN_EDITOR لتعطيل التحميل الفعلي للحزم من الخادم.
- الخطوة الأولى هي تقديم طلب إلى الخادم للحصول على ملف البيان الخاص بالباقة
- - , , - ( _externalResourcesStorage ) , , ( , ), , null
- , Caching , , ( )
- , , , , - ( ). , ( )
ملاحظة : من المهم أن نفهم لماذا يتم تخزين قيمة التجزئة بشكل منفصل. يعد هذا ضروريًا للحالة التي لا يوجد فيها إنترنت ، أو أن الاتصال غير مستقر ، أو كان هناك خطأ ما في الشبكة ولم نتمكن من تنزيل الحزمة من الخادم ، وفي هذه الحالة نضمن تنزيل الحزمة من ذاكرة التخزين المؤقت في حالة وجودها .على غرار الطريقة الموضحة أعلاه ، في المدير ، يمكنك / تحتاج إلى الحصول على وظائف أخرى للعمل مع البيانات: GetJson ، GetTexture ، GetText ، GetAudio ، إلخ.وأخيرًا ، تحتاج إلى الحصول على طريقة تسمح لك بتنزيل مجموعات الموارد. ستكون هذه الطريقة مفيدة إذا كنا بحاجة إلى تنزيل أو تحديث شيء ما في بداية التطبيق. 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; } } }
TaskManager , , , . .
: ,
Coroutine ,
async/await , , ( ).
الخاتمة
. , . , ( f2p ), , , , , .
, :
assetstore.unity.com/packages/tools/simple-disk-utils-59382habr.com/post/352296habr.com/post/282524