# 联动生物事件与行为

最后,我们来探索生物的触发器组件。在本节中,我们欲通过触发器Trigger)来做到一个松鼠的“避难”机制。

# 事件和触发器

通过之前的章节,我们已经了解到事件是实体定义文件中一种非常重要的机制。我们可以通过定义不同的事件来触发不同的效果、添加或移除组件组。而事件的触发机制也是多种多样的。不过,除了内置事件的硬编码触发以外,最多的触发手段应该当属触发器组件了。

触发器组件也是一种实体行为包组件。不同于其他类型的组件,触发器组件主要用于触发一个事件。换句话说,触发器组件就是为了事件而生的。触发器的原理与模组SDK中的事件监听类似,都是通过监听一定的系统事件,然后在特定的时机执行指定的内容。只不过,在实体定义文件这种数据驱动文件中,事件响应执行的内容往往是有限的。在这里,触发器只能用于触发另一个定义在实体行为包定义文件中的事件。

实体的触发器组件也有很多类型。我们这里想制作一个松鼠受伤后就会逃跑的机制,于是我们需要用到与受伤有关的触发器。在掠夺者的组件定义中,我们便可以发现类似的触发器。

"minecraft:on_hurt": {
  "event": "minecraft:ranged_mode",
  "target": "self"
},
"minecraft:on_hurt_by_player": {
  "event": "minecraft:ranged_mode",
  "target": "self"
},
"minecraft:on_target_escape": {
  "event": "minecraft:calm",
  "target": "self"
}

这里,minecraft:on_hurt是实体受到伤害时触发,minecraft:on_hurt_by_player是实体受到玩家伤害时触发,而minecraft:on_target_escape是该实体的目标消失后(即不再将某个实体视为目标后)触发。

我们可以使用minecraft:on_hurtminecraft:on_target_escape来设计一个逃跑机制。

# 为松鼠设计逃跑机制

我们知道,要想使松鼠逃跑,我们应该需要使用一种可以用来规避生物的AI意向。通过对原版机制的了解,我们知道苦力怕会规避“猫科”生物。我们利用这一点到苦力怕的行为文件中寻找相关AI意向。不出所料,我们找到了规避特定生物的AI意向minecraft:behavior.avoid_mob_type,这是他在苦力怕文件中的表达:

"minecraft:behavior.avoid_mob_type": {
  "priority": 3,
  "entity_types": [
    {
      "filters": {
        "any_of": [
          { "test" :  "is_family", "subject" : "other", "value" :  "ocelot"},
          { "test" :  "is_family", "subject" : "other", "value" :  "cat"}
        ]
      },
      "max_dist": 6,
      "walk_speed_multiplier": 1,
      "sprint_speed_multiplier": 1.2
    }
  ]
}

我们便可以使用这个AI意向来作为松鼠规避对它造成伤害的生物的意向。为了改造成我们需要的这种功能,我们需要先了解这个AI意向中使用的一种我的世界机制——过滤器Filter)。

上面这段代码中的filters字段便是用于指定过滤器的字段。我们可以看到原版的苦力怕指定了两个过滤器,并且使用any_of逻辑,意为只要两者出现其一满足,便可以触发该AI意向。这里原版使用了is_family过滤器。该过滤器是一个字符串测试,这里原版分别测试了对立实体是否为豹猫或猫,一旦满足便执行规避的AI意向。

我们期望我们的实体规避对其造成伤害的实体。而我们知道我们已经有了minecraft:behavior.hurt_by_targetAI意向,任意对其造成伤害的实体都会被其标记为目标。因此我们只需要has_target过滤器,该过滤器是一个布尔测试。我们只需要测试对立实体是否为我们实体的目标即可。

我们改造后的组件如下:

"minecraft:behavior.avoid_mob_type": {
  "priority": 3,
  "entity_types": [
    {
      "filters": {
        "test": "is_target",
        "subject": "other",
        "value": true
      },
      "max_dist": 6,
      "walk_speed_multiplier": 1,
      "sprint_speed_multiplier": 1.2
    }
  ]
}

