Statika permainan, atau bagaimana saya berhenti merasa takut dan menyukai Skrip Google Apps





Salam pembuka! Hari ini saya ingin berbicara tentang satu topik yang ditemukan oleh setiap desainer game dengan satu atau lain cara. Dan topik ini adalah rasa sakit dan penderitaan, bekerja dengan statis . Apa itu statika? Singkatnya, ini adalah semua data konstan yang berinteraksi dengan pemain, apakah itu karakteristik senjatanya atau parameter dungeon dan penghuninya.



Bayangkan Anda memiliki 100.500 jenis pedang yang berbeda di dalam game dan semuanya tiba-tiba perlu sedikit meningkatkan kerusakan dasarnya. Biasanya, dalam hal ini, Excel lama yang baik dimanfaatkan, dan hasilnya kemudian dimasukkan ke dalam JSON / XML dengan tangan atau menggunakan reguler, tetapi ini panjang, merepotkan, dan penuh dengan kesalahan validasi.



Mari kita lihat bagaimana Google Spreadsheets dan Google Spreadsheets built-in dapat sesuai untuk tujuan seperti ituGoogle Apps Script dan dapat menghemat waktu untuk itu.



Saya akan membuat reservasi sebelumnya bahwa kita berbicara tentang statika untuk f2p -games atau layanan-permainan, yang ditandai dengan pembaruan rutin mekanik dan pengisian konten, mis. proses di atas adalah ยฑ konstan.



Jadi, untuk mengedit pedang yang sama, Anda perlu melakukan tiga operasi:



  1. ekstrak indikator kerusakan saat ini (jika Anda tidak memiliki tabel perhitungan yang sudah jadi);
  2. menghitung nilai yang diperbarui di Excel lama yang baik;
  3. mentransfer nilai baru ke JSON game.


Selama Anda memiliki alat yang siap pakai dan cocok untuk Anda, semuanya baik-baik saja dan Anda dapat mengedit seperti biasa. Tetapi bagaimana jika alat tersebut hilang? Atau lebih buruk lagi, tidak ada game itu sendiri. apakah masih dalam pengembangan Dalam hal ini, selain mengedit data yang ada, Anda juga perlu memutuskan di mana menyimpannya dan struktur apa yang akan dimilikinya.



Dengan penyimpanan, masih kurang lebih jelas dan terstandarisasi: dalam banyak kasus, statis hanyalah sekumpulan JSON terpisah yang ada di suatu tempat di VCS... Ada, tentu saja, kasus yang lebih eksotis ketika semuanya disimpan dalam database relasional (atau tidak begitu), atau, yang terburuk, dalam XML. Tapi, jika Anda memilihnya, dan bukan JSON biasa, kemungkinan besar Anda sudah punya alasan bagus untuk itu, karena kinerja dan kegunaan dari opsi-opsi ini sangat dipertanyakan.



Tetapi untuk struktur statika dan penyuntingannya - perubahan sering kali radikal dan harian. Tentu saja, dalam beberapa situasi, tidak ada yang dapat menggantikan efisiensi Notepad ++ biasa, ditambah dengan pelanggan tetap, tetapi kami masih menginginkan alat dengan ambang entri yang lebih rendah dan kenyamanan untuk mengedit dengan perintah.



Sebagai alat seperti itu, Google Spreadsheets yang dangkal dan terkenal datang kepada saya secara pribadi. Seperti alat apa pun, ia memiliki pro dan kontra. Saya akan mencoba untuk mempertimbangkan mereka dari sudut pandang Duma Negara.



pro Minus
  • Pengeditan bersama
  • Lebih mudah mentransfer kalkulasi dari spreadsheet lain
  • Makro (Skrip Google Apps)
  • Ada riwayat edit (sampai ke sel)
  • Integrasi asli dengan Google Drive dan layanan lainnya


  • Tertinggal dengan banyak rumus
  • Anda tidak dapat membuat cabang perubahan terpisah
  • Batas waktu untuk menjalankan skrip (6 menit)
  • Kesulitan menampilkan JSON bertingkat




Bagi saya, nilai plusnya lebih besar daripada minusnya secara signifikan, dan dalam hal ini, diputuskan untuk mencoba mencari solusi untuk setiap minus yang disajikan.



Apa yang terjadi pada akhirnya?



Di Google Spreadsheets, dokumen terpisah telah dibuat, di mana ada lembar Utama, tempat kami mengontrol pembongkaran, dan sisa lembaran, satu untuk setiap objek permainan.

Pada saat yang sama, untuk menyesuaikan JSON bersarang yang biasa ke dalam meja datar, sepeda perlu sedikit diciptakan kembali. Katakanlah kami memiliki JSON berikut:



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


Dalam tabel, struktur ini dapat direpresentasikan sebagai pasangan nilai "jalur penuh" - "nilai". Dari sini lahirlah bahasa markup jalan buatan sendiri di mana:



  • teks adalah bidang atau objek
  • / - pemisah hierarki
  • teks [] - larik
  • #number - indeks elemen dalam larik


