Statique du jeu ou comment j'ai cessé d'avoir peur et aimé Google Apps Script





Salutations! Aujourd'hui, j'aimerais parler d'un sujet que tout concepteur de jeu aborde d'une manière ou d'une autre. Et ce sujet est la douleur et la souffrance, travailler avec l' électricité statique . Qu'est-ce que la statique? Bref, ce sont toutes les données constantes avec lesquelles le joueur interagit, que ce soit les caractéristiques de son arme ou les paramètres du donjon et de ses habitants.



Imaginez que vous ayez 100 500 types d'épées différents dans votre jeu et que tous doivent soudainement augmenter un peu leurs dégâts de base. Habituellement, dans ce cas, le bon vieil Excel est exploité, et les résultats sont ensuite insérés dans JSON / XML à la main ou en utilisant des habitués, mais c'est long, gênant et plein d'erreurs de validation.



Voyons comment les feuilles de calcul Google et les feuilles de calcul Google intégrées peuvent être adaptées à de telles finsGoogle Apps Script et est-il possible de gagner du temps dessus.



Je ferai une réservation à l'avance pour que nous parlions de statique pour les jeux f2p ou les services de jeux, qui se caractérisent par des mises à jour régulières de la mécanique et du réapprovisionnement du contenu, c'est-à-dire le processus ci-dessus est ± constant.



