Einführung
Hallo liebe Leser, heute werden wir über die Arbeit mit externen Ressourcen in der Unity 3d-Umgebung sprechen.
Aus Tradition werden wir zunächst bestimmen, was es ist und warum wir es brauchen. Was genau sind diese externen Ressourcen? Im Rahmen der Spieleentwicklung können solche Ressourcen alles sein, was für das Funktionieren der Anwendung erforderlich ist, und sollten nicht im endgültigen Build des Projekts gespeichert werden. Externe Ressourcen können sich sowohl auf der Festplatte des Computers des Benutzers als auch auf einem externen Webserver befinden. Im Allgemeinen handelt es sich bei solchen Ressourcen um alle Dateien oder Datensätze, die wir in unsere bereits ausgeführte Anwendung laden. Wenn sie im Rahmen von Unity 3d sprechen, können sie sein:
- Textdatei
- Texturdatei
- Audiodatei
- Byte-Array
- AssetBundle (Archiv mit Assets des Unity 3d-Projekts)
Im Folgenden werden wir die integrierten Mechanismen für die Arbeit mit diesen in Unity 3d vorhandenen Ressourcen genauer untersuchen sowie einfache Manager für die Interaktion mit dem Webserver und das Laden von Ressourcen in die Anwendung schreiben.
Hinweis : Der
Rest dieses Artikels verwendet Code mit C # 7+ und wurde für den Roslyn-Compiler entwickelt, der in Unity3d in den Versionen 2018.3+ verwendet wird.Merkmale von Unity 3d
Vor der Unity-Version 2017 wurde ein Mechanismus (außer selbst beschrieben) verwendet, um mit Serverdaten und externen Ressourcen zu arbeiten, die in der Engine enthalten waren - dies ist die WWW-Klasse. Diese Klasse ermöglichte die Verwendung verschiedener http-Befehle (get, post, put usw.) in synchroner oder asynchroner Form (über Coroutine). Die Arbeit mit dieser Klasse war recht einfach und unkompliziert.
IEnumerator LoadFromServer(string url) { var www = new WWW(url); yield return www; Debug.Log(www.text); }
Ebenso können Sie nicht nur Textdaten abrufen, sondern auch andere:
Ab Version 2017 verfügt Unity jedoch über ein neues Serversystem, das von der
UnityWebRequest- Klasse eingeführt wurde und sich im Netzwerk-Namespace befindet. Bis Unity 2018 existierte es zusammen mit dem
WWW , aber in der neuesten Version der
WWW- Engine wurde es nicht empfohlen und wird in Zukunft vollständig entfernt. Daher konzentrieren wir uns weiter nur auf
UnityWebRequest (im Folgenden: UWR).
Die Arbeit mit UWR als Ganzes ähnelt im Kern dem WWW, es gibt jedoch Unterschiede, die später erörtert werden. Unten finden Sie ein ähnliches Beispiel für das Laden von Text.
IEnumerator LoadFromServer(string url) { var request = new UnityWebRequest(url); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); request.Dispose(); }
Die wichtigsten Änderungen, die das neue UWR-System eingeführt hat (zusätzlich zur Änderung des Prinzips der Arbeit im Inneren), sind die Möglichkeit, Handler zum Hoch- und Herunterladen von Daten vom Server selbst zuzuweisen. Weitere Details finden Sie
hier . Standardmäßig sind dies die Klassen
UploadHandler und
DownloadHandler . Unity selbst bietet eine Reihe von Erweiterungen dieser Klassen für die Arbeit mit verschiedenen Daten wie Audio, Texturen, Assets usw. Lassen Sie uns genauer mit ihnen arbeiten.
Mit Ressourcen arbeiten
Text
Das Arbeiten mit Text ist eine der einfachsten Optionen. Die Methode zum Herunterladen wurde bereits oben beschrieben. Wir schreiben es ein wenig um, indem wir eine direkte http Get-Anfrage erstellen.
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(); }
Wie Sie dem Code
entnehmen können, wird hier der Standard-
DownloadHandler verwendet. Die text-Eigenschaft ist ein Getter, der ein Byte-Array in UTF8-codierten Text konvertiert. Das Laden von Text vom Server wird hauptsächlich zum Empfangen einer JSON-Datei verwendet (serialisierte Darstellung der Daten in Textform). Sie können diese Daten mit der Unity
JsonUtility- Klasse
abrufen .
var data = JsonUtility.FromJson<T>(value);
Audio
Um mit Audio zu arbeiten, müssen Sie die spezielle Methode zum Erstellen der
UnityWebRequestMultimedia.GetAudioClip- Anforderung verwenden. Um die Datendarstellung in der für die Arbeit in Unity erforderlichen Form zu erhalten, müssen Sie
DownloadHandlerAudioClip verwenden . Darüber hinaus müssen Sie beim Erstellen einer Anforderung den Typ der Audiodaten angeben, die durch die
AudioType- Aufzählung dargestellt werden, die das Format festlegt (wav, aiff, oggvorbis usw.).
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(); }
Textur
Das Herunterladen von Texturen ähnelt dem für Audiodateien. Die Anforderung wird mit
UnityWebRequestTexture.GetTexture erstellt . Um Daten in der für Unity erforderlichen Form abzurufen, wird
DownloadHandlerTexture verwendet.
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(); }
Asset-Bundle
Wie bereits erwähnt, handelt es sich bei dem Bundle tatsächlich um ein Archiv mit Unity-Ressourcen, das in einem bereits laufenden Spiel verwendet werden kann. Diese Ressourcen können beliebige Projektressourcen sein, einschließlich Szenen. Die Ausnahme bilden C # -Skripte, die nicht übergeben werden können. Zum Laden des
AssetBundle wird eine Abfrage verwendet, die mit
UnityWebRequestAssetBundle.GetAssetBundle erstellt wird. DownloadHandlerAssetBundle wird verwendet, um Daten in der für Unity erforderlichen Form abzurufen.
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(); }
Die Hauptprobleme und Lösungen bei der Arbeit mit einem Webserver und externen Daten
Oben wurden einfache Methoden zur Interaktion zwischen einer Anwendung und einem Server hinsichtlich des Ladens verschiedener Ressourcen beschrieben. In der Praxis sind die Dinge jedoch viel komplizierter. Betrachten Sie die Hauptprobleme, die Entwickler begleiten, und überlegen Sie, wie Sie sie lösen können.
Nicht genug freier Speicherplatz
Eines der ersten Probleme beim Herunterladen von Daten vom Server ist ein möglicher Mangel an freiem Speicherplatz auf dem Gerät. Es kommt häufig vor, dass der Benutzer alte Geräte für Spiele verwendet (insbesondere unter Android), und dass die heruntergeladenen Dateien selbst sehr groß sein können (Hallo PC). In jedem Fall muss diese Situation korrekt verarbeitet werden und der Spieler muss im Voraus darüber informiert werden, dass nicht genügend Platz vorhanden ist und wie viel. Wie kann man das machen? Das erste, was Sie wissen müssen, ist die Größe der heruntergeladenen Datei. Dies erfolgt mithilfe der
Anforderung UnityWebRequest.Head () . Unten ist der Code, um die Größe zu erhalten.
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); } }
Es ist wichtig, Folgendes zu beachten: Damit die Anforderung ordnungsgemäß funktioniert, muss der Server in der Lage sein, die Größe des Inhalts zurückzugeben. Andernfalls wird (um tatsächlich den Fortschritt anzuzeigen) der falsche Wert zurückgegeben.
Nachdem wir die Größe der heruntergeladenen Daten erhalten haben, können wir sie mit der Größe des freien Speicherplatzes vergleichen. Um letzteres zu bekommen, benutze ich das
kostenlose Plugin aus dem Asset Store .
Hinweis :
Sie können die Cache- Klasse in Unity3d verwenden. Sie kann freien und verwendeten Cache-Speicherplatz anzeigen. Es ist jedoch zu berücksichtigen, dass diese Daten relativ sind. Sie werden basierend auf der Größe des Caches selbst berechnet, standardmäßig sind es 4 GB. Wenn der Benutzer mehr freien Speicherplatz als die Größe des Caches hat, gibt es keine Probleme. Ist dies jedoch nicht der Fall, können die Werte Werte annehmen, die im Verhältnis zum tatsächlichen Stand der Dinge falsch sind.Überprüfung des Internetzugangs
Sehr oft muss vor dem Herunterladen von Daten vom Server die Situation eines fehlenden Internetzugangs behoben werden. Es gibt verschiedene Möglichkeiten, dies zu tun: vom Pingen einer Adresse bis zu einer GET-Anfrage an google.ru. Meiner Meinung nach ist das Herunterladen einer kleinen Datei von Ihrem eigenen Server (dem gleichen, von dem die Dateien heruntergeladen werden) das korrekteste und schnellste und stabilste Ergebnis. Wie das geht, ist oben im Abschnitt über das Arbeiten mit Text beschrieben.
Zusätzlich zur Überprüfung der Tatsache, dass ein Internetzugang vorhanden ist, muss auch der Typ (mobil oder WLAN) ermittelt werden, da es unwahrscheinlich ist, dass ein Spieler mehrere hundert Megabyte im mobilen Datenverkehr herunterladen möchte. Dies kann über die
Application.internetReachability- Eigenschaft erfolgen.
Caching
Das nächste und eines der wichtigsten Probleme ist das Zwischenspeichern heruntergeladener Dateien. Wofür ist dieses Caching?
- Verkehr speichern (nicht heruntergeladene Daten nicht herunterladen)
- Sicherstellen, dass auch ohne Internet gearbeitet wird (Sie können Daten aus dem Cache anzeigen).
Was muss zwischengespeichert werden? Die Antwort auf diese Frage ist alles, alle Dateien, die Sie herunterladen, müssen zwischengespeichert werden. Wie das geht, lesen Sie weiter unten und beginnen Sie mit einfachen Textdateien.
Leider verfügt Unity nicht über einen integrierten Mechanismus zum Zwischenspeichern von Text sowie Texturen und Audiodateien. Daher ist es für diese Ressourcen abhängig von den Anforderungen des Projekts erforderlich, Ihr System zu schreiben oder nicht zu schreiben. Im einfachsten Fall schreiben wir die Datei einfach in den Cache und nehmen die Datei aus dem Internet, wenn kein Internet vorhanden ist. In einer etwas komplexeren Version (ich verwende sie in Projekten) senden wir eine Anfrage an den Server, die json zurückgibt und die Versionen der Dateien angibt, die auf dem Server gespeichert sind. Sie können Dateien aus dem Cache schreiben und lesen, indem Sie die C # -Klasse der
File- Klasse verwenden oder auf andere Weise, die von Ihrem Team bequem und akzeptiert wird.
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); }
Ebenso Daten aus dem Cache abrufen.
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; }
Hinweis :
Warum dasselbe UWR mit einer URL der Formulardatei: // nicht zum Laden von Texturen verwendet wird. Im Moment gibt es Probleme damit, die Datei wird einfach nicht geladen, also musste ich eine Problemumgehung finden.Hinweis :
Ich verwende das direkte Laden von AudioClip nicht in Projekten, sondern speichere alle diese Daten in AssetBundle. Bei Bedarf kann dies jedoch problemlos mit den Funktionen der AudioClip-Klassen GetData und SetData durchgeführt werden.
Unitys einfache Ressourcen für
AssetBundle . Unity verfügt über einen integrierten Caching-Mechanismus. Betrachten wir es genauer.
Grundsätzlich kann dieser Mechanismus zwei Ansätze verwenden:
- Verwendung von CRC und Versionsnummer
- Hash-Werte verwenden
Grundsätzlich können Sie jede davon verwenden, aber ich habe selbst entschieden, dass Hash am akzeptabelsten ist, da ich ein eigenes Versionssystem habe und nicht nur die
AssetBundle- Version, sondern auch die Version der Anwendung berücksichtigt, da das Bundle häufig nicht mit der Version kompatibel ist. in Geschäften präsentiert.
Wie wird das Caching durchgeführt?
- Wir fordern eine Bundle-Datei vom Manifest-Server an (diese Datei wird beim Erstellen automatisch erstellt und enthält eine Beschreibung der darin enthaltenen Assets sowie Hash, CRC, Größe usw.). Die Datei hat denselben Namen wie das Bundle und die Erweiterung .manifest.
- Ruft den Hash128-Wert aus dem Manifest ab
- Wir erstellen eine Anfrage an den Server, um ein AssetBundle zu erhalten, in dem zusätzlich zur URL der empfangene Hash128-Wert angegeben wird
Code für den oben beschriebenen Algorithmus: IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response) {
Im obigen Beispiel prüft Unity auf Anfrage an den Server zunächst, ob sich eine Datei mit dem angegebenen Hash128-Wert im Cache befindet. Wenn dies der Fall ist, wird sie zurückgegeben. Andernfalls wird die aktualisierte Datei heruntergeladen. Um alle Cache-Dateien in Unity zu verwalten, gibt es eine
Caching- Klasse, mit der wir herausfinden können, ob sich eine Datei im Cache befindet, alle zwischengespeicherten Versionen abrufen sowie unnötige löschen oder vollständig löschen können.
Hinweis :
Warum so eine seltsame Art, Hash-Werte zu erhalten? Dies liegt an der Tatsache, dass zum Abrufen von Hash128 auf die in der Dokumentation beschriebene Weise das Laden des gesamten Bundles und das anschließende Empfangen des AssetBundleManifest- Assets von diesem und von dort bereits vorhandenen Hash-Werten erforderlich sind. Der Nachteil dieses Ansatzes ist, dass das gesamte AssetBundle schwingt, aber wir brauchen es einfach nicht. Daher laden wir zuerst nur die Manifestdatei vom Server herunter, nehmen Hash128 von ihm und laden dann, falls erforderlich, die Bundle-Datei herunter, und der Hash128-Wert muss durch die Interpretation der Zeilen herausgezogen werden.Arbeiten Sie mit Ressourcen im Editor-Modus
Das letzte Problem, oder besser gesagt das Problem des Debuggens und der Bequemlichkeit der Entwicklung, ist das Arbeiten mit herunterladbaren Ressourcen im Editor-Modus. Wenn es keine Probleme mit regulären Dateien gibt, sind die Dinge mit Bundles nicht so einfach. Natürlich können Sie sie jedes Mal erstellen, auf den Server hochladen und die Anwendung im Unity-Editor starten und beobachten, wie alles funktioniert, aber selbst diese Beschreibung klingt wie eine „Krücke“. Damit muss etwas getan werden, und die
AssetDatabase- Klasse wird uns helfen.
Um die Arbeit mit Bundles zu vereinheitlichen, habe ich einen speziellen Wrapper erstellt:
public class AssetBundleWrapper { private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } }
Jetzt müssen wir zwei Modi für die Arbeit mit Assets hinzufügen, je nachdem, ob wir uns im Editor oder im Build befinden. Für den Build verwenden wir Wrapper für die Funktionen der
AssetBundle- Klasse, und für den Editor verwenden wir die
oben erwähnte
AssetDatabase- Klasse.
Somit erhalten wir den folgenden Code: 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 }
Hinweis : Der Code verwendet die
TaskManager- Klasse.
Dies wird im Folgenden
erläutert . Kurz gesagt, dies ist ein Wrapper für die Arbeit mit
Coroutine .
Darüber hinaus ist es während der Entwicklung hilfreich, zu prüfen, was wir heruntergeladen haben und was sich derzeit im Cache befindet. Zu diesem Zweck können Sie die Möglichkeit nutzen, einen eigenen Ordner festzulegen, der zum Zwischenspeichern verwendet wird (Sie können auch heruntergeladenen Text und andere Dateien in denselben Ordner schreiben):
#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);
Wir schreiben einen Netzwerkanforderungsmanager oder arbeiten mit einem Webserver
Oben haben wir die Hauptaspekte der Arbeit mit externen Ressourcen in Unity untersucht. Jetzt möchte ich auf die Implementierung der API eingehen, die alle oben genannten Punkte verallgemeinert und vereinheitlicht. Lassen Sie uns zunächst auf den Netzwerkabfrage-Manager eingehen.
Hinweis : Im
Folgenden verwenden wir den Wrapper über Coroutine in Form der TaskManager- Klasse. Ich habe in einem anderen Artikel über diesen Wrapper geschrieben .Lassen Sie uns die entsprechende Klasse erhalten:
public class Network { public enum NetworkTypeEnum { None, Mobile, WiFi } public static NetworkTypeEnum NetworkType; private readonly TaskManager _taskManager = new TaskManager(); }
Das statische Feld
NetworkType ist erforderlich, damit die Anwendung Informationen zum Typ der Internetverbindung erhält. Im Prinzip kann dieser Wert überall gespeichert werden. Ich habe entschieden, dass dies der Ort in der
Netzwerkklasse ist .
Fügen Sie die Grundfunktion zum Senden einer Anforderung an den Server hinzu: 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(); }
Wie Sie dem Code entnehmen können, hat sich die Methode zur Verarbeitung des Abschlusses einer Anforderung im Vergleich zum Code in den vorherigen Abschnitten geändert. Dies soll den Fortschritt des Datenladens anzeigen. Außerdem werden alle gesendeten Anforderungen in einer Liste gespeichert, sodass sie bei Bedarf storniert werden können.
Fügen Sie eine linkbasierte Abfrageerstellungsfunktion für AssetBundle hinzu: 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); }
In ähnlicher Weise werden Funktionen für Textur, Audio, Text und Byte-Array erstellt.
Jetzt müssen Sie sicherstellen, dass der Server Daten über den Befehl Post sendet. Oft müssen Sie etwas an den Server übergeben und je nachdem, was genau, eine Antwort erhalten. Fügen Sie die entsprechenden Funktionen hinzu.
Senden von Daten in Form eines Schlüsselwertsatzes: 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); }
Senden von Daten als 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); }
Jetzt werden wir öffentliche Methoden hinzufügen, mit denen wir Daten laden, insbesondere 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); }
In ähnlicher Weise werden Methoden für Textur, Audiodatei, Text usw. hinzugefügt.
Und schließlich fügen wir die Funktion zum Abrufen der Größe der heruntergeladenen Datei und die Bereinigungsfunktion hinzu, um alle erstellten Anforderungen zu stoppen. 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(); }
Damit ist unser Manager für die Arbeit mit Netzwerkanfragen fertig. Bei Bedarf kann jedes Subsystem des Spiels, das die Arbeit mit dem Server erfordert, seine eigenen Instanzen der Klasse erstellen.
Wir schreiben den Manager für das Laden externer Ressourcen
Zusätzlich zu der oben beschriebenen Klasse benötigen wir einen separaten Manager, der nicht nur die Daten herunterlädt, sondern die Anwendung auch über den Beginn des Downloads, den Abschluss, den Fortschritt, den Mangel an freiem Speicherplatz und auch über Caching-Probleme informiert, um vollständig mit externen Daten arbeiten zu können.
Wir starten die entsprechende Klasse, die in meinem Fall ein Singleton ist 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); } }
Wie Sie sehen können, legt der Designer den Ordner für das Caching fest, je nachdem, ob wir uns im Editor befinden oder nicht. Außerdem haben wir ein privates Feld für eine Instanz der Network-Klasse eingerichtet, die wir zuvor beschrieben haben.Jetzt werden wir Zusatzfunktionen für die Arbeit mit dem Cache hinzufügen, die Größe der heruntergeladenen Datei bestimmen und den freien Speicherplatz dafür überprüfen. Weiter und weiter unten wird der Code anhand eines Beispiels für die Arbeit mit AssetBundle angegeben. Für den Rest der Ressourcen erfolgt alles analog.Hilfsfunktionscode 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; }
Fügen wir nun anhand des AssetBundle-Beispiels Funktionen zum Laden von Daten hinzu. 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 }
Was passiert also in dieser Funktion:- Die Vorkompilierungsanweisung DONT_USE_SERVER_IN_EDITOR wird verwendet, um das tatsächliche Laden von Bundles vom Server zu deaktivieren.
- Der erste Schritt besteht darin, eine Anfrage an den Server zu richten, um die Manifestdatei für das Bundle abzurufen
- - , , - ( _externalResourcesStorage ) , , ( , ), , null
- , Caching , , ( )
- , , , , - ( ). , ( )
Hinweis : Es ist wichtig zu verstehen, warum der Hashwert separat gespeichert wird. Dies ist erforderlich, wenn kein Internet vorhanden ist oder die Verbindung instabil ist oder ein Netzwerkfehler aufgetreten ist und wir das Bundle nicht vom Server herunterladen konnten. In diesem Fall garantieren wir, dass das Bundle aus dem Cache heruntergeladen wird, wenn es dort vorhanden ist .Ähnlich wie bei der oben beschriebenen Methode können / müssen Sie im Manager andere Funktionen für die Arbeit mit Daten abrufen: GetJson, GetTexture, GetText, GetAudio usw.Und schließlich benötigen Sie eine Methode, mit der Sie Ressourcensätze herunterladen können. Diese Methode ist nützlich, wenn wir zu Beginn der Anwendung etwas herunterladen oder aktualisieren müssen. 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; } } }
Hier lohnt es sich, die Besonderheit des TaskManagers zu verstehen , der im Netzwerkanforderungsmanager verwendet wird. Standardmäßig funktioniert er und führt alle Aufgaben nacheinander aus. Daher erfolgt der Download der Dateien entsprechend.Hinweis : Für diejenigen, die Coroutine nicht mögen , kann alles leicht in async / await übersetzt werden. In diesem Fall habe ich mich jedoch für eine verständlichere Option für Anfänger entschieden (wie es mir scheint).Fazit
In diesem Artikel habe ich versucht, die Arbeit mit externen Ressourcen von Spieleanwendungen so kompakt wie möglich zu beschreiben. Dieser Ansatz und Code wird in Projekten verwendet, die veröffentlicht wurden und unter meiner Teilnahme entwickelt werden. Es ist recht einfach und in einfachen Spielen anwendbar, in denen keine ständige Kommunikation mit dem Server besteht (MMO und andere komplexe f2p-Spiele), aber es erleichtert die Arbeit erheblich, wenn wir zusätzliche Materialien, Sprachen, die serverseitige Validierung von Einkäufen und andere Daten herunterladen müssen einmalig oder nicht zu oft in der Anwendung verwendet.Im Artikel bereitgestellte Links :assetstore.unity.com/packages/tools/simple-disk-utils-59382habr.com/post/352296habr.com/post/282524