Dengan demikian, JSON akan ditulis ke tabel sebagai berikut:







Dengan demikian, menambahkan objek baru jenis ini adalah kolom lain di tabel dan, jika objek memiliki bidang khusus, memperluas daftar string dengan kunci di keypath.



Pembagian menjadi root dan level lainnya adalah kemudahan tambahan untuk menggunakan filter dalam tabel. Selebihnya, aturan sederhana berfungsi: jika nilai dalam objek tidak kosong, maka kami akan menambahkannya ke JSON dan membongkar.



Jika bidang baru ditambahkan ke JSON dan seseorang membuat kesalahan di jalur, itu diperiksa oleh regular regular berikut pada level pemformatan bersyarat:



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


Dan sekarang tentang proses bongkar muat. Untuk melakukan ini, buka lembar Utama, pilih objek yang diinginkan untuk dibongkar di kolom #ACTION dan ...

klik Palpatine (อก ยฐ อœส– อก ยฐ)







Sebagai hasilnya, skrip akan diluncurkan yang akan mengambil data dari lembar yang ditentukan di bidang #OBJECT dan membongkar mereka ke JSON. Jalur unggah ditentukan di bidang #PATH, dan lokasi tempat file akan diunggah adalah Google Drive pribadi Anda yang terkait dengan akun Google tempat Anda melihat dokumen.



Bidang #METHOD memungkinkan Anda mengonfigurasi cara Anda ingin mengunggah JSON:



  • Jika satu - satu file diunggah dengan nama yang sama dengan nama objeknya (tanpa emoji, tentunya di sini hanya untuk dibaca)
  • Jika terpisah - setiap objek dari sheet akan diturunkan ke JSON terpisah.


Bidang yang tersisa lebih bersifat informatif dan memungkinkan Anda memahami berapa banyak objek yang sekarang siap untuk dibongkar dan siapa yang terakhir membongkar.



Saat mencoba menerapkan panggilan jujur ke metode ekspor, saya menemukan fitur menarik dari spreadsheet: Anda dapat menggantung panggilan fungsi pada gambar, tetapi Anda tidak dapat menentukan argumen dalam panggilan fungsi ini. Setelah beberapa saat frustrasi, diputuskan untuk melanjutkan percobaan dengan sepeda dan ide untuk menandai lembar data itu sendiri lahir.



Jadi, misalnya, jangkar ### data ### dan ### end_data ### muncul di tabel pada lembar data, yang menentukan area atribut untuk pengunggahan.



Kode sumber



Oleh karena itu, seperti apa koleksi JSON pada tingkat kode:



  1. Kami mengambil bidang #OBYEK dan mencari semua data lembar dengan nama ini



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


Selesai! Sekarang buka Google Drive dan ambil file Anda di sana.



Mengapa perlu mengutak-atik file di Google Drive, dan mengapa tidak memposting langsung ke Git? Pada dasarnya - hanya agar Anda dapat memeriksa file sebelum mereka terbang ke server dan melakukan yang tidak dapat diperbaiki . Di masa mendatang, akan lebih cepat untuk melakukan push file secara langsung.



Apa yang tidak dapat diselesaikan secara normal: ketika melakukan berbagai pengujian A / B, selalu perlu untuk membuat cabang statika yang terpisah, di mana bagian datanya berubah. Tetapi karena sebenarnya ini adalah salinan lain dari dict, kami dapat menyalin spreadsheet itu sendiri untuk pengujian A / B, mengubah data di dalamnya dan dari sana membongkar data untuk pengujian.



Kesimpulan



Bagaimana keputusan seperti itu berakhir? Sangat cepat. Asalkan sebagian besar pekerjaan ini sudah dilakukan di spreadsheet, menggunakan alat yang tepat ternyata merupakan cara terbaik untuk mengurangi waktu pengembangan.



Karena fakta bahwa dokumen hampir tidak menggunakan rumus yang mengarah pada pembaruan berjenjang, praktis tidak ada yang perlu diperlambat. Mentransfer perhitungan saldo dari tabel lain sekarang umumnya membutuhkan waktu minimum, karena Anda hanya perlu pergi ke lembar yang diinginkan, mengatur filter, dan menyalin nilai.



Hambatan kinerja utama adalah API Google Drive: mencari dan menghapus / membuat file membutuhkan waktu maksimum, hanya mengunggah tidak semua file sekaligus atau mengunggah lembar bukan sebagai file terpisah, tetapi dalam satu bantuan JSON.



Saya berharap jalinan penyimpangan ini akan bermanfaat bagi mereka yang masih mengedit JSON dengan tangan dan pelanggan tetap mereka, serta melakukan perhitungan keseimbangan statika di Excel alih-alih Google Spreadsheets.



Tautan



Contoh tautan pengekspor spreadsheet

ke proyek di Google Apps Script



All Articles