Skip to content

Avatars animés et effets Phaser ​

Recette pour l’iframe SDK V2 d’une appli ou d’un widget. kapp.gameEngines.phaser charge le moteur et les modules Kappapps existants. Pour une simple image d’avatar, préférer user.getAvatarPng() ; voir composants visuels.

Ordre des opérations ​

  1. Charger Phaser avec await kapp.gameEngines.phaser.load() avant new Phaser.Game.
  2. Attendre create de la scène ; ne pas supposer qu’elle existe juste après le constructeur du jeu.
  3. Charger les modules voulus séquentiellement dans cette scène : les loaders partagent notamment leur chemin de base.
  4. Créer l’avatar après loadAvatarModule(scene) ; jouer un effet après loadEffectsModule(scene).

Le loader SDK utilise par défaut le Phaser hébergé en 3.55.2. Les dépendances de compilation du module avatar mentionnent une autre version : cela ne publie pas automatiquement une nouvelle version à l’URL /overlay/phaser/<version>/phaser.min.js. Vérifier la version réellement servie ; ne pas inventer une URL 3.90.0. Dans plusieurs scènes, réutiliser les plugins installés et charger leurs assets sur chacune au lieu de réinstaller aveuglément les plugins globaux.

Une scène, un avatar et un effet ​

Fonction à intégrer après initialisation SDK. avatarData vient de await user.getAvatarData() pour un viewer résolu, ou d’un objet d’apparence choisi. Des champs absents peuvent être randomisés. Les éléments HTML #visual et #visual-status existent dans l’iframe.

js
async function showAvatarEffect(avatarData) {
    const status = document.getElementById('visual-status');
    let game;
    let avatar;
    let closed = false;
    let timer;
    let rejectPending;
    const cleanup = () => {
        if (closed) return;
        closed = true;
        clearTimeout(timer);
        if (rejectPending) rejectPending(new Error('Affichage interrompu'));
        avatar?.destroy();
        game?.destroy(true);
    };
    kapp.onBeforeExit('*', cleanup);
    status.textContent = 'Chargement de l’avatar…';
    const stopped = new Promise((_, reject) => { rejectPending = reject; });
    try {
        const work = (async () => {
            await kapp.gameEngines.phaser.load();
            if (closed) throw new Error('Affichage interrompu');
            await new Promise((resolve, reject) => {
                game = new Phaser.Game({
                    type: Phaser.AUTO, parent: 'visual', transparent: true,
                    width: 640, height: 480,
                    scene: {create() {
                        const scene = this;
                        void (async () => {
                            await kapp.gameEngines.phaser.loadAvatarModule(scene);
                            if (closed) return;
                            await kapp.gameEngines.phaser.loadEffectsModule(scene);
                            if (closed) return;
                            avatar = scene.add.k_avatar(320, 410, avatarData, {sizeIndex: 0.4});
                            avatar.play('Idle', true);
                            status.textContent = 'Avatar prêt';
                            kapp.display();
                            scene.effects.play('01', {x: 320, y: 240}, {endCallback: resolve});
                        })().catch(reject);
                    }},
                });
            });
        })();
        await Promise.race([work, stopped, new Promise((_, reject) => {
            timer = setTimeout(() => reject(new Error('Chargement ou effet trop long')), 15000);
        })]);
        status.textContent = 'Effet terminé';
    } catch (error) {
        status.textContent = 'Affichage indisponible';
        throw error;
    } finally {
        cleanup();
    }
}

La fonction attend la fin de l’effet, détruit explicitement l’avatar, puis ce jeu dédié. L’avatar possède notamment un timer de clignement hors de la boucle Phaser : détruire seulement le jeu laisse ce timer appeler un objet Spine détruit. Elle ne termine pas l’appli ; son appelant choisit la suite et traite son rejet. Dans un widget, l’appeler avec await à l’intérieur de executeWidgetFunction, sans médias intégrés concurrents involontaires. Pour une scène durable, conserver le jeu et gérer les objets selon le cycle de vie du jeu ; ne pas utiliser cette recette comme boucle de création par frame.

La destruction du jeu est finalisée par la boucle Phaser : son canvas peut encore être présent à la résolution de cette fonction, puis disparaître au passage suivant. Pour vérifier le nettoyage, attendez le retrait effectif du canvas avec une attente bornée ; sa présence dans la même pile d’appel ne prouve pas une fuite. Vérifiez également l’absence d’erreurs tardives après destruction et un nouvel appel dans la même appli.

scene.effects.play('01', {x,y}, options) retourne un sprite, pas une Promise. Le module détruit ce sprite à la fin de l’animation et appelle endCallback. Les identifiants fournis vont de '01' à '54'. avatar.play('Idle', true) boucle ; avatar.destroy() retire l’avatar si le jeu doit rester ouvert.

Le délai de la recette rend visible un chargement bloqué, mais n’annule pas les requêtes déjà parties des loaders. Les gardes empêchent de créer l’avatar ou l’effet après fermeture. Le runtime des modules ne garantit pas qu’un asset absent rejette toujours sa Promise : tester ce cas, puis vérifier les ressources et la console.

La zone 640 × 480 est locale à ce composant ; la placer dans le cadre donné par overlay-layout, sans double zoom. Adapter la caméra ou la disposition au besoin ; un canvas fixe ne devient pas automatiquement responsive. Une réussite des tests avec scène doublée ne prouve pas le rendu WebGL/Spine : prévoir aussi une recette visuelle sur les ressources réellement servies.

Dans une preview hors écran, vérifier aussi la cadence de la boucle : des assets chargés ne prouvent pas que l’animation avance normalement. Une attente peut expirer si requestAnimationFrame est ralenti. Pour isoler ce cas, une copie de diagnostic peut utiliser fps: {target: 60, forceSetTimeOut: true} dans la configuration du jeu. Cette comparaison utilise toujours le moteur réel, mais ne valide pas la cadence du rendu par défaut dans OBS ou dans un navigateur visible ; ne pas modifier silencieusement le moteur de production pour masquer un problème de preview.