Le développement de jeux multijoueurs est compliqué pour de nombreuses raisons: leur hébergement peut être coûteux, la structure n'est pas évidente et la mise en œuvre est difficile. Dans ce tutoriel, je vais essayer de vous aider à surmonter la dernière barrière.
Cet article est destiné aux développeurs qui peuvent créer des jeux et qui connaissent JavaScript, mais qui n'ont jamais écrit de jeux en ligne multijoueurs auparavant. Après avoir terminé ce didacticiel, vous maîtriserez l'implémentation des composants réseau de base dans votre jeu et pourrez le développer en quelque chose de plus! Voici ce que nous allons créer:
Vous pouvez jouer le jeu terminé
ici ! Lorsque vous appuyez sur les touches W ou "haut", le vaisseau s'approche du curseur, lorsque vous cliquez sur la souris, il tire.
(Si personne n'est en ligne, pour vérifier le fonctionnement du multijoueur, ouvrez deux fenêtres de navigateur sur un ordinateur ou l'une d'entre elles sur le téléphone). Si vous souhaitez exécuter le jeu localement, le code source complet est disponible sur
GitHub .
Lors de la création du jeu, j'ai utilisé les ressources graphiques du
Kenney's Pirate Pack et du framework de jeu
Phaser . Dans ce didacticiel, vous êtes affecté au rôle de programmeur réseau. Le point de départ sera une version mono-utilisateur entièrement fonctionnelle du jeu, et notre tâche sera d'écrire un serveur sur Node.js en utilisant
Socket.io pour la partie réseau. Afin de ne pas surcharger le tutoriel, je vais me concentrer sur les parties liées au multijoueur et ignorer les concepts liés à Phaser et Node.js.
Vous n'avez pas besoin de configurer quoi que ce soit localement, car nous allons créer ce jeu complètement dans le navigateur sur
Glitch.com ! Glitch est un outil génial pour créer des applications Web, y compris des backends, des bases de données, etc. Il est idéal pour le prototypage, la formation et la collaboration, et je serai très heureux de vous présenter ses capacités dans ce tutoriel.
Commençons.
1. Préparation
J'ai posté l'ébauche du projet sur
Glitch.com .
Conseils d'interface: vous pouvez lancer l'aperçu de l'application en cliquant sur le bouton
Afficher (en haut à gauche).
La barre latérale verticale à gauche contient tous les fichiers d'application. Pour éditer cette application, vous devez créer son «remix». Nous allons donc en créer une copie dans notre compte (ou «fork» en jargon git). Cliquez sur le bouton
Remixer ce bouton.
À ce stade, vous modifiez l'application sous un compte anonyme. Pour enregistrer votre travail, vous pouvez vous connecter (en haut à droite).
Maintenant, avant de continuer, il est important que vous vous familiarisiez avec le jeu dans lequel nous ajouterons un mode multijoueur. Jetez un œil à
index.html . Il a trois fonctions importantes que vous devez connaître:
preload
(ligne 99),
create
(ligne 115) et
GameLoop
(ligne 142), ainsi que l'objet joueur (ligne 35).
Si vous préférez apprendre en vous entraînant, assurez-vous de comprendre le travail du jeu en effectuant les tâches suivantes:
- Augmentez la taille du monde (ligne 29) - notez qu'il existe une taille du monde distincte pour le monde du jeu et une taille de fenêtre pour le canevas de page lui-même .
- Permet d'avancer à l'aide de «l'espace» (ligne 53).
- Modifiez le type de navire du joueur (ligne 129).
- Ralentissez le mouvement des obus (ligne 155).
Installez Socket.io
Socket.io est une bibliothèque pour gérer les communications en temps réel à l'intérieur d'un navigateur à l'aide de
WebSockets (au lieu d'utiliser des protocoles comme UDP, qui sont utilisés pour créer des jeux multijoueurs classiques). De plus, la bibliothèque dispose de moyens redondants pour garantir son fonctionnement, même lorsque les WebSockets ne sont pas pris en charge. Autrement dit, elle s'occupe des protocoles de messagerie et permet l'utilisation d'un système de messagerie basé sur les événements.
La première chose que nous devons faire est d'installer le module Socket.io. Dans Glitch, cela peut être fait en accédant au fichier
package.json , puis en entrant le module requis dans les dépendances, ou en cliquant sur
Ajouter un package et en entrant «socket.io».
C'est le bon moment pour gérer les journaux du serveur. Cliquez sur le bouton
Journaux à gauche pour ouvrir le journal du serveur. Vous devriez voir qu'il installe Socket.io avec toutes ses dépendances. C'est là que vous devez rechercher toutes les erreurs et la sortie du code serveur.
Passons maintenant à
server.js . C'est là que se trouve notre code serveur. Jusqu'à présent, il n'y a que du code standard pour servir notre HTML. Ajoutez une ligne en haut du fichier pour activer Socket.io:
var io = require('socket.io')(http);
Maintenant, nous devons également activer Socket.io dans le client, revenons donc à
index.html et ajoutons les lignes suivantes à l'intérieur de la
<head>
:
<!-- Socket.io --> <script src="/socket.io/socket.io.js"></script>
Remarque: Socket.io traite automatiquement le chargement de la bibliothèque cliente le long de ce chemin, donc cette ligne fonctionne même s'il n'y a pas de répertoire /socket.io/ dans vos dossiers.Maintenant, Socket.io est inclus dans le projet et prêt à démarrer!
2. Reconnaissance et ponte des joueurs
Notre première vraie étape sera d'accepter les connexions sur le serveur et de créer de nouveaux joueurs dans le client.
Accepter les connexions au serveur
Ajoutez ce code au bas de
server.js :
Nous demandons donc à Socket.io d'écouter tous
connection
événements de
connection
qui se produisent automatiquement lorsqu'un client se connecte. La bibliothèque crée un nouvel objet
socket
pour chaque client, où
socket.id
est l'identificateur unique de ce client.
Pour vérifier que cela fonctionne, revenez au client (
index.html ) et ajoutez cette ligne quelque part dans la fonction de
création :
var socket = io();
Si vous démarrez le jeu et regardez le journal du serveur (cliquez sur le bouton
Journaux ), vous verrez que le serveur a enregistré cet événement de connexion!
Maintenant, lors de la connexion d'un nouveau joueur, nous attendons de lui qu'il nous donne des informations sur son état. Dans notre cas, nous devons connaître au moins
x ,
y et l'
angle afin de le créer correctement au bon point.
L'événement de
connection
était un événement en ligne déclenché par Socket.io. Nous pouvons écouter tous les événements définis indépendamment. Je nommerai mon
new-player
événement, et je m'attendrai à ce que le client l'envoie dès qu'il se connecte avec des informations sur sa position. Cela ressemblera à ceci:
Si vous exécutez ce code, jusqu'à ce que vous voyiez quoi que ce soit dans le journal du serveur, car nous n'avons pas encore dit au client de générer cet événement de
new-player
. Mais supposons un instant que nous l'avons déjà fait et continuons à travailler sur le serveur. Que devrait-il se passer après avoir obtenu l'emplacement d'un nouveau joueur se joignant?
Nous pouvons envoyer un message à tous les
autres joueurs connectés afin qu'ils sachent qu'un nouveau joueur est apparu. Socket.io a une fonction pratique pour cela:
socket.broadcast.emit('create-player',state_data);
Lorsque
socket.emit
appelé
socket.emit
message est simplement transmis à ce client unique. Lorsque
socket.broadcast.emit
est appelé
socket.broadcast.emit
il est envoyé à chaque client connecté au serveur, sauf sur le socket duquel cette fonction a été appelée.
La fonction
io.emit
envoie un message à chaque client connecté au serveur sans aucune exception. Dans notre schéma, nous n'en avons pas besoin, car si nous recevons un message du serveur nous demandant de créer notre propre vaisseau, nous obtiendrons un double du sprite, car nous avons déjà créé notre propre vaisseau au début du jeu.
Voici une astuce pratique sur les différents types de fonctionnalités de messagerie que nous utiliserons dans ce didacticiel.
Le code du serveur devrait maintenant ressembler à ceci:
Autrement dit, chaque fois qu'un joueur se connecte, nous nous attendons à ce qu'il nous envoie un message avec des informations sur sa position, et nous envoyons ces données à tous les autres joueurs afin qu'ils puissent créer son sprite.
Génération de clients
Maintenant, pour terminer ce cycle, nous devons effectuer deux actions dans le client:
- Générez un message avec les données de notre emplacement après la connexion.
- Écoutez les événements de
create-player
et créez un joueur à ce stade.
Pour effectuer la première action après avoir créé un joueur dans la fonction de
création (approximativement à la ligne 135), nous pouvons générer un message contenant les données de localisation que nous devons envoyer:
socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation})
Nous n'avons pas à nous soucier de la sérialisation des données envoyées. Vous pouvez les transférer dans n'importe quel type d'objet, et Socket.io le traitera pour nous.
Avant de continuer,
testez le code . Nous devrions voir un message similaire dans les journaux du serveur:
New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 }
Nous savons maintenant que notre serveur reçoit une notification sur la connexion d'un nouveau lecteur et lit correctement les données sur son emplacement!
Ensuite, nous voulons écouter les demandes de création d'un nouveau lecteur. Nous pouvons placer ce code immédiatement après avoir généré le message, il devrait ressembler à ceci:
socket.on('create-player',function(state){
Testez maintenant
le code . Ouvrez deux fenêtres avec le jeu et assurez-vous que cela fonctionne.
Vous devriez voir qu'après avoir ouvert deux clients, le premier client a créé deux navires et le second n'en a qu'un.
Tâche: pouvez-vous comprendre pourquoi cela s'est produit? Ou comment pouvez-vous résoudre ce problème? Suivez pas à pas la logique client / serveur que nous avons écrite et essayez de la déboguer.
J'espère que vous avez essayé de le découvrir par vous-même! Voici ce qui se passe: lorsque le premier joueur se connecte, le serveur envoie un événement
create-player
à tous les autres joueurs, mais aucun joueur ne peut encore le recevoir. Après avoir connecté le deuxième joueur, le serveur envoie à nouveau ses messages, et le premier joueur le reçoit et crée correctement le sprite, tandis que le deuxième joueur a raté le message du premier joueur.
Autrement dit, le problème est que le deuxième joueur se connecte au jeu plus tard et qu'il a besoin de connaître l'état du jeu. Nous devons informer tous les nouveaux joueurs connectés que les joueurs existent déjà (ainsi que d'autres événements qui ont eu lieu dans le monde) afin qu'ils puissent s'orienter. Avant de passer à la résolution de ce problème, j'ai un bref avertissement.
Avertissement de synchronisation de l'état du jeu
Il existe deux approches pour implémenter la synchronisation de tous les joueurs. La première consiste à envoyer un minimum d'informations sur les changements survenus sur le réseau. Autrement dit, chaque fois qu'un nouveau joueur est connecté, nous n'enverrons à tous les autres joueurs que des informations sur ce nouveau joueur (et enverrons une liste de tous les autres joueurs du monde à ce nouveau joueur), et après la déconnexion, nous informerons tous les joueurs que ce joueur particulier s'est déconnecté.
La deuxième approche consiste à transmettre l'état du jeu dans son intégralité. Dans ce cas, chaque fois que vous vous connectez ou vous déconnectez, nous envoyons à chacun une liste complète de tous les joueurs.
La première approche est meilleure en ce qu'elle minimise la quantité d'informations transmises sur le réseau, mais elle peut être très difficile à mettre en œuvre et il est probable que les joueurs ne soient pas synchronisés. La seconde garantit que les joueurs sont toujours synchronisés, mais chaque message devra envoyer plus de données.
Dans notre cas, au lieu d'essayer d'envoyer des messages lorsqu'un joueur est connecté pour le créer et lorsqu'il est déconnecté pour le supprimer, ainsi que lors d'un déplacement pour mettre à jour sa position, nous pouvons combiner tout cela en un seul événement de
update
commun. Cet événement de mise à jour enverra toujours les positions de chaque joueur à tous les clients. C'est ce que le serveur doit faire. La tâche du client est de maintenir la conformité du monde avec l'état reçu.
Pour mettre en œuvre un tel schéma, je ferai ce qui suit:
- Je garderai un dictionnaire des joueurs, dont la clé sera leur identifiant, et la valeur sera les données sur leur emplacement.
- Ajoutez un lecteur à ce dictionnaire lorsqu'il est connecté et envoyez un événement de mise à jour.
- Supprimez le lecteur de ce dictionnaire lorsqu'il est éteint et envoyez un événement de mise à jour.
Vous pouvez essayer d'implémenter ce système vous-même, car ces étapes sont assez simples (
mon conseil sur les fonctionnalités peut être utile ici). Voici à quoi pourrait ressembler l'implémentation complète:
Le côté client est un peu plus compliqué. D'une part, nous ne devrions plus nous préoccuper que de l'événement
update-players
, mais d'autre part, nous devrions envisager de créer de nouveaux navires si le serveur envoie plus de navires que nous ne le pensons, ou de les supprimer s'il y en a trop.
Voici comment je gère cet événement dans le client:
Côté client, je stocke les vaisseaux dans le dictionnaire
other_players
, que je viens de définir en haut du script (il n'est pas affiché ici). Étant donné que le serveur envoie les données des joueurs à tous les joueurs, je dois ajouter une vérification afin que le client ne crée pas de sprite supplémentaire pour lui-même. (Si vous avez des problèmes avec la structuration, alors voici le
code complet qui devrait être dans index.html pour le moment).
Testez maintenant
le code . Vous devriez pouvoir créer plusieurs clients et voir le nombre correct de navires créés dans les bonnes positions!
3. Synchronisation des positions des navires
Ici commence une partie très intéressante. Nous voulons synchroniser les positions des navires sur tous les clients. Cela révélera la simplicité de la structure que nous avons créée en ce moment. Nous avons déjà un événement de mise à jour qui peut synchroniser les emplacements de tous les navires. Il nous suffit de faire ce qui suit:
- Forcer le client à générer un message chaque fois qu'il se déplace vers une nouvelle position.
- Apprenez au serveur à écouter ce message de déplacement et à mettre à jour l'élément de données du joueur dans le dictionnaire des
players
. - Générez un événement de mise à jour pour tous les clients.
Et cela devrait suffire! C'est maintenant à votre tour d'essayer de l'implémenter vous-même.
Si vous êtes complètement confus et que vous avez besoin d'un indice, regardez le
projet terminé .
Remarque sur la minimisation des données transmises sur le réseau
Le moyen le plus simple de le mettre en œuvre est de mettre à jour les positions de tous les joueurs à chaque fois qu'un événement de mouvement est reçu d'
un joueur. C'est formidable si les joueurs obtiennent toujours les dernières informations immédiatement après leur apparition, mais le nombre de messages transmis sur le réseau peut facilement atteindre des centaines par image. Imaginez que vous avez 10 joueurs, chacun envoyant un message de mouvement dans chaque image. Le serveur doit les renvoyer aux 10 joueurs. C'est déjà 100 messages par trame!
Il serait préférable de le faire: attendez que le serveur reçoive tous les messages de tous les joueurs, puis envoyez à tous les joueurs une grande mise à jour contenant toutes les informations. Ainsi, nous réduirons le nombre de messages transmis au nombre d'utilisateurs présents dans le jeu (au lieu du carré de ce nombre). Le problème ici est que tous les utilisateurs connaîtront le même retard que le lecteur avec la connexion la plus lente.
Une autre solution consiste à envoyer les mises à jour du serveur à une fréquence constante, quel que soit le nombre de messages reçus du lecteur. Une norme courante consiste à mettre à jour le serveur environ 30 fois par seconde.
Cependant, lors du choix de la structure de votre serveur, vous devez évaluer le nombre de messages transmis dans chaque trame aux premiers stades du développement du jeu.
4. Synchronisation du shell
Nous avons presque fini! La dernière partie sérieuse est la synchronisation sur un réseau de coques. Nous pouvons l'implémenter de la même manière que les joueurs synchronisés:
- Chaque client envoie les positions de tous ses coques dans chaque trame.
- Le serveur les redirige vers chaque joueur.
Mais il y a un problème.
Protection contre la triche
Si vous redirigez tout ce que le client envoie en tant que vraies positions des obus, le joueur peut facilement tricher en modifiant son client et en vous transmettant de fausses données, par exemple, des obus se téléportant aux positions des navires. Vous pouvez facilement le vérifier vous-même en téléchargeant la page Web, en modifiant le code en JavaScript et en l'ouvrant à nouveau. Et c'est un problème non seulement pour les jeux par navigateur. Dans le cas général, nous ne pouvons jamais faire confiance aux données provenant de l'utilisateur.
Pour résoudre partiellement ce problème, nous allons essayer d'utiliser un autre schéma:
- Le client génère un message sur la coquille de tir avec sa position et sa direction.
- Le serveur simule le mouvement des obus.
- Le serveur met à jour les données de chaque client, en passant la position de tous les shells.
- Les clients rendent les obus dans les positions reçues du serveur.
Ainsi, le client est responsable de la position du projectile, mais pas de sa vitesse et non de son mouvement ultérieur. Le client peut changer la position des obus pour lui-même, mais cela ne changera pas ce que les autres clients voient.
Pour implémenter un tel schéma, nous ajouterons la génération de message lors du déclenchement. Je ne créerai plus le sprite lui-même, car son existence et son emplacement seront entièrement déterminés par le serveur. Maintenant, notre nouveau projectile tiré dans
index.html ressemblera à ceci:
De plus, nous pouvons maintenant commenter le fragment de code entier en mettant à jour les shells dans le client:
Enfin, nous devons obliger le client à écouter les mises à jour du shell. J'ai décidé de l'implémenter de la même manière qu'avec les joueurs, c'est-à-dire que le serveur envoie simplement un tableau de toutes les positions de shell dans un événement appelé
bullets-update
, et le client crée ou détruit des shells pour maintenir la synchronisation. Voici à quoi ça ressemble:
, . , , , ,
.
server.js . , :
var bullet_array = [];
:
60 :
— - ( for):
! , , . , , , . , , , , , , .
5.
Il s'agit de la dernière mécanique de base que nous implémentons. J'espère que vous êtes déjà habitué à la procédure de planification de votre implémentation, en complétant tout d'abord l'implémentation client, puis en passant au serveur (ou vice versa). Cette méthode est beaucoup moins sujette aux erreurs que de sauter lorsqu'elle est implémentée d'avant en arrière.La vérification des collisions est un mécanisme de jeu crucial, nous voulons donc qu'il soit protégé contre la tricherie. Nous l'implémentons sur le serveur de la même manière que nous l'avons fait avec les shells. Nous avons besoin des éléments suivants:- Vérifiez si le projectile est suffisamment proche de n'importe quel joueur sur le serveur.
- Générez un événement pour tous les clients lorsqu'un projectile frappe un joueur.
- Apprenez au client à écouter l'événement à succès et à faire clignoter le navire lorsqu'il est touché.
. , - 0:
player.sprite.alpha = 0;
( ). , - - :
for(var id in other_players){ if(other_players[id].alpha < 1){ other_players[id].alpha += (1 - other_players[id].alpha) * 0.16; } else { other_players[id].alpha = 1; } }
, ( ).
, , , . , .
6. Lissage de mouvement
Si vous avez terminé toutes les étapes jusqu'à ce point, je peux vous féliciter. Vous venez de créer un jeu multijoueur fonctionnel! Envoyez le lien à un ami et voyez comment la magie du multijoueur en ligne peut rassembler les joueurs!Le jeu est entièrement fonctionnel, mais notre travail ne s'arrête pas là. Il y a quelques problèmes qui peuvent affecter négativement le gameplay, et nous devons y faire face:- Si tout le monde n'a pas une connexion rapide, alors le mouvement des autres joueurs semble très nerveux.
- Les obus semblent lents, car ils ne sont pas tirés immédiatement. Avant d'apparaître sur l'écran du client, ils attendent un message de retour du serveur.
Nous pouvons résoudre le premier problème en interpolant nos données de position du navire dans le client. Par conséquent, si nous ne recevons pas les mises à jour assez rapidement, nous pouvons déplacer facilement le navire à l'endroit où il devrait être, et pas seulement le téléporter là-bas.Les coques nécessitent une solution plus complexe. Nous voulons que le serveur traite les obus pour se protéger contre la tricherie, mais nous avons également besoin d'une réaction instantanée: un tir et un projectile volant. La meilleure solution est une approche hybride. Le serveur et le client peuvent simuler des shells, et le serveur enverra toujours des mises à jour des positions des shells. S'ils ne sont pas synchronisés, nous supposons que le serveur a raison et redéfinissons la position du projectile dans le client.Nous n'implémenterons pas ce système shell dans ce tutoriel, mais il est agréable de savoir que cette méthode existe.L'interpolation simple des positions des navires est très simple. Au lieu de définir une position directement dans l'événement de mise à jour, où nous recevons d'abord de nouvelles données de position, nous enregistrons simplement la position cible:
Ensuite, dans la fonction de mise à jour (également côté client), nous bouclons tous les autres joueurs et les poussons vers leur objectif:
Ainsi, le serveur nous envoie des mises à jour 30 fois par seconde, mais nous pouvons toujours jouer à 60 fps et le jeu semble toujours aussi fluide!Conclusion
Nous avons examiné de nombreuses questions. Répertorions-les: nous avons appris à transférer des messages entre le client et le serveur, à synchroniser l'état du jeu, à le diffuser du serveur à tous les joueurs. C'est le moyen le plus simple de mettre en œuvre un jeu en ligne multijoueur.Nous avons également appris à protéger le jeu contre la triche, à simuler ses parties importantes sur le serveur et à informer les clients des résultats. Moins vous faites confiance au client, plus le jeu sera sûr., , . — ( ). — . , , , , .
— , . , , , . . . .
Glitch, (Advanced Options) :