Skip to content

Simuler un environnement d’exécution qui dit non : comment nous transformons le navigateur en bac à sable pour qu’il se comporte comme WeChat

Par Oleg Sidorkin, CTO et cofondateur de Cinevva

Une petite scène de jeu 3D sous un dôme de verre transparent, avec des icônes d’outils barrées flottant à l’extérieur

Nous avons récemment lancé un mode mini-jeu WeChat dans notre Créateur de jeux : un profil de compilation qui maintient les jeux générés par IA dans le sous-ensemble de la plateforme web capable de survivre à un portage vers l’environnement d’exécution de WeChat. Ce profil est un ensemble de règles de génération. Pas d’interface DOM, pas de Pointer Lock, audio en mp3 uniquement, pas de code dynamique. Le modèle les respecte comme les modèles respectent les règles, c’est-à-dire généralement.

« Généralement » ne suffit pas lorsque chaque violation est invisible dans le navigateur et fatale après l’export. Un jeu doté d’une barre de vie en HTML fonctionne parfaitement dans notre aperçu, puis affiche un écran vide sur WeChat, car les mini-jeux WeChat n’ont aucun DOM. Nous avons donc construit ce qui transforme ces règles en lois physiques plutôt qu’en suggestions : un bac à sable en mode strict qui force le navigateur lui-même à refuser ce que WeChat ne peut pas faire. Cet article explique son fonctionnement.

La décision fondamentale : simuler l’absence, pas la présence

L’approche évidente consisterait à émuler WeChat : implémenter wx.createCanvas, wx.onTouchStart, wx.setStorageSync, puis exécuter les jeux sur cette surface wx.* émulée. Nous avons choisi l’approche inverse, en raison de la structure de notre pipeline d’export.

En mode WeChat, les jeux sont toujours écrits à partir des API du navigateur. Lors de la mise en paquet, un adaptateur — selon l’approche standard de la communauté weapp-adapter — mappe ces API de navigateur vers wx.*. Nos jeux n’appellent donc jamais directement wx.* ; un émulateur wx testerait des chemins de code qui n’existent pas. Ce qui casse réellement un portage n’est pas l’absence de wx.* dans le navigateur. C’est la présence d’API de navigateur sans équivalent dans WeChat, utilisées discrètement pendant la génération. document.createElement('div'). requestPointerLock(). IndexedDB. Un fichier .ogg parfaitement décodé par Chrome qui ne le sera jamais sur WeChat pour iOS.

Le bac à sable simule donc l’absence. Tout ce que WeChat ne possède pas est supprimé, rendu inutilisable ou signalé dans l’aperçu, et le jeu se développe dès sa première image dans l’intersection des deux plateformes.

Son emplacement : un service worker qui était déjà là

