
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 é
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:
- extrair os indicadores de danos atuais (se você não tiver tabelas de cálculo prontas);
- calcular valores atualizados no bom e velho Excel;
- 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 |
---|---|
|
|
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:
- Pegamos o campo #OBJECT e procuramos todos os dados da planilha com este nome
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name)
- , ( , == )
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; }
- , ( ###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 }
- ###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; }
-
function GetJsonKeys(data){ var keys = []; for(var i=1; i<data.length; i++){ keys.push(data[i][0]) } return keys; }
-
// . // , 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) }
- 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
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