# 了解自定义特征

特征Feature,又译地物)是一种由一个或多个方块组成的在世界中发挥点缀地形效果的结构,我们在原版世界中看到的树木、花草、矿石乃至遗迹、村庄、湖泊、晶洞等地形都是通过特征来生成的。特征充当了玩家所看到的世界的各种微观细节的角色,有了特征的生成,世界中由噪声和生物群系产生的局部一成不变的地形结构才有了更多地细节变化。在本节中,我们一起来了解如何自定义一个特征。

# 了解特征文件的结构

在第六章中,我们曾经一起学习了如何通过编辑器自定义一个特征,不过同时也了解到了当前编辑器暂时只支持配置一个结构特征Structure Feature,又译结构地物)。这个结构特征其实是一种中国版独占的特征类型。除了这种中国版独占的特征类型之外,我们还有很多国际版提供的特征类型可以用于自定义。为了自定义更多类型的特征,我们需要手动编辑JSON文件来更改特征的类型和参数。在此之前,我们先一窥之前所创建的中国版结构特征的JSON内容:

{
  "format_version": "1.14.0",
  "netease:structure_feature": {
    "description": {
      "identifier": "tutorial_demo:demo_feature"
    },
    "places_structure": "tutorial_demo:pondwaterland"
  }
}

我们可以看到,我们的结构模板特征存放在行为包的netease_features文件夹中的。在这个文件夹中的结构的生成不需要开启实验性玩法,并且同时支持所有国际版的特征类型,所以我们之后的特征可以全部都放置在这个文件夹中。

netease:structure_feature是该特征的模式标识符Schema Identifier)。顾名思义,模式标识符就是用于确定该JSON文件的模式Schema)的标识符。之前我们了解过,JSON文件的模式决定了JSON文件使用的文件结构,比如,netease:structure_feature的模式便决定了其下有descriptionplaces_structure两个字段,缺一不可。再比如,minecraft:client_entity的模式决定了其下只能有一个description字段,不过description中倒是可以有很多各种功能的字段;而minecraft:entity模式决定了下面可以有一个description和一个components,而且components中可以存在许多行为包组件。而格式版本Format Version)则是在确定了模式标识符之后对各个字段的结构和属性根据游戏版本的变更进行的修改,这包括字段名称的修改、字段的必需和可选性的修改以及字段的增删等。比如,minecraft:client_entity1.8.0格式版本的description中存在animation_controllers数组,用于定义动画控制器,而1.10.0的格式版本则删除了这一字段,将动画控制器统一定义到了与动画相同的位置animations对象中。

事实上,如果你查看过原版的特征JSON文件,你应该可以注意到,特征的模式标识符并不像方块的minecraft:block、物品的minecraft:item、实体的客户端和服务端的minecraft:client_entityminecraft:entity那样是统一的。特征的模式标识符对于不同种类的特征将有所不同。比如中国版的结构特征的模式标识符便使用了netease:structure_feature,而国际版提供的各种特征类型却使用了其他的模式标识符。如minecraft:ore_featureminecraft:single_block_feature等。也就是说,不像物品、方块、实体在同一个模式标识符下定义描述和组件那样,特征是依据不同的特征类型使用不同的模式标识符,同时使用不同的模式结构的。所以对于每种类型的特征,我们都需要单独学习它的文件结构。

因为模式标识符并不一致,因此格式版本也应随之变化。中国版的结构特征netease:structure_feature的格式版本为1.14.0。除此之外,国际版的各种类型的特征的格式版本不尽相同,但是大部分都为1.13.0,另有少部分模式标识符使用1.16.01.16.100的格式版本。

接下来,我们通过分模式标识符来学习一些基本类型的特征。

# 自定义矿石特征

矿石特征的模式标识符为minecraft:ore_feature,惯用格式版本为1.13.0,也可以使用1.16.0。矿石特征旨在模拟矿石生成的矿床结构,事实上,任何方块都可以作为矿石特征的输入方块,并不一定必须是一个矿石。我们来看一个例子:

{
  "format_version": "1.13.0",
  "minecraft:ore_feature": {
    "description": {
      "identifier": "example:malachite_ore_feature"
    },
    "count": 12,
    "replace_rules": [
      {
        "places_block": "example:malachite_ore",
        "may_replace": [
          "minecraft:stone"
        ]
      },
      {
        "places_block": "example:granite_malachite_ore",
        "may_replace": [
          "minecraft:granite"
        ]
      },
      {
        "places_block": "example:andesite_malachite_ore",
        "may_replace": [
          "minecraft:andesite"
        ]
      }
    ]
  }
}