这样,只要对立实体是其目标,我们的松鼠便会自动规避至与其距离6米远之外。

当然,这里我们意图通过触发器来做到这一点。虽然由于过滤器的存在,再使用触发器便没有必要了,但是我们依旧设计了一个使用触发器触发的松鼠。设计完毕的实体定义文件如下:

{
  "format_version": "1.12.0",
  "minecraft:entity": {
    "description": {
      "identifier": "tutorial_demo:squirrel",
      "is_experimental": false,
      "is_spawnable": true,
      "is_summonable": true
    },
    "component_groups": {
      "tutorial_demo:color_red": {
        "minecraft:variant": {
          "value": 0
        }
      },
      "tutorial_demo:color_gray": {
        "minecraft:variant": {
          "value": 1
        }
      },
      "tutorial_demo:escaping": {
        "minecraft:behavior.avoid_mob_type": {
          "priority": 3,
          "entity_types": [
            {
              "filters": {
                "test": "is_target",
                "subject": "other",
                "value": true
              },
              "max_dist": 6,
              "walk_speed_multiplier": 1,
              "sprint_speed_multiplier": 1.2
            }
          ]
        }
      }
    },
    "components": {
      // ...
      "minecraft:behavior.hurt_by_target": {
        "priority": 1
      },
      "minecraft:behavior.delayed_attack": {
        "priority": 4,
        "reach_multiplier": 1.5,
        "attack_duration": 0.75,
        "hit_delay_pct": 0.25,
        "track_target": true,
        "sound_event": "attack.strong"
      },
      "minecraft:behavior.random_stroll": {
        "priority": 6,
        "xz_dist": 2,
        "y_dist": 1,
        "speed_multiplier": 0.8
      },
      "minecraft:behavior.random_look_around": {
        "priority": 9
      },
      "minecraft:behavior.look_at_player": {
        "priority": 11
      },
      // ...
      "minecraft:movement": {
        "value": 0.3
      },
      "minecraft:health": {
        "value": 30,
        "max": 30
      },
      "minecraft:on_hurt": {
        "event": "tutorial_demo:escape_started",
        "target": "self"
      },
      "minecraft:on_hurt_by_player": {
        "event": "tutorial_demo:escape_started",
        "target": "self"
      },
      "minecraft:on_target_escape": {
        "event": "tutorial_demo:escape_ended",
        "target": "self"
      }
    },
    "events": {
      "minecraft:entity_spawned": {
        // ...
      },
      "tutorial_demo:escape_started": {
        "add": {
          "component_groups": [
            "tutorial_demo:escaping"
          ]
        }
      },
      "tutorial_demo:escape_ended": {
        "remove": {
          "component_groups": [
            "tutorial_demo:escaping"
          ]
        }
      }
    }
  }
}

这里我们添加了一个tutorial_demo:escaping组件组,用于minecraft:behavior.avoid_mob_typeAI意向的动态增减。然后我们通过tutorial_demo:escape_startedtutorial_demo:escape_ended事件来进行组件组的增减。最后,我们通过minecraft:on_hurtminecraft:on_hurt_by_playerminecraft:on_target_escape来实现两个事件的触发。这样,一个动态增删功能(这里是增删规避生物的AI意向)的实现就完成了。

当然,如果我们的“脑补”能力比较强,我们可以发现这样的设计有个明显的缺点。那就是由于minecraft:behavior.delayed_attack的存在,虽然其优先级为4,低于优先级为3的minecraft:behavior.avoid_mob_type,但是minecraft:behavior.avoid_mob_type只会让实体规避至6米以外。当实体行走至6米以外时,minecraft:behavior.avoid_mob_type便会暂时被停用,此时minecraft:behavior.delayed_attack便会由于高优先级的AI意向被停用从而被激活,实体就会重新接近之前攻击它的目标。而接近到一定距离时,又会因为太近而重新激活minecraft:behavior.avoid_mob_type,同时minecraft:behavior.delayed_attack再次被停用。实体便会处于一会规避,一会接近的循环状态。我们也可以在电脑开发版中通过ImGui来直观地看到这一点。

我们需要对此进行优化。

