… mostly!
Wer auf dem Entwickler-Meeting letzten Montag dem 24.07. war (wiederholt sich jeden Montag!) der hat vielleicht geahnt was da bald kommen könnte.
Wir haben in der letzten Woche bei unserem internen Hackathon unter anderem daran gearbeitet eine UserApp zu bauen die wir größtenteils mit Unity3D umsetzen. Da die App aber trotzdem, wie alle Apps mit HTML-UI, im jeweiligen Browser des Clients laufen muss muss sie natürlich in eine kleine Webseite eingebettet sein. Daraus folgt natürlich, dass die Schnittstellen zwischen “dem was Unity ausspuckt” und der Knuddels-API geschaffen werden müssen, denn beide kennen sich natürlich nicht.
Unsere App mit der wir das realisiert haben ist
Untergrund Hermann.
Die App basiert auf der
Unity3D Spieleengine. Neben Windows, Linux und Mac kann Unity auch einen WebGL/asm.js Player erzeugen, der es erlaubt, das entwickelte Spiel auf einer Website einzubetten.
WebGL ist eine relativ neue Technologie, die es erlaubt, native Grafikbeschleuniger über eine JavaScript-Variante von OpenGL anzusprechen.
asm.js erlaubt es, für native Umgebungen entwickelten Code - wie zum Beispiel C oder C# - in einer JavaScript Umgebung auszuführen.
Der Browser muss dann lediglich WebGL unterstützen, was heutzutage bei praktisch jedem Browser der Fall ist. - Da unser Standalone App leider noch eine ältere Technologie einsetzt und demnach WebGL fehlt, laufen dort keine Unity Spiele.
Ein generisches Unity Spiel als UserApp zu veröffentlichen, ist also weder besonders komplex noch aufwändig… in der Theorie jedenfalls.
Leider hat das UserApp System ein paar gemeine Sonderheiten. Zum Beispiel überschreiben wir
URL, was Unity intern zum nachladen von Spielinhalten nutzt. Glücklicherweise können wir unsere eigenen JavaScript Dateien schnell anpassen. Der Übeltäter überschreibt nun zwar immernoch URL, stellt in window._URL aber das ursprüngliche Objekt bereit. Damit kann in der von Unity generierten HTML Seite ein kleines Schnippsel eingebunden werden, das _URL wieder zurück in URL kopiert.
Es muss also die Vorlage der von Unity generierten index.html angepasst werden. Unity bietet dafür
benutzerdefinierte Templates. Damit kann das folgende Schnippsel leicht eingebunden werden, welches alle von Unity benötigten Funktionen des URL Objekts wiederherstellt. Stellt sicher, dass das Schnippsel eingebunden wird, bevor “UnityLoader.instantiate” aufgerufen wird.
<script>
URL.createObjectURL = _URL.createObjectURL;
URL.revokeObjectURL = _URL.revokeObjectURL;
</script>
Damit kann nun ein generisches Unity Spiel als UserApp veröffentlicht werden.
Wir wollten allerdings noch einen Schritt weiter gehen und auch die Vorteile unserer UserApps API (wie z.b. Serverkommunikation) verwenden. Das bedeutet also, dass wir zum einen aus Unity’s C# Code heraus JavaScript im Browser aufrufen und zum anderen aus dem Browser Daten zurück in den C# Code schicken müssen.
Auch dafür bietet Unity zum Glück schon eine
Lösung. Die Dokumentation verschweigt leider einige wichtige Limitationen, der Interaktions-API - zum Beispiel, dass man ausschliesslich einzelne Strings und Zahlen als Methodenparameter übergeben kann. Um euch vor viel Trail-and-Error Zeit zu bewahren, hier unsere ‘KnuddelsApi.jslib’, die die JavaScript Schnittstelle für den C# Code definiert.
var KnuddelsApi = {
Client_addEventListener: function (pType) {
var type = Pointer_stringify(pType);
console.log("Registered event handler for " + type);
Client.addEventListener(type, function (payload) {
console.log("(JS) Received server event " + JSON.stringify(payload));
SendMessage("_Connector", "OnEvent", payload.data);
});
},
Client_sendEvent: function (pData) {
var jsonPayload = Pointer_stringify(pData);
console.log("Sending event with payload " + jsonPayload);
Client.sendEvent("onData", JSON.parse(jsonPayload));
}
};
mergeInto(LibraryManager.library, KnuddelsApi);
Im C# Code können dann die Funktionen Client_addEventListener(string) und Client_sendEvent(string) importiert werden.
Da unser Demoprojekt nur die Event-API der UserApp nutzt, reichen uns diese beiden Funktionen. Möchte man auch noch Zugriff auf die HostFrame-API, usw., muss die jslib Datei natürlich angepasst werden.
Um Events vom Server zu empfangen, müssen Callbacks in Client.addEventListener registriert werden. Aus der oben genannten Parameterrestriktion ergibt sich allerdings ein Problem. Ein Callback ist weder ein String noch eine Zahl. Unser Workaround für dieses Problem sieht so aus:
Das Spiel hat ein eigenes Mapping von Eventtyp auf Callback. Der UserApp API wird in addEventListener dann ein generisches Callback übergeben, das mit ‘SendMessage’ immer eine Nachricht an das gleiche GameObject sendet. Als Parameter von SendMessage übergeben wir die empfangenen Daten als JSON-String. Da es für unsere Zwecke einfacher war, den Eventtyp direkt in den Inhalt des Events zu kodieren, registriert der C# Code nur einen einzigen Listener auf “onData”. Dieser parst dann die empfangenen JSON Daten und leitet die Daten an den entsprechenden C#-Eventhandler weiter. Damit kann das UserApp Backend also Events an den Client senden.
Für den umgekehrten Weg, gibt es Client_sendEvent. Diese Funktion erwartet als Parameter die als JSON kodierten Daten. Diese werden geparst und dann als JavaScript-Objekt an Client.sendEvent der UserApp API weitergegeben.
Damit sind praktisch alle technischen Grundlagen für Unity Spiele als UserApp erklärt.
Ich hab das ganze natürlich nicht alleine gemacht, sondern wir haben das ganze im Team umgesetzt. Credits gehen an:
- juack (Entwicklung)
- brems (Entwicklung)
- Turmfalke (Entwicklung)
- flatplate (Entwicklung)
- digo84 (Entwicklung)
- nic95 (Entwicklung)
- TobyB (Kommunikation)
Falls ihr nun selbst Lust bekommen habt, das einmal auszuprobieren, traut euch! :)
Falls ihr Hilfe braucht oder Fragen habe, zögert nicht in diesem Thread zu schreiben.