Fermer

octobre 30, 2019

Ajouter le chat à notre jeu (partie 3)


À 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

Cette troisième partie de la série se concentrera sur l'ajout d'un client textuel pour le moteur de jeu créé dans la partie 2 . Fernando Doglio explique la conception architecturale de base, la sélection d'outils et les points forts du code en vous expliquant comment créer une interface utilisateur textuelle à l'aide de Node.js.

Je vous ai d'abord montré comment définir un projet comme celui-ci. et vous a donné les bases de l'architecture ainsi que les mécanismes derrière le moteur de jeu. Ensuite, je vous ai montré l'implémentation de base du moteur une API REST de base qui vous permet de parcourir un monde défini par JSON.

Aujourd'hui, je vais vous montrer comment créer un client texte old-school pour notre API en n’utilisant rien d’autre que Node.js.

Révision du dessin original

Lorsque j’ai proposé pour la première fois une structure filaire pour l’UI j’ai proposé quatre sections sur le écran:

( Grand aperçu )

Bien qu'en théorie, cela me semble correct, j'ai manqué le fait qu'il serait difficile de passer de l'envoi de commandes de jeu à des messages texte. manuellement, notre analyseur de commande s'assure qu'il est capable de discerner si nous essayons de communiquer avec le jeu ou avec nos amis.

Ainsi, au lieu d'avoir quatre sections sur notre écran, nous en avons maintenant trois. :

( Grand aperçu )

Il s'agit d'une capture d'écran réelle de la client de jeu final. Vous pouvez voir l’écran de jeu à gauche et le chat à droite, avec un seul champ de saisie commun en bas. Le module que nous utilisons nous permet de personnaliser les couleurs et certains effets de base. Vous serez en mesure de cloner ce code à partir de Github et de faire ce que vous voulez avec l'apparence.

Une mise en garde cependant: bien que la capture d'écran ci-dessus montre que le chat fonctionne dans le cadre de l'application, cet article reste concentré. sur la configuration du projet et la définition d’un cadre dans lequel nous pouvons créer une application dynamique à base d’interface texte. Nous nous concentrerons sur l’ajout de l’aide au clavardage dans le dernier et dernier chapitre de cette série.

Les outils dont nous aurons besoin

Bien que de nombreuses bibliothèques permettent de créer des outils CLI avec Node.js, en ajoutant un L'interface utilisateur basée sur le texte est une bête complètement différente à apprivoiser. En particulier, je n’ai trouvé qu’une bibliothèque (très complète, ce qui me permettrait de faire exactement ce que je voulais): Blessed .

Cette bibliothèque est très puissante et offre de nombreuses fonctionnalités ne sera pas utilisé pour ce projet (comme la projection d’ombres, le glisser-déposer, etc.). Il ré-implémente la totalité de la bibliothèque ncurses (une bibliothèque C qui permet aux développeurs de créer des interfaces utilisateur textuelles) qui n’a pas de liaisons Node.js, et ce directement en JavaScript; donc, si nous devions le faire, nous pourrions très bien vérifier son code interne (quelque chose que je ne recommanderais pas sauf si vous deviez absolument le faire).

Bien que la documentation de Blessed soit assez volumineuse, elle se compose principalement de détails individuels les uns des autres. méthode fournie (au lieu d’avoir des tutoriels expliquant comment utiliser ces méthodes ensemble) et il manque d’exemples partout. Il peut donc être difficile d’y creuser si vous devez comprendre comment une méthode particulière fonctionne. Cela dit, une fois que vous l'avez compris, tout fonctionne de la même manière, ce qui est un avantage considérable, car toutes les bibliothèques et même tous les langages (je vous regarde, PHP) ont une syntaxe cohérente.

Mais la documentation de côté; Le gros avantage de cette bibliothèque est qu’elle fonctionne avec les options JSON. Par exemple, si vous souhaitez dessiner une boîte dans le coin supérieur droit de l’écran, procédez comme suit:

 var box = blessed.box ({
  en haut: "0",
  à droite: '0',
  largeur: '50% ',
  hauteur: '50% ',
  content: 'Bonjour {bold} monde {/ bold}!',
  tags: true,
  frontière: {
    type: 'ligne'
  },
  style: {
    fg: 'blanc',
    bg: 'magenta',
    frontière: {
      fg: '# f0f0f0'
    },
    vol stationnaire: {
      bg: 'vert'
    }
  }
}); 