# 优化数据

我们意图通过改变minecraft:behavior.delayed_attack的存在性来解决这个问题。我们期望两种变体的松鼠的行为不一致,红色松鼠比较“勇”,只要有人打它就反击,而灰色松鼠比较“逊”,有人打它就逃跑。那么,我们如何实现这一机制呢?事实上,minecraft:on_hurt等触发器也是可以使用过滤器来进行条件触发的。我们可以以如下这种方式来写入一个过滤器:

"minecraft:on_hurt": {
  "event": "tutorial_demo:escape_started",
  "filters":{
    "test": "is_variant",
    "value": 1
  },
  "target": "self"
},
"minecraft:on_hurt_by_player": {
  "event": "tutorial_demo:escape_started",
  "filters":{
    "test": "is_variant",
    "value": 1
  },
  "target": "self"
},
"minecraft:on_target_escape": {
  "event": "tutorial_demo:escape_ended",
  "target": "self"
}

其中is_variant过滤器是用于检测变体的值的过滤器。灰色松鼠的变体值为1,因此,我们只有检测到变体值为1时我们才触发添加对应AI意向的事件。而红色松鼠则是直接在生成时也通过组件组的形式添加延迟攻击组件,这样,灰色松鼠就不会附带有这个组件。我们将minecraft:behavior.delayed_attack组件移动到红色松鼠的变体组件组中,如下:

{
  "format_version": "1.12.0",
  "minecraft:entity": {
    "description": {
      "identifier": "tutorial_demo:squirrel",
      "is_experimental": false,
      "is_spawnable": true,
      "is_summonable": true
    },
    "component_groups": {
      "tutorial_demo:color_red": {
        "minecraft:behavior.delayed_attack": {
          "priority": 4,
          "reach_multiplier": 1.5,
          "attack_duration": 0.75,
          "hit_delay_pct": 0.25,
          "track_target": true,
          "sound_event": "attack.strong"
        },
        "minecraft:variant": {
          "value": 0
        }
      },
      "tutorial_demo:color_gray": {
        "minecraft:variant": {
          "value": 1
        }
      },
      "tutorial_demo:escaping": {
        "minecraft:behavior.avoid_mob_type": {
          "priority": 3,
          "entity_types": [
            {
              "filters": {
                "test": "is_target",
                "subject": "other",
                "value": true
              },
              "max_dist": 6,
              "walk_speed_multiplier": 1,
              "sprint_speed_multiplier": 1.2
            }
          ]
        }
      }
    },
    "components": {
      // ...
      "minecraft:behavior.hurt_by_target": {
        "priority": 1
      },
      "minecraft:behavior.random_stroll": {
        "priority": 6,
        "xz_dist": 2,
        "y_dist": 1,
        "speed_multiplier": 0.8
      },
      "minecraft:behavior.random_look_around": {
        "priority": 9
      },
      "minecraft:behavior.look_at_player": {
        "priority": 11
      },
      // ...
      "minecraft:on_hurt": {
        "event": "tutorial_demo:escape_started",
        "filters":{
          "test": "is_variant",
          "value": 1
        },
        "target": "self"
      },
      "minecraft:on_hurt_by_player": {
        "event": "tutorial_demo:escape_started",
        "filters":{
          "test": "is_variant",
          "value": 1
        },
        "target": "self"
      },
      "minecraft:on_target_escape": {
        "event": "tutorial_demo:escape_ended",
        "target": "self"
      }
    },
    "events": {
      "minecraft:entity_spawned": {
        // ...
      },
      "tutorial_demo:escape_started": {
        // ...
      },
      "tutorial_demo:escape_ended": {
        // ...
      }
    }
  }
}

这样,我们便实现了红灰两种松鼠的行为特异化,我们进入游戏来检测我们的内容。我们可以通过电脑开发版的ImGui直观地看到组件组的增删和AI意向的激活与停用。

这样,我们便修复了我们的异常。下面,我们放出我们目前为止行为包定义文件中全部的内容,供各位开发者们参考:

