Estática do jogo ou como parei de ter medo e adorei o Google Apps Script





Saudações! Hoje eu gostaria de falar sobre um assunto que qualquer designer de jogos encontra de uma forma ou de outra. E esse tópico é dor e sofrimento, trabalhar com estática . O que é estática? Enfim, são todos os dados permanentes com os quais o jogador interage, sejam as características de sua arma ou os parâmetros da masmorra e seus habitantes.



Imagine que você tem 100.500 tipos de espadas diferentes no jogo e todas elas de repente precisaram aumentar um pouco o dano básico. Normalmente, neste caso, o bom e velho Excel é aproveitado e os resultados são inseridos em JSON / XML manualmente ou usando regulares, mas isso é longo, problemático e repleto de erros de validação.



Vamos ver como as planilhas do Google e as planilhas integradas do Google podem ser adequadas para tais finsScript do Google Apps e você pode economizar tempo com isso.



Farei uma reserva antecipada de que estamos falando de estática para jogos f2p ou serviços de jogos, que se caracterizam por atualizações regulares da mecânica e reposição de conteúdo, ou seja, o processo acima é ± constante.



Portanto, para editar as mesmas espadas, você precisa realizar três operações:



  1. extrair os indicadores de danos atuais (se você não tiver tabelas de cálculo prontas);
  2. calcular valores atualizados no bom e velho Excel;
  3. transferir novos valores para jogos JSONs.


Contanto que você tenha uma ferramenta pronta e ela se adapte a você, está tudo bem e você pode editar da maneira que está acostumado. Mas e se não houver ferramenta? Ou pior ainda, não existe um jogo propriamente dito. ainda está em desenvolvimento? Nesse caso, além de editar os dados existentes, você também precisa decidir onde armazená-los e que estrutura terá.



Com o armazenamento, ainda é mais ou menos claro e padronizado: na maioria dos casos, estático é apenas um conjunto de JSONs separados em algum lugar do VCS... Existem, é claro, casos mais exóticos quando tudo é armazenado em um banco de dados relacional (ou não) ou, o pior de tudo, em XML. Mas, se você os escolheu, e não um JSON comum, provavelmente você já tem bons motivos para isso, porque o desempenho e a usabilidade dessas opções são altamente questionáveis.



Mas quanto à estrutura da estática e sua edição - as mudanças serão freqüentemente radicais e diárias. Claro, em algumas situações, nada pode substituir a eficiência do Notepad ++ normal, juntamente com os regulares, mas ainda queremos uma ferramenta com um limite de entrada mais baixo e conveniência para edição por um comando.



O banal e conhecido Google Spreadsheets veio até mim pessoalmente como tal ferramenta. Como qualquer ferramenta, ela tem seus prós e contras. Tentarei considerá-los do ponto de vista da Duma.



prós Minuses
  • Coedição
  • É conveniente transferir cálculos de outras planilhas
  • Macros (Google Apps Script)
  • Há um histórico de edição (até a célula)
  • Integração nativa com o Google Drive e outros serviços


  • Atrasos com muitas fórmulas
  • Você não pode criar ramos de mudança separados
  • Limite de tempo para execução de scripts (6 minutos)
  • Dificuldade em exibir JSONs aninhados




Para mim, os pontos positivos superaram os pontos negativos significativamente e, a esse respeito, decidiu-se tentar encontrar uma solução alternativa para cada um dos pontos negativos apresentados.



O que aconteceu no final?



No Google Spreadsheets, foi feito um documento separado, que contém a planilha principal, onde controlamos o descarregamento, e o restante das planilhas, uma para cada objeto do jogo.

Ao mesmo tempo, para encaixar o JSON aninhado usual em uma mesa plana, tivemos que reinventar um pouco a bicicleta. Digamos que temos o seguinte 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 }
	}
  }
}


Em tabelas, essa estrutura pode ser representada como um par de valores "caminho completo" - "valor". A partir daqui nasceu uma linguagem de marcação de caminho feita por você mesmo na qual:



  • o texto é um campo ou objeto
  • / - separador de hierarquia
  • text [] - array
  • # número - o índice do elemento na matriz