Donc, pour éditer les mêmes épées, vous devez effectuer trois opérations:



  1. extraire les indicateurs de dommages actuels (si vous ne disposez pas de tableaux de calcul prêts à l'emploi);
  2. calculer les valeurs mises à jour dans le bon vieil Excel;
  3. transférer de nouvelles valeurs aux JSON du jeu.


Tant que vous avez un outil prêt à l'emploi et qu'il vous convient, tout va bien et vous pouvez éditer comme vous en avez l'habitude. Mais que faire s'il n'y a pas d'outil? Ou pire encore, il n'y a pas de jeu en lui-même. est-il encore en développement? Dans ce cas, en plus de modifier les données existantes, vous devez également décider où les stocker et quelle structure elles auront.



Avec le stockage, c'est encore plus ou moins clair et standardisé: dans la plupart des cas, statique n'est qu'un ensemble de JSON séparés se trouvant quelque part dans le VCS... Il y a, bien sûr, des cas plus exotiques où tout est stocké dans une base de données relationnelle (ou pas), ou, pire que tout, en XML. Mais, si vous les avez choisis, et non JSON ordinaire, vous avez probablement déjà de bonnes raisons à cela, car les performances et la facilité d'utilisation de ces options sont très discutables.



Mais en ce qui concerne la structure de la statique et son édition, les changements seront souvent radicaux et quotidiens. Bien sûr, dans certaines situations, rien ne peut remplacer l'efficacité du Notepad ++ classique, couplé aux habitués, mais nous voulons tout de même un outil avec un seuil d'entrée inférieur et une facilité d'édition par une commande.



Les feuilles de calcul Google, banales et bien connues, m'ont été personnellement considérées comme un tel outil. Comme tout outil, il a ses avantages et ses inconvénients. J'essaierai de les considérer du point de vue de la Douma d'État.



avantages Moins
  • Coédition
  • Il est pratique de transférer des calculs à partir d'autres feuilles de calcul
  • Macros (script Google Apps)
  • Il y a un historique des modifications (jusqu'à la cellule)
  • Intégration native avec Google Drive et d'autres services


  • Retards avec beaucoup de formules
  • Vous ne pouvez pas créer des branches de modification distinctes
  • Délai d'exécution des scripts (6 minutes)
  • Difficulté à afficher les JSON imbriqués




Pour moi, les avantages l'emportaient considérablement sur les inconvénients, et à cet égard, il a été décidé d'essayer de trouver une solution de contournement pour chacun des inconvénients présentés.



Ce qui est arrivé à la fin?



Un document séparé a été créé dans Google Spreadsheets, qui contient la feuille principale, où nous contrôlons le déchargement, et le reste des feuilles, une pour chaque objet du jeu.

Dans le même temps, pour intégrer le JSON imbriqué habituel dans une table plate, nous avons dû réinventer un peu le vélo. Disons que nous avons le JSON suivant:



{
  "test_craft_01": {
    "id": "test_craft_01",
    "tags": [ "base" ],
	"price": [ {"ident": "wood", "count":100}, {"ident": "iron", "count":30} ],
	"result": {
		"type": "item",
		"id": "sword",
		"rarity_wgt": { "common": 100, "uncommon": 300 }
	}
  },
  "test_craft_02": {
    "id": "test_craft_02",
	"price": [ {"ident": "sword", "rarity": "uncommon", "count":1} ],
	"result": {
		"type": "item",
		"id": "shield",
		"rarity_wgt": { "common": 100 }
	}
  }
}


Dans les tableaux, cette structure peut être représentée par une paire de valeurs «chemin complet» - «valeur». De là est né un langage de balisage de chemin auto-créé dans lequel:



  • le texte est un champ ou un objet
  • / - séparateur de hiérarchie
  • text [] - tableau
  • #number - l'index de l'élément dans le tableau


Ainsi, le JSON sera écrit dans la table comme suit: En







conséquence, l'ajout d'un nouvel objet de ce type est une autre colonne dans la table et, si l'objet avait des champs spéciaux, alors étendre la liste des chaînes avec des clés dans le chemin de clé.



La division en niveaux racine et autres est une commodité supplémentaire pour l'utilisation de filtres dans une table. Pour le reste, une règle simple fonctionne: si la valeur de l'objet n'est pas vide, nous l'ajouterons au JSON et le déchargerons.



Dans le cas où de nouveaux champs sont ajoutés à JSON et que quelqu'un fait une erreur sur le chemin, cela est vérifié par le régulier suivant au niveau du formatage conditionnel:



=if( LEN( REGEXREPLACE(your_cell_name, "^[a-zA_Z0-9_]+(\[\])*(\/[a-zA_Z0-9_]+(\[\])*|\/\#*[0-9]+(\[\])*)*", ""))>0, true, false)


Et maintenant sur le processus de déchargement. Pour ce faire, allez dans la feuille principale, sélectionnez les objets désirés pour le téléchargement dans la colonne #ACTION et ...

cliquez sur Palpatine (͡ ° ͜ʖ ͡ °)







En conséquence, un script sera lancé qui prendra les données des feuilles spécifiées dans le champ #OBJET et les déchargera en JSON. Le chemin de téléchargement est spécifié dans le champ #PATH et l'emplacement où le fichier sera téléchargé est votre Google Drive personnel associé au compte Google sous lequel vous visualisez le document.



Le champ #METHOD vous permet de configurer la façon dont vous souhaitez télécharger JSON:



  • Si unique - un fichier est téléchargé avec un nom égal au nom de l'objet (sans emoji, bien sûr, ils ne sont ici que pour la lisibilité)
  • S'il est séparé , chaque objet de la feuille sera déchargé dans un JSON distinct.


Les champs restants sont de nature plus informative et vous permettent de comprendre combien d'objets sont maintenant prêts pour le déchargement et qui les a déchargés en dernier.



En essayant d'implémenter un appel honnête à la méthode d'exportation, je suis tombé sur une fonctionnalité intéressante des feuilles de calcul: vous pouvez accrocher un appel de fonction sur une image, mais vous ne pouvez pas spécifier d'arguments dans l'appel de cette fonction. Après une courte période de frustration, il a été décidé de poursuivre l'expérience avec le vélo et l'idée de marquer les fiches techniques elles-mêmes est née.



Ainsi, par exemple, les ancres ### data ### et ### end_data ### sont apparues dans les tableaux des feuilles de données, par lesquelles les zones d'attributs à télécharger sont déterminées.



Codes source



En conséquence, à quoi ressemble la collection JSON au niveau du code:



  1. On prend le champ #OBJECT et on cherche toutes les données de la feuille avec ce nom



    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
  2. , ( , == )



    function GetAnchorCoordsByName(anchor, data){
      var coords = { x: 0, y: 0 }
      
      for(var row=0; row<data.length; row++){
        for(var column=0; column<data[row].length; column++){
          if(data[row][column] == anchor){
            coords.x = column;
            coords.y = row;  
          }
        }
      }
      return coords;
    }
    
  3. , ( ###enable### true|false)



    function FilterActiveData(data, enabled){  
      for(var column=enabled.x+1; column<data[enabled.y].length; column++){
        if(!data[enabled.y][column]){
          for(var row=0; row<data.length; row++){
            data[row].splice(column, 1);
          }
          column--;
        }
      }
      return data
    }
    
  4. ###data### ###end_data###



    function FilterDataByAnchors(data, start, end){
      data.splice(end.y)
      data.splice(0, start.y+1);
      
      for(var row=0; row<data.length; row++){
        data[row].splice(0,start.x);
      }
      return data;
    }
    




  5. function GetJsonKeys(data){
      var keys = [];
      
      for(var i=1; i<data.length; i++){
        keys.push(data[i][0])
      }
      return keys;
    }
    




  6. //    . 
    // ,     single-file, -      . 
    // -    ,    separate JSON-
    function PrepareJsonData(filteredData){
      var keys = GetJsonKeys(filteredData)
      
      var jsonData = [];
      for(var i=1; i<filteredData[0].length; i++){
        var objValues = GetObjectValues(filteredData, i);   
        var jsonObject = {
          "objName": filteredData[0][i],
          "jsonBody": ParseToJson(keys, objValues)
        }
        jsonData.push(jsonObject)
      }  
      return jsonData;
    }
    
    //  JSON   ( -)
    function ParseToJson(fields, values){
      var outputJson = {};
      for(var field in fields){
        if( IsEmpty(fields[field]) || IsEmpty(values[field]) ){ 
          continue; 
        }
        var key = fields[field];
        var value = values[field];
        
        var jsonObject = AddJsonValueByPath(outputJson, key, value);
      }
      return outputJson;
    }
    
    //    JSON    
    function AddJsonValueByPath(jsonObject, path, value){
      if(IsEmpty(value)) return jsonObject;
      
      var nodes = PathToArray(path);
      AddJsonValueRecursive(jsonObject, nodes, value);
      
      return jsonObject;
    }
    
    // string     
    function PathToArray(path){
      if(IsEmpty(path)) return [];
      return path.split("/");
    }
    
    // ,    ,    - 
    function AddJsonValueRecursive(jsonObject, nodes, value){
      var node = nodes[0];
      
      if(nodes.length > 1){
        AddJsonNode(jsonObject, node);
        var cleanNode = GetCleanNodeName(node);
        nodes.shift();
        AddJsonValueRecursive(jsonObject[cleanNode], nodes, value)
      }
      else {
        var cleanNode = GetCleanNodeName(node);
        AddJsonValue(jsonObject, node, value);
      }
      return jsonObject;
    }
    
    //      JSON.    .
    function AddJsonNode(jsonObject, node){
      if(jsonObject[node] != undefined) return jsonObject;
      var type = GetNodeType(node);
      var cleanNode = GetCleanNodeName(node);
      
      switch (type){
        case "array":
          if(jsonObject[cleanNode] == undefined) {
            jsonObject[cleanNode] = []
          }
          break;
        case "nameless": 
          AddToArrayByIndex(jsonObject, cleanNode);
          break;
        default:
            jsonObject[cleanNode] = {}
      }
      return jsonObject;
    }
    
    //       
    function AddToArrayByIndex(array, index){
      if(array[index] != undefined) return array;
      
      for(var i=array.length; i<=index; i++){
        array.push({});
      }
      return array;
    }
    
    //    ( ,      )
    function AddJsonValue(jsonObject, node, value){
      var type = GetNodeType(node);
      var cleanNode = GetCleanNodeName(node);
      switch (type){
        case "array":
          if(jsonObject[cleanNode] == undefined){
            jsonObject[cleanNode] = [];
          }
          jsonObject[cleanNode].push(value);
          break;
        default:
          jsonObject[cleanNode] = value;
      }
      return jsonObject
    }
    
    //  .
    // object -      
    // array -     ,   
    // nameless -         ,     - 
    function GetNodeType(key){
      var reArray       = /\[\]/
      var reNameless    = /#/;
      
      if(key.match(reArray) != null) return "array";
      if(key.match(reNameless) != null) return "nameless";
      
      return "object";
    }
    
    //           JSON
    function GetCleanNodeName(node){
      var reArray       = /\[\]/;
      var reNameless    = /#/;
      
      node = node.replace(reArray,"");
      
      if(node.match(reNameless) != null){
        node = node.replace(reNameless, "");
        node = GetNodeValueIndex(node);
      }
      return node
    }
    
    //     nameless-
    function GetNodeValueIndex(node){
      var re = /[^0-9]/
      if(node.match(re) != undefined){
        throw new Error("Nameless value key must be: '#[0-9]+'")
      }
      return parseInt(node-1)
    }
    
  7. JSON Google Drive



    // ,    : ,   ( )  string  .
    function CreateFile(path, filename, data){
      var folder = GetFolderByPath(path) 
      
      var isDuplicateClear = DeleteDuplicates(folder, filename)
      folder.createFile(filename, data, "application/json")
      return true;
    }
    
    //    GoogleDrive   
    function GetFolderByPath(path){
      var parsedPath = ParsePath(path);
      var rootFolder = DriveApp.getRootFolder()
      return RecursiveSearchAndAddFolder(parsedPath, rootFolder);
    }
    
    //      
    function ParsePath(path){
      while ( CheckPath(path) ){
        var pathArray = path.match(/\w+/g);
        return pathArray;
      }
      return undefined;
    }
    
    //     
    function CheckPath(path){
      var re = /\/\/(\w+\/)+/;
      if(path.match(re)==null){
        throw new Error("File path "+path+" is invalid, it must be: '//.../'");
      }
      return true;
    }
    
    //         ,      , -    . 
    // -   , ..    
    function DeleteDuplicates(folder, filename){
      var duplicates = folder.getFilesByName(filename);
      
      while ( duplicates.hasNext() ){
        duplicates.next().setTrashed(true);
      }
    }
    
    //     ,         ,      
    function RecursiveSearchAndAddFolder(parsedPath, parentFolder){
      if(parsedPath.length == 0) return parentFolder;
       
      var pathSegment = parsedPath.splice(0,1).toString();
    
      var folder = SearchOrCreateChildByName(parentFolder, pathSegment);
      
      return RecursiveSearchAndAddFolder(parsedPath, folder);
    }
    
    //  parent  name,    - 
    function SearchOrCreateChildByName(parent, name){
      var childFolder = SearchFolderChildByName(parent, name); 
      
      if(childFolder==undefined){
        childFolder = parent.createFolder(name);
      }
      return childFolder
    }
    
    //    parent    name  
    function SearchFolderChildByName(parent, name){
      var folderIterator = parent.getFolders();
      
      while (folderIterator.hasNext()){
        var child = folderIterator.next();
        if(child.getName() == name){ 
          return child;
        }
      }
      return undefined;
    }
    


Terminé! Maintenant, nous allons sur Google Drive et y prenons notre fichier.



Pourquoi était-il nécessaire de manipuler des fichiers dans Google Drive, et pourquoi ne pas publier directement sur Git? Fondamentalement - uniquement pour que vous puissiez vérifier les fichiers avant qu'ils ne volent vers le serveur et commettent l'irréparable . À l'avenir, il sera plus rapide de pousser les fichiers directement.



Ce qui n'a pas pu être résolu normalement: lors de la réalisation de divers tests A / B, il est toujours nécessaire de créer des branches séparées de la statique, dans lesquelles une partie des données change. Mais comme il s'agit essentiellement d'une autre copie du dict, nous pouvons copier la feuille de calcul elle-même pour le test A / B, modifier les données qu'elle contient et à partir de là décharger les données pour le test.



Conclusion



Comment une telle décision finit-elle par faire face? Étonnamment rapide. À condition que la plupart de ce travail soit déjà effectué dans des feuilles de calcul, l'utilisation du bon outil s'est avérée être le meilleur moyen de réduire le temps de développement.



Du fait que le document n'utilise presque pas de formules qui conduisent à des mises à jour en cascade, il n'y a pratiquement rien à ralentir. Le transfert des calculs de solde à partir d'autres tables prend désormais généralement un minimum de temps, car il vous suffit d'aller sur la feuille souhaitée, de définir des filtres et de copier des valeurs.



Le principal goulot d'étranglement des performances est l'API Google Drive: la recherche et la suppression / la création de fichiers prennent le maximum de temps, ne téléchargeant pas tous les fichiers à la fois ou télécharger une feuille non pas en tant que fichiers séparés, mais dans un seul JSON aide.



J'espère que cet enchevêtrement de perversions sera utile pour ceux qui éditent encore des JSON avec leurs mains et leurs habitués, ainsi que pour faire des calculs d'équilibre de la statique dans Excel au lieu de Google Spreadsheets.



Liens



Exemple d' exportateur de feuille de calcul

Lien vers un projet dans Google Apps Script



All Articles