Fermer

novembre 6, 2019

Ajouter le chat à notre jeu (Partie 4)


À propos de l'auteur

Fernando Doglio travaille depuis 13 ans en tant que développeur Web et en tant que responsable technique depuis quatre ans. À cette époque, il adorait les…
Plus d'informations sur
Fernando
Doglio

Voici la dernière partie d'une série sur la création d'un moteur d'aventure multijoueur en mode texte. Aujourd’hui, nous nous concentrerons sur l’ajout de la prise en charge de la discussion en ligne au client texte de partie 3 . Nous allons passer en revue la conception de base d'un serveur de discussion utilisant Node.js et socket.io, l'interaction de base avec l'interface utilisateur et la manière dont nous avons intégré le code de discussion dans l'interface utilisateur existante.

Toute plate-forme qui permet Le jeu collaboratif entre les personnes devra posséder une caractéristique très particulière: la capacité (en quelque sorte) des joueurs de se parler. C’est exactement pourquoi notre moteur d’aventure texte construit dans Node.js ne serait pas complet sans un moyen pour les membres du groupe de pouvoir communiquer entre eux. Et comme il s’agit bien d’une aventure text cette forme de communication sera présentée sous la forme d’une fenêtre de discussion.

Donc, dans cet article, je vais expliquer comment j’ai ajouté le support de discussion pour le client texte ainsi que la façon dont j'ai conçu un serveur de conversation rapide avec Node.js.

Parties précédentes de cette série

Retour au plan initial

Absence de compétences en conception, le modèle filaire / maquette original du client basé sur le texte que nous avons intégré au partie précédente de la série:

( Grand aperçu )

Le côté droit de cette image est destiné aux communications entre joueurs et a été planifié comme une discussion depuis le début. Ensuite, lors du développement de ce module particulier (le client texte), j’ai réussi à le simplifier comme suit:

( Grand aperçu )

