Spielstatik oder wie ich aufgehört habe, Angst zu haben und Google Apps Script geliebt habe





Schöne Grüße! Heute möchte ich über ein Thema sprechen, auf das jeder Spieledesigner auf die eine oder andere Weise stößt. Und dieses Thema ist Schmerz und Leiden, Arbeiten mit statischer Aufladung . Was ist Statik? Kurz gesagt, dies sind alle permanenten Daten, mit denen der Spieler interagiert, sei es die Eigenschaften seiner Waffe oder die Parameter des Dungeons und seiner Bewohner.



Stellen Sie sich vor, Sie haben 100.500 verschiedene Schwerter im Spiel und alle müssen plötzlich ihren Grundschaden ein wenig erhöhen. In diesem Fall wird normalerweise das gute alte Excel genutzt, und die Ergebnisse werden dann von Hand oder unter Verwendung von Stammdaten in JSON / XML eingefügt. Dies ist jedoch langwierig, mühsam und mit Validierungsfehlern behaftet.



Lassen Sie uns sehen, wie Google Spreadsheets und die integrierten Google Spreadsheets für solche Zwecke geeignet sindGoogle Apps Script und ist es möglich, Zeit darauf zu sparen.



Ich werde im Voraus reservieren, dass es sich um Statik für f2p- Spiele oder Spieledienste handelt, die durch regelmäßige Aktualisierungen der Mechanik und Auffüllung von Inhalten gekennzeichnet sind, d. H. Der obige Prozess ist ± konstant.



Um dieselben Schwerter zu bearbeiten, müssen Sie drei Operationen ausführen:



  1. Extrahieren Sie die aktuellen Schadensindikatoren (wenn Sie keine vorgefertigten Berechnungstabellen haben).
  2. aktualisierte Werte in gutem alten Excel berechnen;
  3. Übertragen Sie neue Werte auf Spiel-JSONs.


Solange Sie ein fertiges Werkzeug haben und es zu Ihnen passt, ist alles in Ordnung und Sie können es so bearbeiten, wie Sie es gewohnt sind. Aber was ist, wenn das Tool fehlt? Oder noch schlimmer, es gibt kein Spiel selbst. ist es noch in der Entwicklung? In diesem Fall müssen Sie neben der Bearbeitung der vorhandenen Daten auch entscheiden, wo sie gespeichert werden sollen und welche Struktur sie haben sollen.



Mit Speicher ist es immer noch mehr oder weniger klar und standardisiert: In den meisten Fällen ist statisch nur ein Satz separater JSONs, die irgendwo im VCS liegen... Es gibt natürlich exotischere Fälle, in denen alles in einer relationalen (oder nicht relationalen) Datenbank oder, was am schlimmsten ist, in XML gespeichert ist. Aber wenn Sie sie gewählt haben und nicht gewöhnliches JSON, dann haben Sie höchstwahrscheinlich bereits gute Gründe dafür, weil Die Leistung und Verwendbarkeit dieser Optionen ist höchst fraglich.



Aber was die Struktur der Statik und ihre Bearbeitung betrifft, so sind die Änderungen oft radikal und täglich. Natürlich kann in einigen Situationen nichts die Effizienz von regulärem Notepad ++ in Verbindung mit regulären Elementen ersetzen, aber wir möchten weiterhin ein Tool mit einer niedrigeren Eingabeschwelle und einer bequemen Bearbeitung durch einen Befehl.



Die banalen und bekannten Google Spreadsheets kamen mir persönlich als solches Tool in den Sinn. Wie jedes Werkzeug hat es seine Vor- und Nachteile. Ich werde versuchen, sie aus Sicht der Staatsduma zu betrachten.



Profis Minuspunkte
  • Co-Bearbeitung
  • Es ist bequem, Berechnungen aus anderen Tabellen zu übertragen
  • Makros (Google Apps Script)
  • Es gibt einen Bearbeitungsverlauf (bis zur Zelle)
  • Native Integration mit Google Drive und anderen Diensten


  • Verzögerungen mit vielen Formeln
  • Sie können keine separaten Änderungszweige erstellen
  • Zeitlimit für die Ausführung von Skripten (6 Minuten)
  • Schwierigkeiten beim Anzeigen verschachtelter JSONs




Für mich überwogen die Pluspunkte die Minuspunkte erheblich, und in diesem Zusammenhang wurde beschlossen, für jedes der vorgestellten Minuspunkte eine Problemumgehung zu finden.



Was ist am Ende passiert?



In Google Spreadsheets wurde ein separates Dokument erstellt, in dem sich ein Hauptblatt befindet, in dem wir das Entladen steuern, und der Rest der Blätter, eines für jedes Spielobjekt.

Gleichzeitig war es notwendig, das Fahrrad ein wenig neu zu erfinden, um den üblichen verschachtelten JSON in einen flachen Tisch einzubauen. Angenommen, wir hatten den folgenden 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 }
	}
  }
}


In Tabellen kann diese Struktur als Wertepaar "vollständiger Pfad" - "Wert" dargestellt werden. Von hier aus wurde eine selbst erstellte Pfadmarkierungssprache geboren, in der:



  • Text ist ein Feld oder Objekt
  • / - Hierarchie-Trennzeichen
  • text [] - Array
  • #number - Der Index des Elements im Array