Assim, o JSON será gravado na tabela da seguinte forma:







Conseqüentemente, adicionar um novo objeto deste tipo é outra coluna na tabela e, se o objeto tiver algum campo especial, então expandir a lista de strings com chaves no caminho-chave.



A divisão em raiz e outros níveis é uma conveniência adicional para usar filtros em uma tabela. Para o resto, uma regra simples funciona: se o valor no objeto não estiver vazio, vamos adicioná-lo ao JSON e descarregá-lo.



Caso novos campos sejam adicionados ao JSON e alguém cometa um erro no caminho, ele é verificado pelo seguinte regular regular no nível da formatação condicional:



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


E agora sobre o processo de descarregamento. Para fazer isso, vá para a planilha principal, selecione os objetos desejados para upload na coluna #ACTION e ...

clique em Palpatine (͡ ° ͜ʖ ͡ °)







Como resultado, um script será iniciado que pegará os dados das planilhas especificadas no campo #OBJECT e os descarregará para JSON. O caminho de upload é especificado no campo #PATH e o local para onde o arquivo será carregado é o seu Google Drive pessoal associado à conta do Google sob a qual você está visualizando o documento.



O campo #METHOD permite que você configure como deseja fazer o upload do JSON:



  • Se um único arquivo for carregado com o nome igual ao nome do objeto (sem emoji, é claro, eles estão aqui apenas para facilitar a leitura)
  • Se separado - cada objeto da planilha será descarregado em um JSON separado.


Os campos restantes são de natureza mais informativa e permitem que você entenda quantos objetos estão agora prontos para serem descarregados e quem os descarregou por último.



Ao tentar implementar uma chamada honesta para o método de exportação, encontrei um recurso interessante das planilhas: você pode desligar uma chamada de função em uma imagem, mas não pode especificar argumentos na chamada desta função. Após um curto período de frustração, optou-se por continuar a experiência com a bicicleta e nasceu a ideia de marcar as próprias fichas.



Assim, por exemplo, âncoras ### data ### e ### end_data ### apareceram nas tabelas nas planilhas de dados, pelas quais as áreas de atributos para upload são determinadas.



Códigos-fonte



Da mesma forma, a aparência da coleção JSON no nível do código:



  1. Pegamos o campo #OBJECT e procuramos todos os dados da planilha com este nome



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


Feito! Agora vamos ao Google Drive e levamos nosso arquivo para lá.



Por que era necessário mexer em arquivos no Google Drive e por que não postar diretamente no Git? Basicamente - apenas para que você possa verificar os arquivos antes que eles voem para o servidor e confirmem o irreparável . No futuro, será mais rápido enviar arquivos diretamente.



O que não pôde ser resolvido normalmente: ao realizar vários testes A / B, sempre se torna necessário criar ramos separados da estática, nos quais parte dos dados muda. Mas como na verdade esta é outra cópia do dicionário, podemos copiar a própria planilha para o teste A / B, alterar os dados nela e a partir daí descarregar os dados para o teste.



Conclusão



Como essa decisão pode ser resolvida? Surpreendentemente rápido. Desde que grande parte desse trabalho já seja feito em planilhas, usar a ferramenta certa acabou sendo a melhor forma de reduzir o tempo de desenvolvimento.



Pelo fato de o documento quase não usar fórmulas que levam a atualizações em cascata, não há praticamente nada para desacelerar. Transferir cálculos de saldo de outras tabelas agora geralmente leva um mínimo de tempo, uma vez que você só precisa ir até a planilha desejada, definir filtros e copiar os valores.



O principal gargalo de desempenho é a API do Google Drive: pesquisar e excluir / criar arquivos leva o tempo máximo, apenas o upload não todos os arquivos de uma vez ou o upload de uma planilha não como arquivos separados, mas em um único JSON ajuda.



Espero que este emaranhado de perversões seja útil para aqueles que ainda estão editando JSONs com as mãos e regulares, bem como fazendo cálculos de estática de equilíbrio no Excel em vez de planilhas do Google.



Links



Exemplo de link de exportador de planilha

para um projeto no Google Apps Script



All Articles