Oui, nous avons déjà traité cette image dans le précédent. versement mais notre objectif était la moitié gauche. Aujourd’hui, cependant, nous nous concentrerons sur la moitié droite de ce que vous voyez là-bas. En d'autres termes:

  • Ajout de la possibilité d'extraire de manière réactive les données d'un service tiers et de mettre à jour une fenêtre de contenu.
  • Ajout du support à l'interface de commande pour les commandes de discussion. Changer essentiellement le mode de fonctionnement des commandes et ajouter un support, par exemple «envoyer un message au reste de l'équipe».
  • Créez un serveur de discussion de base sur le back-end qui peut faciliter la communication entre les équipes. 19659019] Permettez-moi de commencer par le dernier point avant de passer à la modification du code existant.

    Création du serveur de discussion

    Avant même d'examiner un code, l'une des premières choses à faire est de définir rapidement le portée de tout nouveau projet. Surtout avec celui-ci, nous devons nous assurer de ne pas consacrer beaucoup de temps à des fonctionnalités dont nous n'avons peut-être pas besoin pour notre cas d'utilisation particulier.

    Vous voyez, tout ce dont nous avons besoin est que les membres du parti puissent envoyer des messages les uns aux autres, mais lorsque l’on pense à un «serveur de discussion», d’autres caractéristiques nous viennent souvent à l’esprit (telles que les forums de discussion, les messages privés, les émoticônes, etc.).

    Donc, pour que notre travail soit gérable et Obtenez quelque chose qui fonctionne, voici ce que le module de serveur de discussion fera réellement:

    • Autorisez une seule salle par partie. Cela signifie que la pièce réelle d'une partie sera automatiquement créée lors de la création du jeu et lorsque le premier joueur commencera à jouer. Tous les autres membres du groupe rejoindront la même salle automatiquement et sans choix.
    • Les messages privés ne seront pas pris en charge. Il n'y a pas besoin d'être secret dans votre parti. Du moins pas dans cette première version.
      Les utilisateurs pourront uniquement envoyer des messages via le chat, rien d’autre.
    • Et pour que tout le monde soit au courant, la seule notification adressée à l’ensemble du groupe sera l’ajout de nouveaux joueurs au jeu. C’est tout.

    Le diagramme suivant montre la communication entre serveurs et clients. Comme je l'ai dit, les mécanismes sont assez simples, le point le plus important à souligner ici est le fait que nous gardons les conversations au sein des mêmes membres du parti:

    ( Grand aperçu )

    Les outils du travail

    Etant donné les restrictions ci-dessus et le fait que tout ce dont nous avons besoin est d’une connexion directe entre les clients et le serveur de discussion, nous allons résoudre ce problème avec un socket de mode ancienne. En d’autres termes, l’outil principal que nous utiliserons est socket.io (notez qu’il existe des services tiers qui fournissent des serveurs de discussion gérés, par exemple, mais pour ce faire, la équivaut à tuer un moustique avec un fusil de chasse).

    Avec socket.io vous pouvez établir une communication bidirectionnelle en temps réel et basée sur des événements entre le serveur et les clients. Contrairement à ce que nous avons fait avec le moteur de jeu, dans lequel nous avions publié une API REST, la connexion par socket offre un moyen de communication plus rapide.

    C'est exactement ce dont nous avons besoin: un moyen rapide de connecter clients et serveur, d'échanger des messages et d'envoyer des diffusions.

    Conception d’un serveur de discussion

    Bien que socket.io soit assez magique pour ce qui est de la gestion des sockets, ce n’est pas un serveur de discussion complet, il nous faut encore définir une logique pour l’utiliser.

    liste particulièrement petite de fonctionnalités, la conception de la logique interne de notre serveur devrait ressembler à ceci:

    • Le serveur devra prendre en charge au moins deux types d'événements différents:
      1. Nouveau message
        Celui-ci est évident, il nous faut pour savoir quand un nouveau message d'un client est reçu, nous aurons donc besoin de l'assistance pour ce type d'événement.
      2. Un nouvel utilisateur a rejoint
        Nous en avons besoin pour nous assurer que nous pourrons avertir le toute la partie lorsqu'un nouvel utilisateur rejoint le salon de discussion.
    • En interne, nous allons gérer des forums de discussion, même si ce concept ne sera pas public pour les clients. Au lieu de cela, ils n’enverront que l’ID du jeu (l’identifiant utilisé par les joueurs pour rejoindre le jeu). Avec cet ID, nous allons utiliser la fonctionnalité de salles de socket.io qui gère chacune de nos salles individuellement.
    • En raison du fonctionnement de socket.io, il laisse ouverte une session en mémoire attribuée automatiquement au socket créé pour chaque client. En d’autres termes, une variable est automatiquement affectée à chaque client, dans laquelle nous pouvons stocker des informations, telles que les noms de joueurs et la salle attribuée. Nous allons utiliser cette session de socket pour gérer certaines associations internes de salle client.
    Remarque à propos des sessions en mémoire

    Le stockage en mémoire n’est pas toujours la meilleure solution. Pour cet exemple particulier, je vais avec parce que cela simplifie le travail. Cela étant dit, une amélioration simple et efficace que vous pourriez mettre en œuvre si vous souhaitiez intégrer cela à un produit prêt à la production serait de le remplacer par un exemple Redis . De cette façon, vous conservez les performances en mémoire mais vous ajoutez une couche supplémentaire de fiabilité au cas où quelque chose se passerait mal et que votre processus mourrait.

    Cela étant dit, permettez-moi de vous montrer la mise en œuvre réelle.

    19659034] Bien que le projet complet puisse être vu sur GitHub le code le plus pertinent se trouve dans le fichier principal ( index.js ):

     // Setup basic express server
    let express = require ('express');
    let config = require ("config")
    laissez app = express ();
    let server = require ('http'). createServer (app);
    let io = require ('socket.io') (serveur);
    let port = process.env.PORT || config.get ('app.port');
    
    server.listen (port, () => {
      console.log ('Le serveur écoute sur le port% d', port);
    });
    
    laisser numUsers = 0;
    
    
    io.on ('connexion', (socket) => {
      let addedUser = false;
    
      // quand le client émet un 'nouveau message', ceci écoute et exécute
      socket.on (config.get ('chat.events.NEWMSG'), (data, done) => {
        let room = socket.roomname
        if (! socket.roomname) {
            socket.emit (config.get ('chat.events.NEWMSG'), "Vous ne faites pas encore partie d'une pièce")
            retour fait ()
        }
    
        // nous disons au client d'exécuter 'nouveau message'
        socket.to (socket.roomname) .emit (config.get ('chat.events.NEWMSG'), {
          chambre: chambre,
          nom d'utilisateur: socket.username,
          message: données
        });
        terminé()
      });
    
      socket.on (config.get ('chat.events.JOINROOM'), (data, done) => {
          console.log ("Demande de rejoindre une salle:", données)
    
          socket.roomname = data.roomname
          socket.username = data.username
          socket.join (data.roomname, _ => {
              socket.to (data.roomname) .emit (config.get ('chat.events.NEWMSG'), {
                nom d'utilisateur: 'serveur de jeu',
                message: socket.username + 'a rejoint la fête!'
              })
              done (null, {join: true})
          })
      })
    
      // quand l'utilisateur se déconnecte .. effectue ceci
      socket.on ('disconnect', () => {
        si (ajoutéUtilisateur) {
          --numUsers;
    
          // répète globalement que ce client est parti
          socket.to (socket.roomname) .emit ('utilisateur gauche', {
            nom d'utilisateur: socket.username,
            numUsers: numUsers
          });
        }
      });
    }); 

    C'est tout ce qu'il y a pour ce serveur particulier. Simple droit? Quelques notes:

    1. J'utilise le module de configuration pour gérer toutes mes constantes. Personnellement, j'aime ce module, il me simplifie la vie chaque fois que je dois garder les «chiffres magiques» dans mon code. Ainsi, tous les éléments de la liste des messages acceptés sur le port écouté par le serveur sont stockés et accessibles via celui-ci.
    2. Il convient de prêter attention à deux événements principaux, comme je l'ai dit précédemment.
      • Lorsqu'un nouveau message est envoyé. reçu, ce qui peut être vu en écoutant config.get ('chat.events.NEWMSG') . Ce code garantit également que vous n’essayez pas par accident d’envoyer un message avant de rejoindre une salle. Cela ne devrait pas arriver si vous implémentez correctement le client de conversation, mais juste au cas où ce type de vérification est toujours utile lorsque les autres écrivent les clients pour vos services.
      • Lorsqu'un nouvel utilisateur rejoint une salle. Vous pouvez voir cet événement sur l'auditeur config.get ('chat.events.JOINROOM') . Dans ce cas, il nous suffit d'ajouter l'utilisateur à la salle (là encore, cela est géré par socket.io, il suffit donc d'une seule ligne de code), puis nous transmettons à la salle un message indiquant à qui vient de rejoindre. La clé ici est que, en utilisant l'instance de socket du joueur qui rejoint, la diffusion sera envoyée à tout le monde dans la salle sauf le joueur . Encore une fois, le comportement fourni par socket.io nous n'avons donc pas à ajouter ceci.

    C'est tout ce qu'il y a dans le code serveur. Voyons maintenant comment j'ai intégré le code côté client. dans le projet client textuel.

    Mise à jour du code client

    Pour intégrer à la fois les commandes de discussion et de jeu, la zone de saisie en bas de l'écran doit analyser les entrées du joueur et décider de ce qu'elles Essayez de le faire.

    La règle est simple: si le joueur essaie d'envoyer un message à la partie, il lancera la commande avec le mot "chat", sinon ce ne sera pas le cas.

    Que se passe-t-il lors de l'envoi d'un message de discussion?

    La liste d'actions suivante est exécutée lorsque l'utilisateur appuie sur la touche Entrée:

    1. Une fois qu'une commande de discussion est trouvée, le code déclenche une nouvelle branche, où une bibliothèque cliente de discussion être utilisé et un nouveau message sera envoyé (émis via la connexion socket active) au serveur.
    2. Le serveur émettra t Le même message sera envoyé à tous les autres joueurs présents dans la salle.
    3. Un rappel (configuré au démarrage) écoutant les nouveaux événements sur le serveur sera déclenché. En fonction du type d'événement (un joueur a envoyé un message ou un joueur qui vient de rejoindre), nous afficherons un message sur la boîte de discussion (c'est-à-dire la zone de texte à droite).

    Le schéma suivant présente une représentation graphique. des étapes ci-dessus; dans l’idéal, elle devrait aider à visualiser les composants impliqués dans ce processus:

    ( Grand aperçu )

    Vérification des modifications du code

    Pour obtenir une liste complète des modifications et du fonctionnement du code complet, vous devrait consulter le répertoire complet sur Github . Ici, je vais rapidement passer en revue certains des éléments de code les plus pertinents.

    Par exemple, lors de la configuration de l'écran principal, vous devez maintenant établir la connexion avec le serveur de discussion et configurer le rappel pour la mise à jour de l'écran. boîte de discussion (zone rouge en haut du diagramme ci-dessus).

     setUpChatBox: function () {
            let handler = require (this.elements ["chatbox"] .meta.handlerPath)
            handler.handle (this.UI.gamestate, (err, evt) => {
                si (err) {
                    this.UI.setUpAlert (err)
                    Renvoie this.UI.renderScreen ()
                }
    
                if (evt.event == config.get ('chatserver.commands.JOINROOM')) {
                    this.elements ["chatbox"] .obj.insertBottom (["::You've joined the party chat room::"])
                    this.elements ["chatbox"] .obj.scroll ((config.get ("screens.main-ui.elements.gamebox.autoscrollspeed")) + 1)
                }
                if (evt.event == config.get ('chatserver.commands.SENDMSG')) {
                    this.elements ["chatbox"] .obj.insertBottom ([evt.msg.username + ' said :> ' + evt.msg.message])
                    this.elements ["chatbox"] .obj.scroll ((config.get ("screens.main-ui.elements.gamebox.autoscrollspeed")) + 1)
                }
                this.UI.renderScreen ()
            })
    
        }, 

    Cette méthode est appelée à partir de la méthode init, comme pour tout le reste. La fonction principale de ce code consiste à utiliser le gestionnaire attribué (le gestionnaire de chatbox) et à appeler sa méthode handle qui se connecte au serveur de discussion, puis configure le rappel (défini également ici). à déclencher quand quelque chose arrive (un des deux événements que nous supportons).

    La logique intéressante de l'extrait de code ci-dessus se trouve à l'intérieur du rappel, car c'est la logique utilisée pour mettre à jour la boîte de discussion.

    Le code qui se connecte au serveur et configure le rappel indiqué ci-dessus est le suivant:

     const io = require ('socket.io-client'),
        config = require ("config"),
        logger = require ("../ utils / logger")
    
    
    // Utilise https ou wss en production.
    let url = config.get ("chatserver.url")
    laisser socket = io (url)
    
    
    module.exports = {
    
        connect2Room: function (état de jeu, terminé) {
            socket.on (config.get ('chatserver.commands.SENDMSG'), msg => {
                done (null, {
                    événement: config.get ('chatserver.commands.SENDMSG'),
                    msg: msg
                })
            })
            socket.emit (config.get ("chatserver.commands.JOINROOM"), {
                nom de la salle: gamestate.gameID,
                nom d'utilisateur: gamestate.playername
            }, _ => {
                logger.info ("Chambre jointe!")
                gamestate.inroom = true
                done (null, {
                    événement: config.get ('chatserver.commands.JOINROOM')
                })
            })
            
        },
    
       handleCommand: fonction (commande, état du jeu, terminé) {
            logger.info ("Envoi de commande au serveur de discussion!")
            
            let message = command.split ("") .splice (1) .join ("")
    
            logger.info ("Message à envoyer:", message)
    
            if (! gamestate.inroom) {// premier envoi du message, alors rejoignez d'abord la salle
                logger.info ("Rejoindre une salle")
                laisser gameId = gamestate.game
                
        socket.emit (config.get ("chatserver.commands.JOINROOM"), {
                    nom de la salle: gamestate.gameID,
                    nom d'utilisateur: gamestate.playername
                }, _ => {
                    logger.info ("Chambre jointe!")
                    gamestate.inroom = true
                    updateGameState = true
    
                    logger.info ("Mise à jour de l'état du jeu ...")
                    socket.emit (config.get ("chatserver.commands.SENDMSG"), message, terminé)
                })
            } autre {
                logger.info ("Envoi d'un message au serveur de discussion:", message)
                socket.emit (config.get ("chatserver.commands.SENDMSG"), message, terminé)
            }
                
        }
    } 

    La méthode connect2room est celle appelée lors de la configuration de l'écran principal, vous pouvez voir comment nous avons configuré le gestionnaire pour les nouveaux messages et émettre l'événement lié à la connexion à une salle (qui le même événement est ensuite diffusé sur les autres joueurs côté serveur).

    L’autre méthode, est la suivante: handleCommand qui s’occupe de l’envoi du message de discussion au serveur (et le fait avec un simple socket.emit ). Celui-ci est exécuté lorsque commandHandler réalise qu'un message de discussion est en cours d'envoi. Voici le code pour cette logique:

     module.exports = {
        handle: fonction (statut de jeu, texte, terminé) {
            let command = text.trim ()
            if (command.indexOf ("chat") === 0) {// commande de chat
                chatServerClient.handleCommand (commande, gamestate, done)
            } autre {
                sendGameCommand (gamestate, text, done)
            }
        }
    } 

    C'est le nouveau code pour le gestionnaire de commandes, la fonction sendGameCommand est l'endroit où l'ancien code est maintenant encapsulé (rien n'y a changé).

    Et c'est tout pour l'intégration, encore une fois, le code qui fonctionne peut être téléchargé et testé à partir du référentiel complet .

    Final Thoughts

    Ceci marque la fin du chemin pour ce projet. Si vous y tenez jusqu'au bout, merci de lire! Le code est prêt à être testé et utilisé, et si c'est le cas, contactez-moi et dites-moi ce que vous en avez pensé.

    Heureusement, avec ce projet, de nombreux fans du genre peuvent obtenir

    Amusez-vous à jouer (et à coder)

    Pour en savoir plus sur SmashingMag:

     Smashing Editorial (dm, yk , il)




Source link