count为该矿石特征尝试替换的次数,而replace_rules则是替换规则。矿石特征的原理是将指定的一个或一些方块替换成特定的方块,往往是矿石方块。比如示例中,该特征尝试把minecraft:stone替换成example:malachite_oreminecraft:granite替换成example:granite_malachite_oreminecraft:andesite替换成example:andesite_malachite_ore。如果不填写replace_rules字段,则代表着方块可以替换任何的其他方块。

# 自定义单方块特征

单方块特征的模式标识符为minecraft:single_block_feature,惯用格式版本为1.13.0,也可以使用1.16.0。单方块特征旨在放置一个单独的方块,比如花草等小方块。在放置期间,我们可以可以像矿石特征那样设置可以替换的方块,也可以设置可以附着的方块,即我们可以使这个方块只能被附着地放置在某种或某些方块的某些面上。

{
  "format_version": "1.13.0",
  "minecraft:single_block_feature": {
    "description": {
      "identifier": "minecraft:cocoa_age_0_feature"
    },
    "places_block": {
      "name": "minecraft:cocoa",
      "states": {
        "age": 0
      }
    },
    "enforce_survivability_rules": false,
    "enforce_placement_rules": false,
    "may_attach_to": {
      "min_sides_must_attach": 1,
      "sides": {
        "name": "minecraft:log",
        "states": {
          "old_log_type": "jungle"
        }
      }
    }
  }
}

places_block是将要被放置的那个方块,这里使用了一个方块引用Block Reference)的格式。不仅是这里,所有可以单独填写一个方块标识符的位置也都可以像这样填写一个方块引用对象{"name": "<namespace:identifier>", "states": {"<state1>": <someValue>, "<state2>": <someValue> ... }}。在不指定方块状态states字段时,单独写一个方块标识符和写一个方块引用对象是等价的。而方块引用的好处就是可以用于指定特定的方块状态,在这里就是用于在特征中直接生成一个特定状态的方块。may_replace字段用于指定可以被替换的方块,是一个和上面矿石特征中may_replace一样的数组。在我们这个示例中没有使用这个may_replace字段,这个字段和places_block是平级的。may_attach_to也和places_block平级,用于指定可以附着到哪些方块的哪些面上。enforce_survivability_rulesenforce_placement_rules分别用于告知引擎是否开启方块的可生存性和可放置性检验。方块可以通过在行为包中设置minecraft:placement_filter方块组件来定义可放置性和可生存性。不过值得注意的是,这个方块组件是1.16.100之后的方块组件,如需使用请注意格式版本。

# 自定义散植特征

散植特征的模式标识符为minecraft:scatter_feature,惯用格式版本为1.13.0。该特征旨在将一个特征进行一次或多次散植Scatter),即相对于特征的起始点在其周围散布一次或多次某个指定的特征。

{
  "format_version": "1.13.0",
  "minecraft:scatter_feature": {
    "description": {
      "identifier": "minecraft:grass_double_plant_patch_feature"
    },
    "iterations": 64,
    "scatter_chance": {
      "numerator": 1,
      "denominator": 80
    },
    "coordinate_eval_order": "zyx",
    "x": {
      "extent": [ -8, 8 ],
      "distribution": "gaussian"
    },
    "z": 10,
    "y": "math.cos(math.random(0, 10))",
    "project_input_to_floor": false,
    "places_feature": "minecraft:grass_double_plant_feature"
  }
}

这是一个改造过的双层高草的斑块特征的示例,iterations代表迭代的次数,即散植的次数。scatter_chance代表散植的可能性,该字段可以为一个数字,代表百分比,比如50就代表“50%”的概率;也可以使用一个对象,该对象代表一个分数,其中numerator为分子,denominator为分母。我们称这种代表一个分数的对象为一个可能性信息Chance Information)。scatter_chance如果判定通过,则会进行iterations次数的散植迭代,如果不通过,则一次迭代都不进行。coordinate_eval_order为坐标串演算顺序,代表下述XYZ三个轴向上的值的演算顺序。一般来说,三个轴向上的演算顺序并不影响最终的结果,但是如果三个轴向上使用Molang表达式进行运算时使用了变量来保存信息, 则演算顺序将造成结果差异。xyz三个字段可以使用一个值、一个Molang表达式或者一个对象来指定一个分布函数。上述示例中X轴向上便使用了高斯分布来计算散植的目的坐标。

