Estadísticas del juego, o cómo dejé de tener miedo y me encantó Google Apps Script





¡Saludos! Hoy me gustaría hablar sobre un tema con el que cualquier diseñador de juegos se encuentra de una forma u otra. Y este tema es el dolor y el sufrimiento, trabajando con estática . ¿Qué es la estática? En definitiva, estos son todos los datos permanentes con los que interactúa el jugador, ya sean las características de su arma o los parámetros de la mazmorra y sus habitantes.



Imagina que tienes 100.500 tipos diferentes de espadas en tu juego y todas de repente necesitan aumentar un poco su daño base. Por lo general, en este caso, se aprovecha el viejo Excel y los resultados se insertan en JSON / XML a mano o con regularidad, pero esto es largo, problemático y está plagado de errores de validación.



Veamos cómo las hojas de cálculo de Google y las hojas de cálculo de Google integradas pueden ser adecuadas para tales fines.Google Apps Script y es posible ahorrar tiempo en él.



Haré una reserva con anticipación de que estamos hablando de estática para juegos f2p o servicios de juegos, que se caracterizan por actualizaciones regulares de mecánicas y reposición de contenido, es decir. el proceso anterior es ± constante.



Entonces, para editar las mismas espadas, debe realizar tres operaciones:



  1. extraiga los indicadores de daños actuales (si no tiene tablas de cálculo listas para usar);
  2. calcular valores actualizados en el buen Excel antiguo;
  3. transferir nuevos valores a los JSON del juego.


Siempre que tenga una herramienta lista para usar y que le convenga, todo estará bien y podrá editar como está acostumbrado. Pero, ¿y si falta la herramienta? O peor aún, no hay juego en sí. ¿todavía está en desarrollo? En este caso, además de editar los datos existentes, también debe decidir dónde almacenarlos y qué estructura tendrá.



Con el almacenamiento, todavía es más o menos claro y estandarizado: en la mayoría de los casos, la estática es solo un conjunto de JSON separados que se encuentran en algún lugar del VCS... Hay, por supuesto, casos más exóticos en los que todo se almacena en una base de datos relacional (o no) o, lo peor de todo, en XML. Pero, si los eligió, y no JSON ordinario, lo más probable es que ya tenga buenas razones para eso, ya que el rendimiento y la usabilidad de estas opciones son muy cuestionables.



Pero en cuanto a la estructura de la estática y su edición, los cambios a menudo serán radicales y diarios. Por supuesto, en algunas situaciones, nada puede reemplazar la eficiencia del Notepad ++ regular, junto con los habituales, pero aún queremos una herramienta con un umbral de entrada más bajo y conveniencia para editar mediante un comando.



Las banales y conocidas hojas de cálculo de Google se me ocurrieron personalmente como una herramienta de este tipo. Como cualquier herramienta, tiene sus pros y sus contras. Intentaré considerarlos desde el punto de vista de la Duma Estatal.



pros Desventajas
  • Coedición
  • Es conveniente transferir cálculos de otras hojas de cálculo
  • Macros (secuencia de comandos de Google Apps)
  • Hay un historial de edición (hasta la celda)
  • Integración nativa con Google Drive y otros servicios


  • Retrasos con muchas fórmulas
  • No puede crear ramas de cambio separadas
  • Límite de tiempo para ejecutar scripts (6 minutos)
  • Dificultad para mostrar JSON anidados




Para mí, las ventajas superaron significativamente a las desventajas y, en este sentido, se decidió tratar de encontrar una solución para cada una de las desventajas presentadas.



¿Que pasó al final?



En Google Spreadsheets se ha realizado un documento aparte, en el que hay una hoja principal, donde controlamos la descarga, y el resto de hojas, una para cada objeto del juego.

Al mismo tiempo, para encajar el JSON anidado habitual en una mesa plana, era necesario reinventar un poco la bicicleta. Digamos que tenemos el siguiente JSON:



{
  "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 }
	}
  }
}


En las tablas, esta estructura se puede representar como un par de valores "ruta completa" - "valor". A partir de aquí nació un lenguaje de marcado de rutas de fabricación propia en el que:



  • el texto es un campo u objeto
  • / - separador de jerarquía
  • texto [] - matriz
  • #number - el índice del elemento en la matriz