{
  "format_version": "1.12.0",
  "minecraft:entity": {
    "description": {
      "identifier": "tutorial_demo:squirrel",
      "is_experimental": false,
      "is_spawnable": true,
      "is_summonable": true
    },
    "component_groups": {
      "tutorial_demo:color_red": {
        "minecraft:behavior.delayed_attack": {
          "priority": 4,
          "reach_multiplier": 1.5,
          "attack_duration": 0.75,
          "hit_delay_pct": 0.25,
          "track_target": true,
          "sound_event": "attack.strong"
        },
        "minecraft:variant": {
          "value": 0
        }
      },
      "tutorial_demo:color_gray": {
        "minecraft:variant": {
          "value": 1
        }
      },
      "tutorial_demo:escaping": {
        "minecraft:behavior.avoid_mob_type": {
          "priority": 3,
          "entity_types": [
            {
              "filters": {
                "test": "is_target",
                "subject": "other",
                "value": true
              },
              "max_dist": 6,
              "walk_speed_multiplier": 1,
              "sprint_speed_multiplier": 1.2
            }
          ]
        }
      }
    },
    "components": {
      "minecraft:hurt_on_condition": {
        "damage_conditions": [{
          "filters": {
            "test": "in_lava",
            "subject": "self",
            "operator": "==",
            "value": true
          },
          "cause": "lava",
          "damage_per_tick": 4
        }]
      },
      "minecraft:pushable": {
        "is_pushable": true,
        "is_pushable_by_piston": true
      },
      "minecraft:experience_reward": {
        "on_death": "query.last_hit_by_player ? Math.Random(0,1) : 0"
      },
      "minecraft:breathable": {
        "total_supply": 15,
        "suffocate_time": 0
      },
      "minecraft:physics": {},
      "minecraft:navigation.walk": {
        "can_path_over_water": true,
        "avoid_water": true
      },
      "minecraft:movement.skip": {},
      "minecraft:jump.static": {},
      "minecraft:behavior.float": {
        "priority": 0
      },
      "minecraft:behavior.hurt_by_target": {
        "priority": 1
      },
      "minecraft:behavior.random_stroll": {
        "priority": 6,
        "xz_dist": 2,
        "y_dist": 1,
        "speed_multiplier": 0.8
      },
      "minecraft:behavior.random_look_around": {
        "priority": 9
      },
      "minecraft:behavior.look_at_player": {
        "priority": 11
      },
      "minecraft:equipment": {
        "table": "loot_tables/entities/squirrel_equipment.json"
      },
      "minecraft:type_family": {
        "family":["squirrel", "mob"]
      },
      "minecraft:can_climb": {},
      "minecraft:collision_box": {
        "width": 0.7,
        "height": 0.7
      },
      "minecraft:attack": {
        "damage": 1.0
      },
      "minecraft:movement": {
        "value": 0.3
      },
      "minecraft:health": {
        "value": 30,
        "max": 30
      },
      "minecraft:on_hurt": {
        "event": "tutorial_demo:escape_started",
        "filters":{
          "test": "is_variant",
          "value": 1
        },
        "target": "self"
      },
      "minecraft:on_hurt_by_player": {
        "event": "tutorial_demo:escape_started",
        "filters":{
          "test": "is_variant",
          "value": 1
        },
        "target": "self"
      },
      "minecraft:on_target_escape": {
        "event": "tutorial_demo:escape_ended",
        "target": "self"
      }
    },
    "events": {
      "minecraft:entity_spawned": {
        "sequence": [
          {
            "randomize": [
              {
                "weight": 1,
                "add": {
                  "component_groups": [
                    "tutorial_demo:color_red"
                  ]
                }
              },
              {
                "weight": 1,
                "add": {
                  "component_groups": [
                    "tutorial_demo:color_gray"
                  ]
                }
              }
            ]
          }
        ]
      },
      "tutorial_demo:escape_started": {
        "add": {
          "component_groups": [
            "tutorial_demo:escaping"
          ]
        }
      },
      "tutorial_demo:escape_ended": {
        "remove": {
          "component_groups": [
            "tutorial_demo:escaping"
          ]
        }
      }
    }
  }
}