游戏静力学,或者我如何不再害怕和喜欢Google Apps脚本





问候!今天,我想谈谈任何游戏设计师以一种或另一种方式遇到的一个话题。而这个话题是痛苦和苦难,需要动静。什么是静数?简而言之,这就是玩家与之互动的所有恒定数据,无论是他的武器的特性还是地牢及其居民的参数。



假设您在游戏中拥有100,500种不同的剑,而它们突然都需要稍微提高其基础伤害。通常,在这种情况下,将利用良好的旧Excel,然后将结果手动或使用常规将其插入JSON / XML中,但这很长,很麻烦并且充满验证错误。



让我们看看Google Spreadsheets和内置的Google Spreadsheets如何适合此类目的Google Apps脚本,您可以节省时间吗?



我会提前保留一下我们谈论的是f2p-游戏或游戏服务的静态特性,其特点是定期更新机制并补充内容,即 上述过程是恒定的。



因此,要编辑相同的剑,您需要执行以下三个操作:



  1. 提取当前的损坏指标(如果您没有现成的计算表);
  2. 在旧的Excel中计算更新值;
  3. 将新值传输到游戏JSON。


只要您有现成的工具并且适合您,一切都很好,您可以编辑惯用的方式。但是,如果没有工具怎么办?甚至更糟的是,tk没有游戏本身。还在开发中吗?在这种情况下,除了编辑现有数据之外,您还需要决定将数据存储在何处以及它将具有什么结构。



借助存储,它或多或少地变得清晰和标准化:在大多数情况下,静态只是位于VCS中某个位置的一组单独的JSON。...当然,在所有情况都存储在关系数据库中(或者不是存储在关系数据库中,或者最糟糕的是在XML中存储)的情况更加奇怪。但是,如果选择了它们,而不是普通的JSON,则很可能已经有充分的理由了,因为这些选项的性能和可用性非常值得怀疑。



但是对于静态结构及其编辑-更改通常会是激进的且每天都会进行。当然,在某些情况下,没有什么能替代常规记事本++和常规记录的效率,但是我们仍然希望使用入门阈值较低且便于通过命令进行编辑的工具。



平凡而著名的Google Spreadsheets就是这样一种工具。像任何工具一样,它也有其优点和缺点。我将尝试从国家杜马的角度考虑它们。



优点 缺点
  • 共同编辑
  • 从其他电子表格传输计算很方便
  • 宏(Google Apps脚本)
  • 有一个编辑历史记录(位于单元格下方)
  • 与Google云端硬盘和其他服务的本机集成


  • 落后于很多公式
  • 您不能创建单独的变更分支
  • 运行脚本的时间限制(6分钟)
  • 难以显示嵌套的JSON




对我而言,优点要比缺点要多得多,因此,在这方面,决定尝试为提出的每个缺点找到一种解决方法。



最后怎么着了?



Google Spreadsheets中已经制作了一个单独的文档,其中包含主工作表(我们在其中控制卸载)和其余工作表(每个游戏对象一个)。

同时,为了将通常的嵌套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的方式:



  • 如果是单个文件-上载一个名称与对象名称相同的文件(当然,没有表情符号,此处仅出于可读性考虑)
  • 如果是单独的,则工作表中的每个对象都将被卸载到单独的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测试时,始终有必要创建单独的静态分支,其中一部分数据会发生变化。但是,由于实际上这是字典的另一个副本,因此我们可以复制电子表格本身以进行A / B测试,更改其中的数据,然后从那里卸载测试数据。



结论



这样的决定最终如何应对?出奇的快。如果大多数工作已在电子表格中完成,则使用正确的工具被证明是减少开发时间的最佳方法。



由于该文档几乎没有使用导致级联更新的公式,因此实际上没有任何可放慢的速度。现在,从其他表格转移余额计算通常需要最少的时间,因为您只需要转到所需的工作表,设置过滤器并复制值即可。



生产力的主要瓶颈是Google Drive API:搜索和删除/创建文件需要花费最多的时间,仅一次上传不是所有文件,或者不是以单独的文件形式上传工作表,而是以单个JSON进行帮助。



我希望这种混乱的缠结对仍在用双手和正则编辑JSON的人,以及在Excel(而不是Google Spreadsheets)中进行静态平衡计算的人中很有用。



链接



电子表格导出程序 示例

链接到Google Apps脚本中的项目



All Articles