Notre aperçu dans l’éditeur suit un chemin de diffusion inhabituel qui a rendu cette fonctionnalité presque gratuite. Dans l’éditeur, les jeux ne sont pas servis depuis le réseau. Un service worker intercepte les requêtes /game/* et sert les fichiers depuis IndexedDB, ce qui permet à l’iframe d’aperçu de se recharger instantanément sans aller-retour réseau. Ce worker injectait déjà deux fichiers virtuels dans chaque page de jeu : un script de capture des erreurs qui transmet à l’éditeur les sorties de console et les plantages, ainsi qu’un inspecteur de scène en direct.

Le bac à sable est le troisième fichier virtuel, _wechat-strict.js, injecté dans le <head> de la page immédiatement après le script de capture et avant l’exécution de tout code du jeu. Cet ordre est crucial à deux égards. Il doit s’exécuter après le script de capture afin que ses appels à console.error soient déjà interceptés et transmis à l’éditeur, mais avant les modules du jeu afin que les interdictions soient en place dès l’exécution de la première ligne de code.

L’activation tient en une seule ligne. L’état du bouton WeChat du créateur est conservé dans localStorage, et l’iframe d’aperçu partage la même origine que l’éditeur ; le harnais n’a donc qu’à lire directement l’indicateur :

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

Pour chaque jeu qui ne cible pas WeChat, le script se résume à une comparaison de chaînes et à un retour anticipé. Aucun indicateur de compilation, aucun aller-retour avec le serveur, aucun état à synchroniser avec le service worker.

La liste des interdictions et la classification à deux niveaux

Toutes les violations ne méritent pas la même réponse ; le harnais distingue donc deux niveaux. Les API qui n’existent pas sur WeChat lèvent une exception, car c’est ce qui se produit après l’export et qu’un plantage pendant la génération constitue un aperçu fidèle d’un plantage lors de la validation. Les éléments qui existent mais cassent plus tard déclenchent des erreurs de console bien visibles sans modifier le comportement, car les bloquer masquerait l’état réel du jeu à la personne qui travaille dessus.

Le niveau qui lève une exception : la création de tout élément d’interface HTML, via createElement comme via createElementNS — Three.js crée son canvas avec la variante NS, les deux chemins doivent donc utiliser le même filtre —, eval, new Function, requestPointerLock et l’enregistrement d’un service worker. Chaque exception indique la correction à apporter dans son message :

js
throw violate("document.createElement('" + tag + "') — Les mini-jeux WeChat n’ont pas de DOM. " +
  "Dessinez l’interface sur un canvas (canvas 2D hors écran -> THREE.CanvasTexture sur un quadrilatère en espace écran) " +
  "et gérez vous-même la détection des touchers.");

Le niveau de signalement : l’accès à indexedDB — qui renvoie undefined, exactement comme WeChat, en plus d’une erreur orientant vers localStorage —, l’audio dans tout format que WeChat pour iOS ne peut pas décoder — vérifié à trois endroits : le constructeur Audio, le setter src du prototype de l’élément multimédia et les URL récupérées —, les requêtes réseau vers toute origine autre que celle du jeu ou notre CDN de ressources — sur WeChat, elles nécessitent un domaine inscrit sur liste blanche et enregistré auprès de l’ICP —, ainsi que toute apparition de Tone.js.

Un audit après chargement s’exécute également peu après la stabilisation de la page : il parcourt le <body> et signale tous les éléments HTML provenant directement du balisage du jeu plutôt que d’une création dynamique. Les HUD statiques échappent à un filtre sur createElement ; ce filtre ne suffit donc pas à lui seul.

Chaque signalement est dédupliqué par message et plafonné à cinquante par session. Un bug de boucle inversée qui crée une div à chaque image produit une seule erreur, pas un déluge qui noie l’information utile.

Les quatre exceptions qui nous ont le plus appris

Construire un bac à sable consiste surtout à décider de ce qu’il ne faut pas rendre inutilisable, et chaque exception que nous avons ajoutée est née d’une panne de nos propres outils pendant le développement.

Notre débogueur repose sur eval. L’outil execute_js du créateur, que l’IA utilise pour sonder le jeu en direct, évalue du code via le gestionnaire de messages du script de capture, qui appelle eval. Rendre eval naïvement inutilisable aurait privé l’IA de ses propres yeux. La solution : avant de le neutraliser, le harnais conserve la véritable fonction dans une propriété non énumérable, puis le gestionnaire du script de capture se rabat sur celle-ci :

js
Object.defineProperty(window, '__cinevvaRealEval', { value: window.eval, enumerable: false });
window.eval = function () { throw violate('eval() est interdit dans les mini-jeux WeChat.'); };
// script de capture, lors de la réception du message :
returnValue = (window.__cinevvaRealEval || eval)(e.data.code);

Le code du jeu qui tente d’utiliser eval échoue toujours. Pas le débogueur.

L’enregistreur crée lui aussi des éléments. Notre enregistreur de reels est injecté dans l’iframe du jeu sous forme de <script> créé avec le propre document.createElement de l’iframe, puis télécharge les vidéos terminées au moyen d’un clic synthétique sur un élément <a>. Bloquer ces balises aurait cassé l’enregistrement, uniquement et précisément en mode WeChat. La balise script reste donc autorisée et a déclenche un avertissement sans lever d’exception. Un jeu qui inclut un véritable lien est tout de même signalé, tandis que nos outils continuent de fonctionner.

Les événements clavier restent disponibles. L’IA vérifie les commandes grâce à un outil qui synthétise des frappes au clavier et observe ensuite comment l’état du jeu a évolué. Supprimer les entrées clavier pour simuler un téléphone aurait détruit la boucle de vérification des commandes qui détecte notamment les inversions de caméra. La priorité au tactile est imposée par les règles de génération et le schéma de commandes du profil, pas par le bac à sable.

WebGL2 reste disponible, et la raison mérite sa propre section ci-dessous. La première version de la liste des interdictions bloquait getContext('webgl2') afin d’imposer le rendu « compatible WebGL1 » exigé par le profil. Sauf que Three.js a entièrement supprimé la prise en charge de WebGL1 dans la version r163, et que nous utilisons la r181 : bloquer WebGL2 aurait rendu vierge chaque jeu 3D créé dans ce mode. La règle de rendu du profil était incohérente, issue d’une vision obsolète de la plateforme. L’audit du bac à sable a donc audité la spécification du bac à sable lui-même, et en tirant sur ce fil, nous avons découvert quelque chose d’important.

La question de WebGL2, correctement résolue

Ne pas toucher à la création du contexte soulevait immédiatement une autre question : si notre moteur exige WebGL2, les jeux cassent-ils sur les appareils WeChat qui ne disposent que de WebGL1 ? Nos recherches nous ont fourni l’image la plus claire dont nous disposons sur le niveau minimal de rendu de la plateforme ; la voici, sources à l’appui.

Sur Android, l’environnement d’exécution des mini-jeux WeChat fournit WebGL2 avec toute version moderne de la bibliothèque de base, et le matériel GLES3 sous-jacent est pratiquement universel. iOS est le véritable enjeu. La documentation technique de WeChat sur la prise en charge de WebGL2 et le mode hautes performances+ précise que, sur iPhone, WebGL2 n’est correctement disponible que dans l’environnement hautes performances+, lequel exige une version récente du client WeChat — 8.0.45 ou ultérieure, davantage sous iOS 14 — et, dans les faits, iOS 15.5 ou plus récent. D’après la propre description de Tencent, le mode hautes performances ordinaire exécute WebGL2 « avec davantage de problèmes », tandis que l’environnement iOS normal ne le prend pas du tout en charge.

Le véritable danger réside dans la forme que prend l’échec. Dans les environnements non pris en charge, getContext('webgl2') peut renvoyer un contexte évalué comme vrai mais défectueux au lieu de null, un comportement que des développeurs ont directement demandé à Tencent de corriger. Un jeu qui fait confiance à cette valeur de retour n’échoue pas proprement. Il affiche des artefacts ou un écran noir silencieux.

Nous traitons donc le problème sur trois niveaux. Le bac à sable ne touche pas à la création du contexte WebGL2, car la rendre inutilisable reviendrait à lutter contre notre propre moteur. Le profil de génération exige désormais une barrière au démarrage dans chaque jeu : la création du moteur de rendu doit être enveloppée dans un bloc try/catch, puis suivie d’un test fonctionnel (typeof gl.createVertexArray === 'function', une capacité propre à WebGL2 qu’un faux contexte ne possédera pas), avec en cas d’échec un message convivial dessiné sur le canvas demandant de mettre WeChat à jour, plutôt qu’un écran vide. C’est le même modèle que celui utilisé par les mini-jeux convertis depuis Unity, ce qui explique pourquoi on rencontre dans la pratique des invitations à mettre à niveau plutôt que des écrans noirs. Enfin, lorsque l’exporteur sera disponible, il imposera le mode hautes performances+ dans game.json afin que le chemin compatible soit celui utilisé par défaut.

Quel est le coût d’un minimum WebGL2 en matière d’audience ? Les utilisateurs sur une version antérieure à iOS 15.5 environ ou avec un client WeChat plus ancien que la version 8.0.45 : une faible proportion à un chiffre en 2026, mais concentrée sur les appareils anciens, ce qui importe davantage pour certains genres que pour d’autres. Si les données réelles de distribution montrent un jour que cette longue traîne mérite d’être ciblée, nous gardons en réserve une solution de repli peu coûteuse : épingler le profil WeChat sur Three r162, la dernière version compatible avec WebGL1. Les jeux du profil n’utilisent que des API fondamentales stables ; le retour à une version antérieure se résume donc à modifier une seule ligne dans l’import map, une option que nous conservons délibérément jusqu’à ce que les données l’exigent.

Boucler la boucle avec le modèle

Voici ce qui rend cette fonctionnalité particulièrement utile pour un produit centré sur l’IA. Après chaque compilation, l’agent du créateur appelle un outil qui lit la console du jeu et attend la fin du démarrage. Le script de capture transmet console.error à ce flux. Une violation [wechat-strict] n’est donc pas un avertissement qu’un humain pourrait ignorer en faisant défiler la page. Elle arrive dans le canal précis que le modèle consulte déjà avant de déclarer la compilation terminée, au cours du même tour, avec la correction indiquée dans le message.

Le profil de génération comble le dernier manque avec une seule instruction : traiter chaque message [wechat-strict] comme un bug bloquant la compilation, en corriger la cause et ne jamais tenter de détecter ou de contourner le harnais. Un jeu qui ne fonctionne que lorsque le bac à sable est désactivé est un jeu qui échouera après l’export, et le modèle en est explicitement informé.

En pratique, le bac à sable transforme un problème de conformité flou — « le modèle a-t-il respecté les quatorze règles ? » — en la boucle de débogage que le système maîtrise déjà — « la console affiche une erreur, corrigez-la ».

Ce qu’un navigateur ne peut pas simuler

Par souci d’honnêteté : ce bac à sable détecte les violations liées à la surface des API, qui représentent selon nous la grande majorité des causes d’échec d’un portage. Il ne peut pas détecter les performances sur un véritable téléphone, l’exécution de JavaScript sans JIT sous iOS, les particularités de l’implémentation WebGL de WeChat ni les différences de comportement des codecs au-delà des extensions de fichiers. Pour cela, il faut utiliser l’environnement réel.

Le bac à sable constitue donc le premier de trois niveaux. Le deuxième, lorsque notre exporteur produira des projets pour les outils de développement WeChat, sera le simulateur officiel piloté sans interface graphique via la CLI des DevTools et miniprogram-automator : démarrer le paquet exporté, vérifier que la première image s’affiche, simuler un toucher. Le troisième est le parcours officiel sur appareil, avec des codes QR d’aperçu et le débogage à distance sur des téléphones physiques, là où résident les vérités que rien d’autre ne peut révéler. Chaque niveau est plus lent et plus fidèle que le précédent, et chacun doit rendre rares les passages au suivant.

Pour essayer ce mode, activez l’option « Mode mini-jeu WeChat » dans le Créateur de jeux. Pour en savoir plus sur le marché et le contexte réglementaire, consultez notre guide pratique pour atteindre le demi-milliard de joueurs de WeChat.

Références