Skip to content

Eine Laufzeitumgebung simulieren, die Nein sagt: So bringen wir den Browser in einer Sandbox dazu, sich wie WeChat zu verhalten

Von Oleg Sidorkin, CTO und Mitgründer von Cinevva

Eine kleine 3D-Spielszene unter einer transparenten Glaskuppel, außerhalb derer durchgestrichene Werkzeugsymbole schweben

Vor Kurzem haben wir einen WeChat-Minigame-Modus in unserem Game Creator veröffentlicht: ein Build-Profil, das KI-generierte Spiele auf den Teil der Webplattform beschränkt, der eine Portierung in die Laufzeitumgebung von WeChat übersteht. Das Profil besteht aus einer Reihe von Generierungsregeln. Keine DOM-Benutzeroberfläche, kein Pointer Lock, nur MP3-Audio, kein dynamischer Code. Das Modell befolgt sie so, wie Modelle Regeln eben befolgen – normalerweise.

„Normalerweise“ reicht nicht, wenn jeder Verstoß im Browser unsichtbar bleibt und nach dem Export fatal ist. Ein Spiel mit einer HTML-Gesundheitsanzeige läuft in unserer Vorschau perfekt und zeigt auf WeChat nur einen leeren Bildschirm, weil WeChat-Minispiele überhaupt kein DOM haben. Deshalb haben wir etwas entwickelt, das diese Regeln von Empfehlungen in physikalische Gesetze verwandelt: eine Strict-Mode-Sandbox, die den Browser selbst alles verweigern lässt, was WeChat nicht kann. In diesem Beitrag erklären wir, wie sie funktioniert.

Die grundlegende Entscheidung: Abwesenheit simulieren, nicht Anwesenheit

Der naheliegende Ansatz wäre, WeChat zu emulieren: wx.createCanvas, wx.onTouchStart und wx.setStorageSync zu implementieren und Spiele gegen die emulierte wx.*-Oberfläche laufen zu lassen. Wir haben uns für den umgekehrten Weg entschieden. Der Grund dafür liegt in der Struktur unserer Export-Pipeline.

Spiele im WeChat-Modus werden weiterhin für Browser-APIs geschrieben. Beim Packaging bildet ein Adapter – nach dem etablierten Community-Ansatz von weapp-adapter – diese Browser-APIs auf wx.* ab. Unsere Spiele rufen wx.* also nie direkt auf, weshalb ein wx-Emulator Codepfade testen würde, die gar nicht existieren. Was eine Portierung tatsächlich scheitern lässt, ist nicht das Fehlen von wx.* im Browser. Es ist die Anwesenheit von Browser-APIs ohne WeChat-Entsprechung, die während der Generierung unbemerkt verwendet werden. document.createElement('div'). requestPointerLock(). IndexedDB. Eine .ogg-Datei, die Chrome problemlos decodiert, unter WeChat auf iOS aber niemals funktionieren wird.

Die Sandbox simuliert daher Abwesenheit. Alles, was WeChat nicht hat, wird in der Vorschau entfernt, vergiftet oder markiert. So wird das Spiel vom ersten Frame an innerhalb der Schnittmenge beider Plattformen entwickelt.

Wo sie sitzt: in einem Service Worker, der bereits vorhanden war

