Noch vor fünf Jahren war die Auswahl an Extended Reality-Geräten auf dem Markt äußerst begrenzt. Heute gibt es zwar eine Vielzahl verschiedener XR-Headsets mit unterschiedlichen Fähigkeiten, Eingabemethoden, Stärken und Schwächen sowie Vor- und Nachteilen. Doch das wirft die Frage auf: Welches Gerät eignet sich für welchen Anwendungsfall? Sollten wir ein VR-Headset wie die Meta Quest für ein immersives Erlebnis verwenden oder müssen Nutzer:innen sich ihrer Umgebung bewusst sein? Sollten wir die HoloLens 2 für eine intuitive Handinteraktion nutzen oder vielleicht lieber RealWear Navigator 500 für ein handfreies Erlebnis?
Bei Augment IT kommt es häufig vor, dass wir dieselbe oder eine ähnliche Lösung für mehrere verschiedene Geräte prototypisch entwickeln oder implementieren müssen. Machbarkeitsstudien, Benutzertests oder produktübergreifende Plattformen sind nur einige solcher Fälle. Doch welche Herausforderung genau stellt sich uns, wenn ein solcher Bedarf entsteht?
Unterschiedliche Betriebssysteme (OS) und Anwendungsprogrammierschnittstellen (API)
Die meisten Headsets laufen auf unterschiedlichen Betriebssystemen (OS). Einige verwenden angepasste Versionen bestehender Systeme, wie die auf Android basierende Meta Quest-Gerätefamilie oder die HoloLens 2, die eine Version des Windows-Betriebssystems nutzt. Andere Geräte wiederum laufen auf einem eigenen, speziell entwickelten OS. Das ist etwa bei Apple Vision Pro und visionOS der Fall, von dem Apple behauptet, es sei von Grund auf neu für „Spatial Computing“ entwickelt worden.
Das bedeutet, dass für jedes zu unterstützende Gerät eine andere Build-Pipeline erforderlich ist, die unterschiedliche Build-Tools verwendet und das Artefakt im entsprechenden Format generiert, wie beispielsweise eine APK-Datei für Android oder eine APPX-Datei für die HoloLens 2.
Da die meisten Geräte auf unterschiedlichen Betriebssystemen laufen, ist es naheliegend, dass sie den Entwickelnden einzigartige Schnittstellen zur Verfügung stellen, damit ihre Apps mit dem Betriebssystem kommunizieren können. Ein Beispiel dafür ist die Art und Weise, wie Benutzer:innen mit dem Gerät und den dafür entwickelten Anwendungen interagieren. Einige Geräte arbeiten mit Controllern, andere mit Handtracking und Gesten, Eyetracking oder Sprachbefehlen. Die meisten nutzen jedoch eine Mischung aus sämtlichen Eingabetypen.
Abgesehen von diesem grundlegenden Unterschied gibt es natürlich Funktionen, die auf manchen, aber nicht allen XR-Geräten verfügbar sind. Ein Beispiel dafür ist das Fotografieren. Die HoloLens 2 bietet eine In-App-API, Android-basierte Geräte verlassen sich entweder auf den In-App-Kamera-Stream oder auf installierte Apps. Apple Vision Pro sieht zum Schutz der Privatsphäre seiner Nutzer:innen Einschränkungen für diesen Anwendungsfall vor und macht es unmöglich, diese mit herkömmlichen Mitteln zu lösen.
Das Problem der plattformübergreifenden Entwicklung fordert eine effiziente Lösung, die langfristig erweitert und gewartet werden kann. Im Laufe der Jahre haben die Ingenieur:innen von Augment IT mit vielen Ansätzen experimentiert, um genau so eine Lösung zu entwickeln.
Die naive Lösung
Geht man ein konkretes, neuartiges oder einzigartiges Problem an, beginnt man üblicherweise mit einer einfachen und naheliegenden Lösung. Probleme werden identifiziert und iteriert und die Lösung Schritt für Schritt verbessert. So haben auch wir mit unserer Reise begonnen. Der ursprüngliche Gedanke war: „Lass uns alles beibehalten und nur die Teile anpassen, die sich unterscheiden.“
Unity ist in der Regel das wichtigste Werkzeug für die Entwicklung von XR-Erlebnissen. Es kann eine Codebasis für viele verschiedene Plattformen exportieren, sodass ein Teil des Problems bereits abgedeckt ist: die Paketierung der Anwendung. Glücklicherweise bietet Unity auch eine Möglichkeit, Code bedingt zu kompilieren, was bedeutet, dass ein Teil des Codes für eine bestimmte Plattform oder ein bestimmtes Gerät ignoriert, für eine andere jedoch einbezogen werden kann.
Dieser Mechanismus ist in der Programmiersprache C# integriert und wird von Unity zu diesem Zweck genutzt. Er wird als Präprozessor-Direktiven bezeichnet. Nehmen wir an, wir entwickeln eine Anwendung, die sowohl auf einem Android-Telefon als auch auf der HoloLens 2 laufen soll. Wenn ein Teil des Codes unter Android ausgeführt werden soll, ein anderer Teil jedoch auf der Universal Windows Platform (UWP), die die Plattform für die HoloLens 2-Entwicklung ist, würde es in etwa so aussehen.
#if UNITY_ANDROID
// Code that would be executed only when the platform is set to Android
#elif UNITY_WSA
// Code that would be executed only when the platform is set to UWP
#endif
Das ist auch hilfreich, wenn es plattformspezifischen Code gibt, der bei der Entwicklung der App nicht im Unity-Editor ausgeführt werden soll, sondern nur, wenn die Anwendung auf dem Gerät läuft. In diesem Fall wird das Symbol UNITY_EDITOR verwendet. Diese Symbole können auch mit einer Verneinung verwendet werden, sodass ein Teil des Codes im Editor ignoriert werden kann, indem die if-Anweisung stattdessen !UNITY_EDITOR überprüft. Auch wenn wir unsere eigenen Symbole definieren können, bietet Unity bereits viele nützliche eingebaute Symbole von Haus aus an.
Das löst zwar einen weiteren Teil des Problems, ist aber keine vollständige Lösung. Unterschiedliche Eingabetypen sind nach wie vor ein Problem. Auf einem Android-Gerät interagieren Benutzer:innen etwa hauptsächlich mit Berührungseingaben, während die HoloLens 2 in erster Linie auf Handtracking und Gesten setzt. Die Verwendung derselben Benutzeroberfläche (UI) ist zwar möglich, davon raten wir aber dringend ab. Dafür gibt es mehrere Gründe.
Im aktuellen Beispiel ist für die Anwendung auf dem Android-Telefon eine 2D-Benutzeroberfläche erforderlich und für die Anwendung auf der HoloLens 2 eine 3D-Benutzeroberfläche. Es ist äußerst unpraktisch, die Unity-Benutzeroberfläche zur Runtime von 2D in 3D und umgekehrt zu verwandeln. Die Lösung wird sogar noch schlechter, wenn wir eine oder mehrere unterstützte Plattformen hinzufügen.
Eine einzelne UI-Komponente mit Handhabung für zwei oder mehr verschiedene Geräte ist schwierig zu warten. Das Beheben eines Problems auf einer Plattform kann Probleme auf einer anderen verursachen, es werden Workarounds implementiert und die Qualität der Codebasis wird sich schnell verschlechtern. Daher ergibt sich eine weitere Frage, die näher an die Lösung heranführt: „Wie können wir das Problem mit den unterschiedlichen Benutzeroberflächen auf praktischere Weise lösen?“
Eine bessere, aber immer noch naive Lösung
An diesem Punkt ist es offensichtlich, dass das größte Problem darin besteht, dass die UI-Logik zu viele Aufgaben zu bewältigen hat und aufgeteilt und vereinfacht werden muss. Unity bietet zum zweiten Mal eine Lösung für dieses Problem, diesmal in Form von additivem Laden von Szenen.
Alles, was in einer Unity-Anwendung angezeigt wird, ist in Szenen organisiert. Dazu gehören die Benutzeroberfläche, 3D-Objekte und alle Skripte, die für das Funktionieren der Anwendung erforderlich sind. Das Wechseln zwischen mehreren Szenen wird normalerweise vermieden, da es ressourcenintensiv sein und Unterbrechungen im Anwendungsablauf verursachen kann. Das ist in der Regel nicht erwünscht, insbesondere nicht in Unternehmensanwendungen.
Anstatt zwischen Szenen zu wechseln, aktiviert das additive Laden zwei oder mehr Szenen gleichzeitig. Damit ist es möglich, alle gemeinsamen Objekte und Komponenten in eine Szene zu extrahieren und die plattformspezifischen Elemente in eine oder mehrere andere Szenen.
Mit ein wenig Umstrukturierung und Skripterstellung ist es also einfach, diese Lösung zu erreichen. Die Core-Szene enthält die gemeinsamen Elemente und ein Skript, das je nach Plattform, für die wir exportieren, andere Szenen additiv lädt.
Die Szenen sind hier alle zur Veranschaulichung in die Hierarchie eingefügt. Es sollte nur die Core-Szene hinzugefügt werden, die dann zur Laufzeit die erforderliche Plattformszene lädt. Alle diese Szenen müssen den Build-Einstellungen hinzugefügt werden, damit dieser Ansatz funktioniert.
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
private void Awake()
{
#if UNITY_ANDROID
// Code that would be executed only when the platform is set to Android
SceneManager.LoadSceneAsync("Android", LoadSceneMode.Additive);
#elif UNITY_WSA
// Code that would be executed only when the platform is set to UWP
SceneManager.LoadSceneAsync("HoloLens2", LoadSceneMode.Additive);
#endif
}
}
Das ist ein weiterer Schritt in die richtige Richtung. Die Benutzeroberfläche ist sauber getrennt und jede Plattform verwaltet ihre eigenen Komponenten. Allerdings befindet sich die gesamte Codebasis in einem einzigen Projekt, was andere Probleme mit sich bringt. Probleme, die durch die selektive Kompilierung, Bibliothekskompatibilität und Tests auf den verschiedenen unterstützten Plattformen verursacht werden, sind nur einige davon. Im nächsten Teil werden wir beschreiben, wie dies noch weiter verbessert werden kann, um sich der neuesten Lösung, die wir am häufigsten verwenden, anzunähern, und ihre Vor- und Nachteile erläutern.