# 自定义聚合特征

聚合特征的模式标识符为minecraft:aggregate_feature,惯用格式版本为1.13.0。聚合特征旨在将一个或多个特征集中生成在一个位置,即将它们聚合Aggregate)在一起。

{
  "format_version": "1.13.0",
  "minecraft:aggregate_feature": {
    "description": {
      "identifier": "minecraft:bamboo_then_podzol_feature"
    },
    "early_out": "first_failure",
    "features": [
      "minecraft:bamboo_feature",
      "minecraft:optional_podzol_feature"
    ]
  }
}

features数组即我们想要聚合的特征列表。early_out字段可以用于指定该次聚合是否会因为什么原因而提前退出。比如这里first_failure便代表在聚合过程中一旦有一个特征放置失败,便终止整次聚合。

# 自定义序列特征

序列特征的模式标识符为minecraft:sequence_feature,惯用格式版本为1.13.0。序列特征和聚合特征非常类似,都是用于放置多个其他的特征的特征,但是,序列特征的放置起始点并不是固定在某个位置的,而是在放置列表中的下一个特征的起始点会使用上一个特征的结束点。

{
  "format_version": "1.13.0",
  "minecraft:sequence_feature": {
    "description": {
      "identifier": "minecraft:jungle_tree_with_cocoa_feature"
    },
    "features": [
      "minecraft:jungle_tree_feature",
      "minecraft:optional_jungle_tree_cocoa_feature"
    ]
  }
}

features数组便是我们想要放置的特征列表。

# 自定义搜索特征

搜索特征的模式标识符是minecraft:search_feature,惯用的格式版本为1.13.0。搜索特征旨在在一个轴对其边界框中按特定的顺序进行搜索,一旦搜索到适合放置的位置即可放置一个特征。默认而言,搜索特征在成功放置一次后边会结束,但是我们可以更改这个结束之前的成功次数。

{
  "format_version": "1.13.0",
  "minecraft:search_feature": {
    "description": {
      "identifier": "minecraft:beehive_search_feature"
    },
    "places_feature": "minecraft:select_beehive_feature",
    "search_volume": {
      "min": [ 0, 0, 0 ],
      "max": [ 0, 6, 0 ]
    },
    "search_axis": "+y",
    "required_successes": 1
  }
}

required_successes是在停止搜索前所需要的成功次数。特征会在搜索满足成果次数或者搜索尽search_volume中指定的轴对其边界框体积后停止搜索和放置。search_axis代表搜索顺序,其中+y便代表Y轴正方向,我们自然可以推知-y±x±z的意义。对于+y来说,搜索的顺序便是从Y值低的层搜索至Y值高的层,然后在每个Y层中从面相搜索方向的视角来看的左下角搜索至右上角。一旦搜索过程中有满足放置要求的位置,即可放置特征并使搜索次数加1。

# 自定义加权随机特征

加权随机特征的模式标识符为minecraft:weighted_random_feature,惯用的格式版本为1.13.0。加权随机特征旨在在众多特征中通过权重随机选出一个特征进行放置。

{
  "format_version": "1.13.0",
  "minecraft:weighted_random_feature": {
    "description": {
      "identifier": "minecraft:random_meadow_flower_feature"
    },
    "features": [
      ["minecraft:yellow_flower_feature", 5],
      ["minecraft:cornflower_feature", 5],
      ["minecraft:tall_grass_feature", 1]
    ]
  }
}

一个加权随机特征只能放置一次,在上述示例中,在一次放置中,minecraft:yellow_flower_feature具有$\frac{5}{11}$的概率被选中,minecraft:cornflower_feature也具有$\frac{5}{11}$的概率被选中,minecraft:tall_grass_feature具有$\frac{1}{11}$的概率被选中。

# 自定义树特征

树特征是最复杂的特征之一,它的模式标识符为minecraft:tree_feature,惯用格式版本为1.13.0。通过树特征,我们可以制作出一些树状的方块结构。我们来看一个原版的桦树特征:

