ゲームの統計、またはGoogle AppsScriptを恐れて愛するのをやめた方法





こんにちは!今日は、ゲームデザイナーが何らかの形で出くわすトピックについてお話ししたいと思います。そして、このトピックは静的な作業での痛みと苦痛です。スタティックとは何ですか?要するに、これは、それが彼の武器の特性であろうと、ダンジョンとその住民のパラメーターであろうと、プレイヤーが相互作用するすべての永続的なデータです。 ゲームに100,500種類の異なる剣があり、それらすべてが突然基本ダメージを少し上げる必要があると想像してください。通常、この場合、古き良きExcelが利用され、結果が手動または通常の方法でJSON / XMLに挿入されますが、これは長く、面倒で、検証エラーが発生します。 GoogleSpreadsheetsと組み込みのGoogleSpreadsheetsがそのような目的にどのように適しているかを見てみましょう







Google Apps Scriptを使用すると、時間を節約できます。



私たちはのための静について話していることを事前に予約をしますF2Pすなわち、力学やコンテンツの補充の定期的な更新によって特徴付けられる-gamesやゲーム、サービス、上記のプロセスは±一定です。



したがって、同じ剣を編集するには、次の3つの操作を実行する必要があります。



  1. 現在の損傷指標を抽出します(既製の計算テーブルがない場合)。
  2. 古き良きExcelで更新された値を計算します;
  3. 新しい値をゲームJSONに転送します。


既製のツールがあり、それが自分に合っている限り、すべてが正常であり、慣れている方法で編集できます。しかし、ツールがない場合はどうなりますか?さらに悪いことに、ゲーム自体はありません、tk。それはまだ開発中ですか?この場合、既存のデータを編集することに加えて、データを保存する場所とその構造を決定する必要もあります。



ストレージを使用すると、それはまだ多かれ少なかれ明確で標準化されています。ほとんどの場合、静的は、VCSのどこかにある個別のJSONのセットです。..。もちろん、すべてがリレーショナル(またはそうではない)データベースに格納されている場合、または最悪の場合、XMLに格納されている場合は、よりエキゾチックなケースがあります。しかし、通常のJSONではなく、それらを選択した場合は、おそらくすでにその理由があります。これらのオプションのパフォーマンスと使いやすさは非常に疑わしいものです。



しかし、統計の構造とその編集に関しては、変更はしばしば急進的で毎日行われます。もちろん、状況によっては、通常のメモ帳++と通常のメモパッド++の効率に取って代わるものはありませんが、入力しきい値が低く、コマンドによる編集に便利なツールが必要です。



平凡で有名なGoogleSpreadsheetsは、そのようなツールとして個人的に思いついたものです。他のツールと同様に、長所と短所があります。国家公爵の観点からそれらを検討しようと思います。



長所 マイナス
  • 共同編集
  • 他のスプレッドシートから計算を転送すると便利です
  • マクロ(Google Appsスクリプト)
  • 編集履歴があります(セルまで)
  • Googleドライブおよびその他のサービスとのネイティブ統合


  • 数式がたくさんあるラグ
  • 個別の変更ブランチを作成することはできません
  • スクリプト実行の制限時間(6分)
  • ネストされたJSONの表示の難しさ




私にとって、プラスはマイナスを大幅に上回っていたので、この点で、提示された各マイナスの回避策を見つけることを試みることにしました。



最後に何が起こったのか?



Google Spreadsheetsでは、アンロードを制御するメインシートと、ゲームオブジェクトごとに1つずつ、残りのシートが含まれる個別のドキュメントが作成されています。

同時に、通常のネストされたJSONをフラットなテーブルに収めるために、自転車を少し作り直す必要がありました。次の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 }
	}
  }
}


テーブルでは、この構造は値のペア「フルパス」-「値」として表すことができます。ここから、次のような自作のパスマークアップ言語が生まれました。



  • テキストはフィールドまたはオブジェクトです
  • / -階層セパレータ
  • テキスト[] -配列
  • #number-配列内の要素のインデックス


