Aller au contenu principal

Multijoueur

Tout ce qui suit n'est actif que si votre projet a le multijoueur activé et que le lecteur a ouvert une salle.

Le reste du temps, ces appels ne font discrètement rien, et c'est délibéré : un script écrit pour le multijoueur tourne parfaitement bien dans un aperçu solo. Vous n'avez pas besoin de deux versions.

ctx.net.id;        // l'identifiant de ce participant
ctx.net.peers(); // tout le monde actuellement dans la salle
ctx.net.onJoin((peer) => {});
ctx.net.onLeave((peer) => {});

Ce qui se réplique tout seul

Seulement les positions des corps physiques marqués partagés. Tout le reste, vous l'envoyez vous-même.

Cela semble peu, et c'est exactement le bon peu : une caisse, une porte ou une balle doivent être un objet sur lequel tout le monde s'accorde, tandis qu'un personnage joueur ne le doit surtout pas — sinon une seule personne qui marche déplacerait l'avatar de tous. → propriété

Messages

Pour les événements : quelqu'un a marqué, une manche a commencé, une porte a été ouverte.

ctx.net.send('score', { points: 10 });
ctx.net.on('score', ({ from, data }) => {
/* … */
});

On envoie et on oublie, à tout le monde sauf soi.

État partagé

Pour les valeurs sur lesquelles tout le monde doit s'accorder. C'est une simple table où la dernière écriture l'emporte, répliquée à tous.

ctx.net.state.set(ctx.net.id, { name, colour, ready: true });
ctx.net.state.get(somePeerId);
ctx.net.state.all();
ctx.net.state.on(ctx.net.id, (value) => {});

Indexer par ctx.net.id est le schéma standard pour l'état par joueur : chacun écrit son entrée et lit celles des autres.

Faire apparaître pour tout le monde

const id = ctx.net.spawn(props.bullet, { position, rotation });
ctx.net.despawn(id);

L'identifiant revient immédiatement, mais l'objet apparaît une fois que la salle l'a confirmé. Cet aller-retour est ce qui garde l'identifiant identique sur chaque client, ce qui permet ensuite à la physique de s'accorder dessus.

Seul celui qui a fait apparaître quelque chose peut le faire disparaître, et quitter la salle emporte vos apparitions avec vous.

Dessiner les autres en douceur

C'est la partie qui rate le plus souvent : elle a donc son appel dédié.

ctx.net.follow(otherPlayerObject, { position, rotation });
ctx.net.unfollow(otherPlayerObject);

Appelez-le avec chaque position que vous recevez — dix à vingt fois par seconde est normal — et le runtime écrit le transform à chaque image à votre place, en rejouant le tampon légèrement en retard pour toujours disposer de deux positions connues entre lesquelles se déplacer.

N'écrivez pas les positions reçues directement dans un transform

Les paquets arrivent bien moins souvent que les images : l'objet reste immobile puis saute, et les joueurs distants semblent avancer par à-coups plutôt que marcher. Adoucir vers le paquet le plus récent n'y change rien non plus — entre deux paquets il n'y a rien devant l'instant présent vers quoi tendre, donc cela cale puis tressaute.

Réinjecter la même position ne coûte rien : appeler follow à chaque image avec ce que vous avez sous la main est l'usage prévu.

Une organisation qui marche

Si vous partez de zéro, cette répartition des rôles vous mènera loin :

  • chaque joueur possède son propre personnage — un corps local, piloté par ses entrées ;
  • sa position part via net.state ou un canal, dix à vingt fois par seconde ;
  • les personnages des autres sont dessinés avec follow ;
  • les objets du monde sont des corps partagés, la physique les garde donc cohérents gratuitement ;
  • le score et l'état de la manche vivent dans net.state, écrits par celui qui fait autorité pour cet événement.

Suite : Dépannage — quand quelque chose ne se comporte pas bien.