Comme vous pouvez l’imaginer, d’autres aspects de la boîte y sont également définis (comme sa taille), ce qui peut parfaitement être dynamique en fonction de la taille du terminal, du type de bordure et de couleurs, même en cas de survol. Si vous avez déjà fait du développement front-end, il y aura beaucoup de chevauchement entre les deux.

Ce que j'essaie de dire ici, c'est que tout ce qui concerne la représentation de la boîte est configuré via le Objet JSON transmis à la méthode box . Cela est parfait pour moi, car je peux facilement extraire ce contenu dans un fichier de configuration et créer une logique métier capable de le lire et de décider des éléments à afficher à l'écran. Plus important encore, cela nous aidera à avoir un aperçu de leur apparence une fois dessinés.

Ce sera la base de tout l'aspect de ce module relatif à l'interface utilisateur () dans une seconde! ).

Architecture du module

L'architecture principale de ce module repose entièrement sur les widgets d'interface utilisateur que nous allons montrer. Un groupe de ces widgets est considéré comme un écran et tous ces écrans sont définis dans un seul fichier JSON (que vous pouvez trouver dans le dossier / config ).

Ce fichier contient plus de 250 lignes. le montrer ici n'a aucun sens. Vous pouvez consulter l'intégralité du fichier en ligne, mais un petit extrait de celui-ci se présente comme suit:

 "screens": {
        "options principales": {
            "fichier": "./main-options.js",
            "éléments": {
                "nom d'utilisateur-demande": {
                    "type": "invite d'entrée",
                    "params": {
                        "position": {
                            "top": "0%",
                            "left": "0%",
                            "largeur": "100%",
                            "hauteur": "25%"
                        },
                        "content": "Entrez votre nom d'utilisateur:",
                        "inputOnFocus": true,
                        "frontière": {
                          "type": "ligne"
                        },
                        "style": {
                          "fg": "blanc",
                          "bg": "bleu",
                          "frontière": {
                              "fg": "# f0f0f0"
                          },
                          "survoler": {
                            "bg": "vert"
                          }
                        }
                    }
                },
                "Les options": {
                    "type": "fenêtre",
                    "params": {
                        "position": {
                            "top": "25%",
                            "left": "0%",
                            "largeur": "100%",
                            "hauteur": "50%"
                        },
                        "contenu": "Veuillez sélectionner une option:  n1. Rejoindre un jeu existant.  n2. Créer un nouveau jeu",
                        "frontière": {
                          "type": "ligne"
                        },
                        "style": {
                        // ...
                        }
                    }
                },
                "contribution": {
                    "type": "entrée",
                    "handlerPath": "../lib/main-options-handler",
                   // ...
                }
            }
        }

L'élément «screens» contiendra la liste des écrans dans l'application. Chaque écran contient une liste de widgets (que je couvrirai un peu) et chaque widget a sa définition spécifique à bénédictions et les fichiers de gestionnaire associés (le cas échéant).

Vous pouvez voir comment chaque élément «params» (à l'intérieur d'un particulier) représente l’ensemble des paramètres attendus par les méthodes que nous avons vues précédemment. Les autres clés définies ici aident à fournir un contexte sur le type de widgets à rendre et leur comportement.

Quelques points d'intérêt:

Gestionnaires d'écran

Chaque élément d'écran possède une propriété de fichier qui référence le code associé à cet écran. Ce code n'est rien d'autre qu'un objet qui doit avoir une méthode init (la logique d'initialisation de cet écran particulier se déroule à l'intérieur de celui-ci). En particulier, le moteur de l'interface utilisateur principale appellera cette méthode init de chaque écran, ce qui, à son tour, devrait être responsable de l'initialisation de la logique dont il peut avoir besoin (c'est-à-dire la configuration des événements de zones de saisie).

Voici le code de l'écran principal, où l'application demande au joueur de sélectionner une option pour démarrer un nouveau jeu ou en rejoindre un existant:

 const logger = require ("../ utils / logger")

module.exports = {
    init: fonction (éléments, interface utilisateur) {
        this.elements = elements
        this.UI = UI
        this.id = "main-options"
        this.setInput ()
    },

    moveToIDRequest: fonction (gestionnaire) {
        return this.UI.loadScreen ('id-request', (err,) => {
            
        })
    },

    createNewGame: function (gestionnaire) {
        handler.createNewGame (this.UI.gamestate.APIKEY, (err, gameData) => {
              this.UI.gamestate.gameID = gameData._id
              handler.joinGame (this.UI.gamestate, (err) => {
                return this.UI.loadScreen ('main-ui', {
                    flashmessage: "Vous avez rejoint le jeu" + this.UI.gamestate.gameID + "avec succès"
                }, (err,) => {
                    
                })
              })
            
          })
    },

    setInput: function () {
        
        let handler = require (this.elements ["input"] .meta.handlerPath)
        let input = this.elements ["input"] .obj
        let usernameRequest = this.elements ['username-request'] .obj
        let usernameRequestMeta = this.elements ['username-request'] .meta
        laisser question = usernameRequestMeta.params.content.trim ()


        usernameRequest.setValue (question)

        this.UI.renderScreen ()

         let validOptions = {
             1: this.moveToIDRequest.bind (this),
             2: this.createNewGame.bind (this)
         }

        usernameRequest.on ('submit', (nom d'utilisateur) => {

            logger.info ("Nom d'utilisateur:" + nom d'utilisateur)
            logger.info ("Nom d'utilisateur:" + nom d'utilisateur.replace (question, ''))
            this.UI.gamestate.playername = nomutilisateur.replace (question, '')

            input.focus ()

            input.on ('submit', (data) => {
                let command = input.getValue ()
                  if (! validOptions [+command]) {
                      this.UI.setUpAlert ("Option non valide:" + commande)
                      Renvoie this.UI.renderScreen ()
                  }
                  return validOptions [+command] (gestionnaire)
            })


        })
        retourne l'entrée
    }
}

Comme vous pouvez le constater, la méthode init appelle la méthode setupInput qui configure fondamentalement le bon rappel pour gérer les entrées de l'utilisateur. Ce rappel maintient la logique permettant de décider quoi faire en fonction de l'entrée de l'utilisateur (1 ou 2).

Gestionnaires de widgets

Certains des widgets (généralement des widgets d'entrée) ont une propriété handlerPath qui référence le fichier contenant la logique derrière ce composant particulier. Ce n'est pas la même chose que le gestionnaire d'écran précédent. Celles-ci ne se soucient guère des composants de l’UI. Ils gèrent plutôt la logique de liaison entre l'interface utilisateur et la bibliothèque que nous utilisons pour interagir avec des services externes (telle que l'API du moteur de jeu).

Types de widgets

Un autre ajout mineur à la définition JSON des widgets est le suivant: leurs types. Au lieu d’utiliser les noms bienheureux définis pour eux, j’en crée de nouveaux pour me donner plus de marge de manœuvre quant à leur comportement. Après tout, un widget de fenêtre peut ne pas toujours "afficher uniquement des informations", ou une zone de saisie peut ne pas toujours fonctionner de la même manière.

Il s'agissait principalement d'un geste préventif, juste pour m'assurer de pouvoir le faire si j'en avais besoin ultérieurement L'avenir, mais comme vous êtes sur le point de le voir, je n'utilise pas de nombreux types de composants de toute façon.

Plusieurs écrans

Bien que l'écran principal soit celui que je vous ai montré dans la capture d'écran ci-dessus, le jeu. nécessite quelques autres écrans pour pouvoir demander des choses telles que votre nom de joueur ou si vous créez une nouvelle session de jeu ou même si vous vous joignez à une session existante. La façon dont j'ai géré cela a été, encore une fois, par la définition de tous ces écrans dans le même fichier JSON. Et pour passer d'un écran à l'autre, nous utilisons la logique à l'intérieur des fichiers de gestionnaire d'écran.

Pour ce faire, utilisez simplement la ligne de code suivante:

 this.UI.loadScreen ('main-ui ', (err) => {
 if (err) this.UI.setUpAlert (err)
 }) 

Je vais vous montrer plus de détails sur la propriété d'interface utilisateur dans une seconde, mais j'utilise simplement cette méthode loadScreen pour restituer l'écran et sélectionner les bons composants dans JSON. fichier en utilisant la chaîne passée en paramètre. Très simple.

Échantillons de code

Il est maintenant temps de consulter la viande et les pommes de terre de cet article: les échantillons de code. Je vais simplement souligner quels sont, à mon avis, les petits bijoux qu’il contient, mais vous pouvez toujours consulter à tout moment le code source complet directement dans le référentiel .

Utilisation des fichiers de configuration pour la génération automatique L'interface utilisateur

J'ai déjà couvert une partie de ce sujet, mais je pense que cela vaut la peine d'explorer les détails derrière ce générateur. L’essentiel de son contenu (fichier index.js dans le dossier / ui ) est qu’il s’agit d’une enveloppe entourant l’objet Béni. Et la méthode la plus intéressante à l’intérieur de celle-ci est la méthode loadScreen .

Cette méthode saisit la configuration (via le module de configuration) pour un écran spécifique et passe en revue son contenu, en essayant de générer les bons widgets sur le type de chaque élément.

 loadScreen: function (sname, extras, done) {
        if (typeof extras == "function") {
            done = extras
        }

        let screen = config.get ('screens.' + sname)
        laisser screenElems = {}
   
        if (this.screenElements.length> 0) {// supprime l'écran précédent
            this.screenElements.map (e => e.detach ())
            this.screen.realloc ()
        }

        Object.keys (screen.elements) .forEach (eName => {
            let elemObj = null
            let element = screen.elements [eName]
            if (element.type == 'window') {
                elemObj = this.setUpWindow (element)
            }
            if (element.type == 'input') {
                elemObj = this.setUpInputBox (element)
            }

            if (element.type == 'invite d'entrée') {
                elemObj = this.setUpInputBox (element)
            }
            screenElems [eName] = {
                meta: element,
                obj: elemObj
            }
        })

        if (typeof extras === 'objet' && extras.flashmessage) {
            this.setUpAlert (extras.flashmessage)
        }


        this.renderScreen ()
        laissez logicPath = require (screen.file)
        logicPath.init (screenElems, this)
        terminé()
    },

Comme vous pouvez le constater, le code est un peu long, mais sa logique est simple:

  1. Il charge la configuration pour l'écran spécifique en cours;
  2. nettoie tous les widgets existants;
  3. Goes sur chaque widget et l'instancie;
  4. Si une alerte supplémentaire est transmise sous forme de message flash (concept fondamentalement que j'ai volé à Web Dev dans lequel vous configurez un message à afficher jusqu'à la prochaine actualisation); [19659053] Rendez l'écran réel
  5. Et enfin, demandez au gestionnaire d'écran et exécutez sa méthode "init".

Vous pouvez consulter le reste des méthodes – elles sont principalement liées à des widgets individuels et à la manière de les rendre.

Communication entre l'interface utilisateur et la logique applicative

Bien que, dans la grande échelle, l'interface utilisateur, le back-end et le serveur de discussion a tous une communication à plusieurs niveaux; l'interface frontale elle-même nécessite au moins une architecture interne à deux couches dans laquelle les éléments d'interface utilisateur purs interagissent avec un ensemble de fonctions qui représentent la logique de base dans ce projet particulier.

Le schéma suivant montre l'architecture interne du client de texte que nous utilisons. construisons:

( Grand aperçu )

Permettez-moi de vous expliquer un peu plus loin. Comme je l'ai mentionné plus haut, le loadScreenMethod créera des présentations d'interface utilisateur des widgets (il s'agit d'objets bénis). Mais ils font partie de l’objet logique d’écran sur lequel nous avons défini les événements de base (tels que onSubmit pour les zones de saisie).

Permettez-moi de vous donner un exemple concret. Voici le premier écran que vous voyez lors du démarrage du client d'interface utilisateur:

( Grand aperçu )

Cet écran comporte trois sections:

  1. Demande de nom d'utilisateur,
  2. Options de menu / informations ,
  3. Écran de saisie des options de menu

En gros, nous souhaitons demander le nom d’utilisateur, puis leur demander de choisir l’une des deux options (lancer un tout nouveau jeu ou en rejoindre un déjà existant). ).

Le code qui s’occupe de cela est le suivant:

 module.exports = {


    init: fonction (éléments, interface utilisateur) {
        this.elements = elements
        this.UI = UI
        this.id = "main-options"
        this.setInput ()
    },

    moveToIDRequest: fonction (gestionnaire) {
        return this.UI.loadScreen ('id-request', (err,) => {
            
        })
    },

    createNewGame: function (gestionnaire) {

        handler.createNewGame (this.UI.gamestate.APIKEY, (err, gameData) => {
              this.UI.gamestate.gameID = gameData._id
              handler.joinGame (this.UI.gamestate, (err) => {
                return this.UI.loadScreen ('main-ui', {
                    flashmessage: "Vous avez rejoint le jeu" + this.UI.gamestate.gameID + "avec succès"
                }, (err,) => {
                    
                })
              })
            
          })
    },

    setInput: function () {
        
        let handler = require (this.elements ["input"] .meta.handlerPath)
        let input = this.elements ["input"] .obj
        let usernameRequest = this.elements ['username-request'] .obj
        let usernameRequestMeta = this.elements ['username-request'] .meta
        laisser question = usernameRequestMeta.params.content.trim ()


        usernameRequest.setValue (question)

        this.UI.renderScreen ()

         let validOptions = {
             1: this.moveToIDRequest.bind (this),
             2: this.createNewGame.bind (this)
         }

        usernameRequest.on ('submit', (nom d'utilisateur) => {

            logger.info ("Nom d'utilisateur:" + nom d'utilisateur)
            logger.info ("Nom d'utilisateur:" + nom d'utilisateur.replace (question, ''))
            this.UI.gamestate.playername = nomutilisateur.replace (question, '')

            input.focus ()



            input.on ('submit', (data) => {
                let command = input.getValue ()
                  if (! validOptions [+command]) {
                      this.UI.setUpAlert ("Option non valide:" + commande)
                      Renvoie this.UI.renderScreen ()
                  }
                  return validOptions [+command] (gestionnaire)
            })


        })

        
        

        retourne l'entrée
    }
}

Je connais beaucoup de code, mais je me concentrerai sur la méthode init . La dernière chose qu’il fait est d’appeler la méthode setInput qui permet d’ajouter les bons événements aux bonnes zones de saisie

Par conséquent, avec ces lignes:

 let handler = require (this. éléments ["input"] .meta.handlerPath)
let input = this.elements ["input"] .obj
let usernameRequest = this.elements ['username-request'] .obj
let usernameRequestMeta = this.elements ['username-request'] .meta
laisser question = usernameRequestMeta.params.content.trim ()

Nous avons accès aux objets Bénis et obtenu leurs références afin de pouvoir ultérieurement organiser les événements submit . Donc, après avoir soumis le nom d'utilisateur, nous passons au deuxième bloc de saisie (littéralement avec input.focus () ).

Selon l'option choisie dans le menu, nous appelons une des méthodes suivantes:

  • createNewGame : crée un nouveau jeu en interagissant avec son gestionnaire associé;
  • moveToIDRequest : affiche l'écran suivant chargé de demander à l'identifiant de jeu de se joindre. 19659080] Communication avec le moteur de jeu

    Enfin et surtout (et suivant l'exemple ci-dessus), si vous frappez 2, vous remarquerez que la méthode createNewGame utilise les méthodes du gestionnaire createNewGame puis joinGame (rejoindre le jeu juste après l'avoir créé)

    Ces deux méthodes visent à simplifier l'interaction avec l'API du moteur de jeu. Voici le code pour le gestionnaire de cet écran:

     const request = require ("request"),
        config = require ("config"),
        apiClient = require ("./ apiClient")
    
    let API = config.get ("api")
    module.exports = {
    
        joinGame: function (apikey, gameId, cb) {
            apiClient.joinGame (apikey, gameId, cb)
        },
    
        createNewGame: function (apikey, cb) {
            request.post (API.url + API.endpoints.games + "? apikey =" + apikey, {// création du jeu
                corps: {
                    cartoucheid: config.get ("app.game.cartdrigename")
                },
                json: true
            }, (err, resp, body) => {
                cb (null, body)
            })
            
        }
    }
    

    Vous voyez ici deux manières différentes de gérer ce problème. La première méthode utilise en fait la classe apiClient qui encapsule à nouveau les interactions avec le GameEngine dans une autre couche d'abstraction.

    La seconde méthode effectue l'action directement en envoyant une requête POST au bon URL avec la bonne charge utile. Rien de spécial n'est fait après; nous renvoyons simplement le corps de la réponse à la logique de l'interface utilisateur.

    Remarque : Si vous êtes intéressé par la version complète du code source de ce client, vous pouvez vérifier. it out here .

    Final Words

    Il en est ainsi pour le client textuel de notre aventure textuelle. J'ai abordé:

    • Comment structurer une application client;
    • Comment j'ai utilisé Blessed comme technologie de base pour la création de la couche de présentation;
    • Comment structurer l'interaction avec les services dorsaux d'un client complexe; [19659053] Et heureusement, avec le référentiel complet disponible.

    Et bien que l'interface utilisateur ne ressemble peut-être pas exactement à la version d'origine, elle remplit son objectif. J'espère que cet article vous a donné une idée de la façon de concevoir une telle entreprise et que vous étiez enclin à l'essayer vous-même à l'avenir. Bien sûr, Blessed est un outil très puissant, mais vous devrez faire preuve de patience pour apprendre à l'utiliser et à naviguer dans leurs documents.

    Dans la dernière et dernière partie, je vais expliquer comment j'ai ajouté le serveur de discussion à la fois sur le back-end et pour ce client texte.

    Rendez-vous sur le suivant!

     Editorial formidable (dm, yk, il)




Source link