したがって、JSONは次のようにテーブルに書き込まれます。







したがって、このタイプの新しいオブジェクトを追加すると、テーブルの別の列になり、オブジェクトに特別なフィールドがある場合は、キーパスにキーがある文字列のリストが展開されます。



ルートレベルと他のレベルへの分割は、テーブルでフィルターを使用するための追加の便利さです。残りの部分については、単純なルールが機能します。オブジェクトの値が空でない場合は、JSONに追加してアンロードします。



新しいフィールドがJSONに追加され、誰かがパスを間違えた場合、条件付き書式のレベルで次の通常の規則によってチェックされます。



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


そして今、荷降ろしプロセスについて。これを行うには、メインシートに移動し、#ACTION列でアップロードするオブジェクトを選択して...

Palpatine(͡°͜ʖ͡°)をクリックします







その結果、#OBJECTフィールドで指定されたシートからデータを取得してアンロードするスクリプトが起動します。 JSONに。アップロードパスは#PATHフィールドで指定され、ファイルがアップロードされる場所は、ドキュメントを表示しているGoogleアカウントに関連付けられている個人のGoogleドライブです。



#METHODフィールドでは、JSONのアップロード方法を構成できます。



  • 単一の場合-1つのファイルがオブジェクトの名前と同じ名前でアップロードされます(もちろん、絵文字なしで、読みやすくするためにここにあります)
  • 個別の場合-シートの各オブジェクトは個別のJSONにアンロードされます。


残りのフィールドは本質的に情報量が多く、アンロードの準備ができているオブジェクトの数と、最後にアンロードしたオブジェクトを理解できます。exportメソッドへの正直な呼び出し



を実装しようとしたときに、スプレッドシートの興味深い機能に出くわしました。画像に関数呼び出しを掛けることはできますが、この関数の呼び出しで引数を指定することはできません。少しの欲求不満の後、自転車で実験を続けることが決定され、データシート自体にマークを付けるというアイデアが生まれました。 したがって、たとえば、アンカー###データ###### end_data ###データシートのテーブルに表示され、アップロードする属性領域が決定されます。







ソースコード



したがって、JSONコレクションはコードレベルでどのように表示されますか。



  1. #OBJECTフィールドを取得して、この名前のシートのすべてのデータを探します



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


完了しました。次に、Googleドライブに移動し、そこでファイルを取得します。



Googleドライブのファイルをいじる必要があったのはなぜですか。また、Gitに直接投稿しないのはなぜですか。基本的に-ファイルがサーバーに飛んで修復不可能なものをコミットする前にファイルをチェックできるようにするためだけです将来的には、ファイルを直接プッシュする方が高速になります。



正常に解決できなかったこと:さまざまなA / Bテストを実行する場合、データの一部が変更される静的の個別のブランチを作成することが常に必要になります。しかし、実際にはこれはdictの別のコピーであるため、A / Bテスト用にスプレッドシート自体をコピーし、その中のデータを変更して、そこからテスト用のデータをアンロードできます。



結論



そのような決定はどのように対処するのですか?驚くほど速い。この作業のほとんどがすでにスプレッドシートで行われている場合、適切なツールを使用することが開発時間を短縮するための最良の方法であることが判明しました。



ドキュメントはカスケード更新につながる式をほとんど使用していないため、速度を落とすものはほとんどありません。他のテーブルからバランス計算を転送するのに、通常は最小限の時間がかかるようになりました。目的のシートに移動し、フィルターを設定して値をコピーするだけです。



主なパフォーマンスのボトルネックはGoogleDrive APIです。ファイルの検索と削除/作成には最大の時間がかかります。一度にすべてのファイルをアップロードするのではなく、個別のファイルとしてではなく単一のJSONでシートをアップロードするだけです。



この倒錯のもつれが、まだ手と常連でJSONを編集している人や、GoogleSpreadsheetsの代わりにExcelで統計のバランス計算を行っている人に役立つことを願っています。



リンク



スプレッドシートエクスポーターのGoogleAppsスクリプトの

プロジェクトへのリンク



All Articles