{
  "format_version": "1.13.0",
  "minecraft:tree_feature": {
    "description": {
      "identifier": "minecraft:birch_tree_feature"
    },
    "trunk": {
      "trunk_height": {
        "range_min": 5,
        "range_max": 8
      },
      "trunk_block": {
        "name": "minecraft:log",
        "states": {
          "old_log_type": "birch"
        }
      }
    },
    "canopy": {
      "canopy_offset": {
        "min": -3,
        "max": 0
      },
      "variation_chance": [
        {
          "numerator": 1,
          "denominator": 2
        },
        {
          "numerator": 1,
          "denominator": 2
        },
        {
          "numerator": 1,
          "denominator": 2
        },
        {
          "numerator": 1,
          "denominator": 1
        }
      ],
      "leaf_block": {
        "name": "minecraft:leaves",
        "states": {
          "old_leaf_type": "birch"
        }
      }
    },
    "base_block": [
      "minecraft:dirt",
      {
        "name": "minecraft:dirt",
        "states": {
          "dirt_type": "coarse"
        }
      }
    ],
    "may_grow_on": [
      "minecraft:dirt",
      "minecraft:grass",
      "minecraft:podzol",
      "minecraft:dirt_with_roots",
      "minecraft:moss_block",
      // Block aliases sure would be sweet
      {
        "name": "minecraft:dirt",
        "states": {
          "dirt_type": "coarse"
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 0
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 1
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 2
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 3
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 4
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 5
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 6
        }
      },
      {
        "name": "minecraft:farmland",
        "states": {
          "moisturized_amount": 7
        }
      }
    ],
    "may_replace": [
      "minecraft:air",
      {
        "name": "minecraft:leaves",
        "states": {
          "old_leaf_type": "oak"
        }
      },
      {
        "name": "minecraft:leaves",
        "states": {
          "old_leaf_type": "spruce"
        }
      },
      {
        "name": "minecraft:leaves",
        "states": {
          "old_leaf_type": "birch"
        }
      },
      {
        "name": "minecraft:leaves",
        "states": {
          "old_leaf_type": "jungle"
        }
      },
      {
        "name": "minecraft:leaves2",
        "states": {
          "new_leaf_type": "acacia"
        }
      },
      {
        "name": "minecraft:leaves2",
        "states": {
          "new_leaf_type": "dark_oak"
        }
      }
    ],
    "may_grow_through": [
      "minecraft:dirt",
      "minecraft:grass",
      {
        "name": "minecraft:dirt",
        "states": {
          "dirt_type": "coarse"
        }
      }
    ]
  }
}

对于树特征来说,首先需要指定一个树干Trunk)的形状,这里使用了trunk字段。事实上,我们有多种树干型状可以选择,比如trunkfallen_trunkacacia_trunkmega_trunkfancy_trunk等。我们只能在众多树干形状字段中选择其一。不同的树干型状对象中的子字段也不尽相同,而一般都会有trunk_heighttrunk_block,分别代表树干的高度和树干使用的方块。我们可以看到,trunk_block使用一个方块引用对象,通过前面的学习我们可以知道,任何可以用方块引用对象的地方都可以直接用一个方块标识符字符串。因此这里trunk_block后面其实也可以直接跟一个方块标识符。接下来是和trunk平级的canopy字段,代表树冠Canopy)的属性。canopy_offset代表树冠偏移量,而variation_chance代表树冠变异的可能性。而树冠使用的树叶方块则由leaf_block指定。与树干一样,树冠也可以有其他的类型,比如往往和金合欢树干一起使用的随机传播型树冠random_spread_canopy。之后,base_block用于指定该树特征的树基Base)方块,即树生成后其脚下应有的方块。如果生成之前树脚下不是该方块,则树将会使脚下替换为该方块。may_grow_on用于指定树可以生成在哪些方块上,我们可以看到,虽然桦树的树基方块中没有草方块,但是桦树是可以生成在草方块上的,这可以在我们能够在草方块上种植桦树树苗这一点上体现。may_replace则是树特征生长过程中可以用于替换的方块,这些方块会被随机替换为树的一部分,成为组成树的方块。最后may_grow_through是树可以穿破的方块,换句话说,树可以将这些方块替换为树自己的方块。

树特征非常复杂但是也非常高效。开发者们可以通过阅读原版的各种特征的JSON文件来充分地学习树特征的写法。当然,其他类型的特征也无一不可通过阅读代码的形式学习。这种学习方式既高效又可以保证正确率,无疑是一种非常有效的学习方式。