Unsere Editor-Vorschau verwendet einen ungewöhnlichen Bereitstellungspfad, durch den sich das fast kostenlos umsetzen ließ. Spiele im Editor werden nicht über das Netzwerk ausgeliefert. Ein Service Worker fängt Anfragen an /game/* ab und stellt Dateien aus IndexedDB bereit. Dadurch erhält der Vorschau-iframe sofortige Neuladungen ohne Umweg über das Netzwerk. Dieser Worker hat bereits zwei virtuelle Dateien in jede Spielseite injiziert: ein Skript zur Fehlererfassung, das Konsolenausgaben und Abstürze an den Editor weiterleitet, sowie einen Live-Szeneninspektor.

Die Sandbox ist die dritte virtuelle Datei, _wechat-strict.js. Sie wird unmittelbar nach dem Erfassungsskript und vor der Ausführung jeglichen Spielcodes in den <head> der Seite injiziert. Die Reihenfolge ist gleich aus zwei Gründen entscheidend. Sie muss nach der Erfassung laufen, damit ihre console.error-Aufrufe bereits abgefangen und an den Editor weitergeleitet werden. Gleichzeitig muss sie vor den Spielmodulen laufen, damit die Sperren bereits greifen, wenn die erste Zeile des Spielcodes ausgeführt wird.

Die Aktivierung erfolgt mit einem Einzeiler. Der WeChat-Schalter im Creator wird in localStorage gespeichert, und der Vorschau-iframe hat denselben Ursprung wie der Editor. Deshalb liest das Testgerüst das Flag einfach direkt aus:

js
try {
  if (localStorage.getItem('cinevva-gc-profile') !== 'wechat-minigame') return;
} catch (e) { return; }

Bei jedem Spiel außerhalb des WeChat-Modus führt das Skript nur einen Stringvergleich und eine vorzeitige Rückkehr aus. Kein Build-Flag, kein Umweg über den Server und kein Zustand, der mit dem Service Worker synchronisiert werden müsste.

Die Poison-Liste und die zweistufige Taxonomie

Nicht jeder Verstoß verdient dieselbe Reaktion. Deshalb unterscheidet das Testgerüst zwei Stufen. APIs, die auf WeChat nicht existieren, lösen eine Exception aus. Genau das geschieht auch nach dem Export, und ein Absturz während der Generierung ist eine ehrliche Vorschau auf einen Absturz bei der Prüfung. Dinge, die existieren, aber später Probleme verursachen, werden als deutliche Konsolenfehler markiert, ohne ihr Verhalten zu verändern. Würden wir sie blockieren, bliebe der tatsächliche Zustand des Spiels für die Person verborgen, die gerade daran arbeitet.

Die Exception-Stufe umfasst das Erstellen beliebiger HTML-UI-Elemente über createElement und createElementNS – Three.js erstellt sein Canvas über die NS-Variante, weshalb beide Pfade dieselbe Sperre benötigen –, außerdem eval, new Function, requestPointerLock und die Registrierung von Service Workern. Jede Exception enthält direkt die nötige Korrektur:

js
throw violate("document.createElement('" + tag + "') — WeChat-Minispiele haben kein DOM. " +
  "Zeichne die Benutzeroberfläche auf ein Canvas (Offscreen-2D-Canvas -> THREE.CanvasTexture auf einem Screen-Space-Quad) " +
  "und implementiere die Trefferprüfung für Berührungen selbst.");

Zur Markierungsstufe gehören: der Zugriff auf indexedDB – er gibt genau wie unter WeChat undefined zurück und meldet zusätzlich einen Fehler mit Verweis auf localStorage –, Audio in Formaten, die WeChat unter iOS nicht decodieren kann – geprüft an drei Stellen: im Audio-Konstruktor, im src-Setter des Medienelement-Prototyps und bei abgerufenen URLs –, Netzwerkanfragen an Ursprünge, die weder zum Spiel selbst noch zu unserem Asset-CDN gehören – unter WeChat benötigen diese eine freigegebene Domain mit ICP-Registrierung – sowie jegliches Auftauchen von Tone.js.

Zusätzlich gibt es eine Prüfung nach dem Laden, die kurz nach dem Stabilisieren der Seite ausgeführt wird. Sie durchläuft den <body> und meldet alle HTML-Elemente, die aus dem eigenen Markup des Spiels stammen, statt dynamisch erzeugt worden zu sein. Statische HUDs können eine createElement-Sperre umgehen, weshalb diese Sperre allein nicht ausreicht.

Jede Meldung wird anhand ihres Textes dedupliziert, und pro Sitzung gilt eine Obergrenze von fünfzig Meldungen. Ein fehlerhafter invertierter Loop, der pro Frame ein div erzeugt, produziert einen Fehler – keinen Meldungsstrom, in dem das eigentliche Signal untergeht.

Die vier Ausnahmen, aus denen wir am meisten gelernt haben

Beim Aufbau einer Sandbox geht es vor allem darum, zu entscheiden, was nicht vergiftet werden darf. Jede Ausnahme, die wir geschaffen haben, entstand dadurch, dass unsere eigenen Werkzeuge während der Entwicklung nicht mehr funktionierten.

Unser Debugger basiert auf eval. Das execute_js-Werkzeug des Creators, mit dem die KI das laufende Spiel untersucht, wertet Code über den Message-Handler des Erfassungsskripts aus, der wiederum eval aufruft. Eine naive Sperre von eval hätte die KI ihrer eigenen Augen beraubt. Die Lösung: Vor der Sperre speichert das Testgerüst die echte Funktion in einer nicht aufzählbaren Eigenschaft. Der Handler des Erfassungsskripts greift darauf zurück:

js
Object.defineProperty(window, '__cinevvaRealEval', { value: window.eval, enumerable: false });
window.eval = function () { throw violate('eval() ist in WeChat-Minispielen verboten.'); };
// Erfassungsskript zum Zeitpunkt der Nachricht:
returnValue = (window.__cinevvaRealEval || eval)(e.data.code);

Spielcode, der eval verwenden will, scheitert weiterhin. Der Debugger nicht.

Auch der Recorder erstellt Elemente. Unser Reel-Recorder wird als <script> in den Spiel-iframe injiziert, das mit document.createElement des iframes selbst erstellt wird. Fertige Videos lädt er über einen synthetischen Klick auf ein <a>-Element herunter. Diese Tags zu blockieren hätte die Aufnahme ausschließlich im WeChat-Modus lahmgelegt. Deshalb bleibt script erlaubt, während a nur eine Warnung ausgibt, ohne eine Exception auszulösen. Ein Spiel, das tatsächlich einen Link ausliefert, wird weiterhin markiert, und die Werkzeuge funktionieren trotzdem.

Tastaturereignisse bleiben erhalten. Die KI überprüft die Steuerung mit einem Werkzeug, das Tastendrücke simuliert und anschließend ausliest, wie sich der Spielzustand verändert hat. Tastatureingaben zu unterdrücken, um ein Smartphone zu simulieren, hätte die Kontrollschleife für die Steuerung zerstört, die unter anderem invertierte Kameras erkennt. Eine Touch-First-Bedienung wird durch die Generierungsregeln und das Steuerungsschema des Profils erzwungen, nicht durch die Sandbox.

WebGL2 bleibt erhalten, und der Grund dafür verdient einen eigenen Abschnitt. Die erste Poison-Liste blockierte getContext('webgl2'), um das im Profil geforderte „WebGL1-sichere“ Rendering zu erzwingen. Allerdings hat Three.js die WebGL1-Unterstützung in r163 vollständig entfernt, und wir verwenden fest r181. Eine Blockierung von WebGL2 hätte jedes 3D-Spiel in diesem Modus lahmgelegt. Die Rendering-Regel des Profils war widersprüchlich und basierte auf einem veralteten Bild der Plattform. Bei der Prüfung der Sandbox wurde also die Spezifikation der Sandbox selbst geprüft – und dieser Faden führte zu einer wichtigen Erkenntnis.

Die WebGL2-Frage, richtig beantwortet

Die Kontexterstellung unangetastet zu lassen, warf die naheliegende Folgefrage auf: Wenn unsere Engine WebGL2 benötigt, funktionieren die Spiele dann nicht auf WeChat-Geräten, die nur WebGL1 unterstützen? Die Untersuchung dieser Frage ergab das bisher klarste Bild, das wir von der Rendering-Untergrenze der Plattform haben. Deshalb folgt es hier mitsamt Quellen.

Unter Android stellt die Laufzeitumgebung für WeChat-Minispiele WebGL2 mit jeder modernen Basisbibliothek bereit, und die zugrunde liegende GLES3-Hardware ist praktisch universell verfügbar. iOS ist der eigentlich interessante Fall. Die technischen Dokumente von WeChat zur WebGL2-Unterstützung und zum High-Performance+-Modus stellen klar, dass WebGL2 auf dem iPhone nur in der High-Performance+-Laufzeitumgebung zuverlässig verfügbar ist. Diese setzt einen aktuellen WeChat-Client voraus – mindestens Version 8.0.45, unter iOS 14 noch höher – und effektiv iOS 15.5 oder neuer. Der gewöhnliche High-Performance-Modus führt WebGL2 nach Tencents eigener Beschreibung „mit mehr Problemen“ aus, während die normale iOS-Laufzeitumgebung es überhaupt nicht unterstützt.

Besonders gefährlich ist die Art des Fehlers. In nicht unterstützten Umgebungen kann getContext('webgl2') statt null einen truthy, aber defekten Kontext zurückgeben – ein Verhalten, bei dem Entwickler Tencent direkt um eine Korrektur gebeten haben. Ein Spiel, das dem Rückgabewert vertraut, scheitert dort nicht sauber. Es rendert Grafikmüll oder zeigt ohne Fehlermeldung einen schwarzen Bildschirm.

Wir begegnen dem in drei Schichten. Die Sandbox lässt die Erstellung des WebGL2-Kontexts unangetastet, weil eine Sperre gegen unsere eigene Engine arbeiten würde. Das Generierungsprofil verlangt nun in jedem Spiel eine Startprüfung: Die Renderer-Erstellung wird in try/catch eingeschlossen und anschließend mit einem funktionalen Test geprüft (typeof gl.createVertexArray === 'function', eine ausschließlich in WebGL2 vorhandene Fähigkeit, die ein lügender Kontext nicht bereitstellt). Bei einem Fehler erscheint statt eines leeren Bildschirms eine freundliche, auf das Canvas gezeichnete Meldung mit der Bitte, WeChat zu aktualisieren. Dasselbe Muster verwenden auch aus Unity konvertierte Minispiele. Deshalb sieht man in veröffentlichten Spielen Aktualisierungsaufforderungen statt schwarzer Bildschirme. Sobald der Exporter verfügbar ist, legt er außerdem den High-Performance+-Modus in game.json fest, sodass der leistungsfähige Pfad standardmäßig verwendet wird.

Wie groß ist der Verlust an Zielgruppe durch eine WebGL2-Untergrenze? Betroffen sind Nutzer mit älteren Versionen als ungefähr iOS 15.5 oder WeChat-Clients vor 8.0.45. Im Jahr 2026 liegt ihr Anteil im niedrigen einstelligen Prozentbereich, konzentriert sich jedoch auf ältere Geräte – was für manche Genres wichtiger ist als für andere. Sollten echte Vertriebsdaten jemals zeigen, dass sich die Unterstützung dieses Restsegments lohnt, halten wir eine günstige Ausweichmöglichkeit bereit: das WeChat-Profil auf Three r162 festzulegen, die letzte Version mit WebGL1-Unterstützung. Spiele dieses Profils verwenden nur stabile Kern-APIs, weshalb das Downgrade lediglich eine einzeilige Änderung der Import Map erfordert. Diese Option bleibt bewusst ungenutzt, bis die Daten sie rechtfertigen.

Die Schleife mit dem Modell schließen

Hier zeigt sich, warum sich diese Entwicklung für ein KI-zentriertes Produkt lohnt. Nach jedem Build ruft der Agent des Creators ein Werkzeug auf, das die Spielkonsole ausliest und wartet, bis der Start abgeschlossen ist. Das Erfassungsskript leitet console.error an diesen Datenstrom weiter. Ein [wechat-strict]-Verstoß ist daher keine Warnung, an der ein Mensch vielleicht vorbeiscrollt. Er landet noch im selben Durchlauf genau in dem Kanal, den das Modell ohnehin prüft, bevor es einen Build als abgeschlossen bezeichnet – und die Meldung enthält direkt die nötige Korrektur.

Das Generierungsprofil schließt die letzte Lücke mit einer Anweisung: Jede [wechat-strict]-Meldung ist als Build-blockierender Fehler zu behandeln, ihre Ursache muss behoben werden, und das Testgerüst darf niemals erkannt oder umgangen werden. Ein Spiel, das nur bei deaktivierter Sandbox funktioniert, ist ein Spiel, das nach dem Export scheitert. Genau das wird dem Modell ausdrücklich mitgeteilt.

Im Ergebnis verwandelt die Sandbox ein unscharfes Compliance-Problem – „Hat das Modell alle vierzehn Regeln eingehalten?“ – in die Debugging-Schleife, die das System bereits gut beherrscht: „Die Konsole zeigt einen Fehler; behebe ihn.“

Was ein Browser nicht simulieren kann

Nun zum ehrlichen Teil. Diese Sandbox erkennt Verstöße gegen die API-Oberfläche, die unserer Einschätzung nach für die große Mehrheit gescheiterter Portierungen verantwortlich sind. Sie kann Folgendes nicht erkennen: die Leistung auf einem echten Smartphone, die Ausführung von JavaScript ohne JIT unter iOS, Eigenheiten der WebGL-Implementierung von WeChat oder Unterschiede im Codec-Verhalten, die über Dateiendungen hinausgehen. Dafür ist die echte Laufzeitumgebung erforderlich.

Die Sandbox ist deshalb der erste von drei Ringen. Ring zwei ist – sobald unser Exporter WeChat-DevTools-Projekte ausgibt – der offizielle Simulator, der headless über die DevTools-CLI und miniprogram-automator gesteuert wird: exportiertes Paket starten, prüfen, ob der erste Frame gerendert wird, eine Berührung skripten. Ring drei ist der offizielle Gerätepfad mit Vorschau-QR-Codes und Remote-Debugging auf physischen Smartphones. Dort kommen die Wahrheiten ans Licht, die kein anderes System offenbart. Jeder Ring ist langsamer und wirklichkeitsgetreuer als der vorherige. Seine Aufgabe besteht jeweils darin, Ausflüge in den nächsten Ring selten zu machen.

Wenn du den Modus ausprobieren möchtest, aktiviere im Game Creator den Schalter „WeChat-Minigame-Modus“. Den Markt- und Regulierungskontext dazu findest du in unserem Praxisleitfaden zum Erreichen der halben Milliarde WeChat-Spieler.

Quellen