Daher wird der JSON wie folgt in die Tabelle geschrieben:







Dementsprechend ist das Hinzufügen eines neuen Objekts dieses Typs eine weitere Spalte in der Tabelle. Wenn das Objekt spezielle Felder enthält, wird die Liste der Zeichenfolgen mit Schlüsseln im Schlüsselpfad erweitert.



Die Unterteilung in Root- und andere Ebenen ist ein zusätzlicher Vorteil für die Verwendung von Filtern in einer Tabelle. Im Übrigen funktioniert eine einfache Regel: Wenn der Wert im Objekt nicht leer ist, fügen wir ihn JSON hinzu und entladen ihn.



Falls JSON neue Felder hinzugefügt werden und jemand einen Fehler auf dem Pfad macht, wird dies von der folgenden regulären Regel auf der Ebene der bedingten Formatierung überprüft:



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


Und nun zum Entladevorgang. Gehen Sie dazu zum Hauptblatt, wählen Sie die gewünschten Objekte zum Hochladen in der Spalte #ACTION aus und ...

klicken Sie auf Palpatine (͡ ° ͜ʖ ͡ °).







Als Ergebnis wird ein Skript gestartet, das Daten aus den im Feld #OBJECT angegebenen Blättern entnimmt und entlädt zu JSON. Der Upload-Pfad wird im Feld #PATH angegeben. Der Speicherort der Datei ist Ihr persönliches Google Drive, das dem Google-Konto zugeordnet ist, unter dem Sie das Dokument anzeigen.



Im Feld #METHOD können Sie konfigurieren, wie Sie JSON hochladen möchten:



  • Wenn Single- One-Dateien mit einem Namen hochgeladen werden, der dem Namen des Objekts entspricht (ohne Emoji sind sie natürlich nur zur besseren Lesbarkeit hier).
  • Wenn getrennt - Jedes Objekt aus dem Blatt wird in einen separaten JSON entladen.


Die verbleibenden Felder sind eher informativer Natur und ermöglichen es Ihnen zu verstehen, wie viele Objekte jetzt zum Entladen bereit sind und wer sie zuletzt entladen hat.



Beim Versuch, einen ehrlichen Aufruf der Exportmethode zu implementieren , bin ich auf eine interessante Funktion von Tabellenkalkulationen gestoßen: Sie können einen Funktionsaufruf an ein Bild hängen, aber Sie können keine Argumente im Aufruf dieser Funktion angeben. Nach einer kurzen Zeit der Frustration wurde beschlossen, das Experiment mit dem Fahrrad fortzusetzen, und die Idee, die Datenblätter selbst zu markieren, war geboren.



So wurden beispielsweise die Anker ### data ### und ### end_data ### in den Tabellen der Datenblätter angezeigt , anhand derer die Attributbereiche für das Hochladen bestimmt werden.



Quellcodes



Wie sieht die JSON-Sammlung auf Codeebene aus:



  1. Wir nehmen das Feld #OBJECT und suchen nach allen Daten des Blattes mit diesem Namen



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


Erledigt! Jetzt gehen wir zu Google Drive und nehmen unsere Datei dort.



Warum musste man mit Dateien in Google Drive herumspielen und warum nicht direkt auf Git posten? Grundsätzlich - nur damit Sie die Dateien überprüfen können, bevor sie zum Server geflogen sind und das irreparable festgeschrieben haben . In Zukunft wird es schneller sein, Dateien direkt zu pushen.



Was normalerweise nicht gelöst werden konnte: Bei der Durchführung verschiedener A / B-Tests müssen immer separate statische Zweige erstellt werden, in denen sich ein Teil der Daten ändert. Da dies jedoch eine weitere Kopie des Diktats ist, können wir die Tabelle selbst für den A / B-Test kopieren, die darin enthaltenen Daten ändern und von dort die Daten für den Test entladen.



Fazit



Wie kommt eine solche Entscheidung zustande? Überraschend schnell. Vorausgesetzt, der größte Teil dieser Arbeit wird bereits in Tabellenkalkulationen ausgeführt, erwies sich die Verwendung des richtigen Tools als der beste Weg, um die Entwicklungszeit zu verkürzen.



Aufgrund der Tatsache, dass das Dokument fast keine Formeln verwendet, die zu kaskadierenden Aktualisierungen führen, gibt es praktisch nichts zu verlangsamen. Das Übertragen von Saldoberechnungen aus anderen Tabellen dauert jetzt in der Regel minimal Sie müssen nur zum gewünschten Blatt gehen, Filter einstellen und Werte kopieren.



Der größte Leistungsengpass ist die Google Drive-API: Das Suchen und Löschen / Erstellen von Dateien nimmt die maximale Zeit in Anspruch. Es werden nur nicht alle Dateien gleichzeitig hochgeladen oder ein Blatt nicht als separate Dateien hochgeladen, sondern in einem einzigen JSON.



Ich hoffe, dass dieses Gewirr von Perversionen für diejenigen nützlich ist, die JSONs noch mit ihren Händen und Stammgästen bearbeiten und anstelle von Google Spreadsheets Bilanzberechnungen der Statik in Excel durchführen.



Links



Beispiel für einen Tabellenkalkulations-Exporter

Link zu einem Projekt in Google Apps Script



All Articles