Por lo tanto, el JSON se escribirá en la tabla de la siguiente manera: en







consecuencia, agregar un nuevo objeto de este tipo es otra columna en la tabla y, si el objeto tenía algún campo especial, luego expandir la lista de cadenas con claves en la ruta clave.



La división en raíz y otros niveles es una conveniencia adicional para usar filtros en una tabla. Por lo demás, funciona una regla simple: si el valor en el objeto no está vacío, lo agregaremos a JSON y lo descargaremos.



En caso de que se agreguen nuevos campos a JSON y alguien cometa un error en la ruta, el siguiente periódico regular lo verifica en el nivel de formato condicional:



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


Y ahora sobre el proceso de descarga. Para hacer esto, vaya a la hoja Principal, seleccione los objetos deseados para cargar en la columna #ACCIÓN y ...

haga clic en Palpatine (͡ ° ͜ʖ ͡ °)







Como resultado, se lanzará un script que tomará datos de las hojas especificadas en el campo #OBJECT y los descargará a JSON. La ruta de carga se especifica en el campo #PATH, y la ubicación donde se cargará el archivo es su Google Drive personal asociado con la cuenta de Google bajo la cual está viendo el documento.



El campo #METHOD te permite configurar cómo quieres subir JSON:



  • Si se carga un solo : un archivo con el nombre igual al nombre del objeto (sin emoji, por supuesto, están aquí solo para facilitar la lectura)
  • Si está separado , cada objeto de la hoja se descargará en un JSON separado.


Los campos restantes son de naturaleza más informativa y le permiten comprender cuántos objetos están ahora listos para descargar y quién los descargó por última vez.



Al intentar implementar una llamada honesta al método de exportación, encontré una característica interesante de las hojas de cálculo: puede colgar una llamada de función en una imagen, pero no puede especificar argumentos en la llamada de esta función. Tras un breve periodo de frustración, se decidió continuar el experimento con la bicicleta y nació la idea de marcar las propias fichas técnicas.



Así, por ejemplo, los anclajes ### data ### y ### end_data ### aparecieron en las tablas de las hojas de datos, mediante las cuales se determinan las áreas de atributos para cargar.



Códigos fuente



En consecuencia, cómo se ve la colección JSON a nivel de código:



  1. Tomamos el campo #OBJECT y buscamos todos los datos de la hoja con este nombre



    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;
    }
    


¡Hecho! Ahora vaya a Google Drive y recoja su archivo allí.



¿Por qué era necesario jugar con archivos en Google Drive y por qué no publicar directamente en Git? Básicamente, solo para que pueda verificar los archivos antes de que volaran al servidor y cometieran lo irreparable . En el futuro, será más rápido enviar archivos directamente.



Lo que no se pudo resolver normalmente: al realizar varias pruebas A / B, siempre se hace necesario crear ramas separadas de estática, en las que parte de los datos cambia. Pero como, de hecho, esta es otra copia del dictado, podemos copiar la hoja de cálculo para la prueba A / B, cambiar los datos que contiene y desde allí descargar los datos para la prueba.



Conclusión



¿Cómo se resuelve esa decisión? Sorprendentemente rápido. Siempre que la mayor parte de este trabajo ya se haya realizado en hojas de cálculo, el uso de la herramienta adecuada resultó ser la mejor manera de reducir el tiempo de desarrollo.



Debido al hecho de que el documento casi no usa fórmulas que conduzcan a actualizaciones en cascada, prácticamente no hay nada que ralentice. La transferencia de cálculos de saldo de otras tablas ahora generalmente toma un mínimo de tiempo, ya que solo tiene que ir a la hoja deseada, establecer filtros y copiar valores.



El principal cuello de botella para la productividad es la API de Google Drive: buscar y eliminar / crear archivos lleva el tiempo máximo, solo que no se cargan todos los archivos a la vez ni se carga una hoja como archivos separados, sino que en un solo JSON ayuda.



Espero que esta maraña de perversiones sea útil para aquellos que todavía están editando JSON con sus manos y usuarios habituales, además de hacer cálculos de balance de estática en Excel en lugar de hojas de cálculo de Google.



Enlaces



Ejemplo de un vínculo exportador de hoja de cálculo

a un proyecto en Google Apps Script



All Articles