# 行为切换实战 > 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。 本文将修改**原版僵尸**的行为,实现一个可在**行走**和**飞行**之间切换的进化版僵尸,来扩展你对生物行为实现方式的理解。 注意,本文仅仅是借用这个简单的例子来进行说明,学习重点还是在行为的实现思路上。 在本教程中,您将学习以下内容。 - ✅行为包动画控制器实现更复杂的生物行为的**思路**; - ✅飞行生物的实现; - ✅切换组件组的几种方式; 请点击[这里](https://g79.gdl.netease.com/FlyingZombies.zip)下载本章节课程的教学包 ## 成果展示 僵尸在行走一段距离之后会自动切换成飞行模式: ![僵尸飞行演示](./assets/1_1.gif) 而在飞行一段时间之后又会自动切换成行走模式: ![僵尸飞行切换成行走演示](./assets/1_2.gif) 这样既能提高僵尸搜寻怪物的效率,又能提高玩家在生存时的游戏难度 : ![生存更具挑战性](./assets/1_3.gif) ## 基础行为编写实战 根据生物的状态,我们很自然的可以把生物组件组分成两个部分 `walk` 和 `fly` 组,加上上节课的 `adult` 和 `baby` 组,那就自然而然的能够搭出如下的基础结构: ```json { "format_version": "1.16.0", "minecraft:entity": { "description": { "identifier": "minecraft:zombie", // ..省略.. }, "component_groups": { "walk": { // ...省略.. }, "fly": { // ...省略.. }, "baby": { // ...省略.. }, "adult": { // ...省略.. } }, "components": { // 基础组件 // AI 组件 }, "events": { "minecraft:entity_spawned": { "randomize": [ { "weight": 1, "add": { "component_groups": [ "adult", "walk" ] } }, { "weight": 1, "add": { "component_groups": [ "baby", "walk" ] } } ] }, // 行走和飞行切换的自定义事件,先省略... } } } ``` ### 飞行的实现 我们目前没有飞行实体的编写经验,最直接的方式就是去**借鉴原版**的实现方式。 飞行的敌对生物,很容易想到[幻翼(phantom)](https://zh.minecraft.wiki/w/%E5%B9%BB%E7%BF%BC),我们利用上节课的方法,直接去查看原版的实现方式(注意尽可能查看更新版本的行为文件,因为组件涉及更新和修复): ```json { "format_version": "1.16.0", "minecraft:entity": { "description": { "identifier": "minecraft:phantom", // ...省略.. }, "component_groups": { }, "components": { // ...省略无关组件.. "minecraft:physics": { "has_gravity": false }, "minecraft:pushable": { "is_pushable": true, "is_pushable_by_piston": true }, "minecraft:attack": { "damage": 6 }, "minecraft:movement": { "value": 1.8 }, "minecraft:movement.glide": { "start_speed": 0.1, "speed_when_turning": 0.2 }, "minecraft:follow_range": { "value": 64, "max": 64 }, "minecraft:behavior.swoop_attack": { "priority": 2, "damage_reach": 0.2, "speed_multiplier": 1.0, "delay_range": [10.0, 20.0] }, "minecraft:behavior.circle_around_anchor": { "priority": 3, "radius_change": 1.0, "radius_adjustment_chance": 0.004, "height_adjustment_chance": 0.002857, "goal_radius": 1.0, "angle_change": 15.0, "radius_range": [5.0, 15.0], "height_offset_range": [-4.0, 5.0], "height_above_target_range": [20.0, 40.0] } } } } ``` 很容易看出来,幻翼的飞行主要是依靠 `movement.glide` 行走方式和 `circle_around_anchor` 来实现的(以及 `physics` 组件设置没有重力)。比较奇怪的是,幻翼本身并没有导航组件,全是依靠 AI 组件来实现路径选择的。 并且攻击使用的 [`swoop_attack` 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitygoals/minecraftbehavior_swoop_attack)。 > 对于我们不熟悉的组件,除了查看原版生物的使用方法之外,还可以去看看[文档](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/componentlist),就算是英文也不用担心,毕竟现代浏览器的翻译功能非常完善。 我们稍加更改之后,就可以直接复制过来到我们的 `fly` 组件组下了: ```json "fly": { "minecraft:physics": { "has_gravity": false }, "minecraft:follow_range": { "value": 64, "max": 64 }, // 基础的飞行组件,还是需要符合我们上节课说的步骤:移速、方式、导航、其他AI组件的思路 // 这里的基础移速在 adult 和 baby 状态下定义 "minecraft:movement.glide": { "start_speed": 0.4, "speed_when_turning": 0.6 }, // 原版的 phantom 没有 navigation 组件,所有的转向都是由 circle_around_anchor 完成的 // AI 组件 "minecraft:behavior.swoop_attack": { "priority": 3, "speed_multiplier": 2.25, // 这里设置成没有延迟,是因为避免攻击之后又执行 circle_around_anchor 组件跑开 "delay_range": [0.0, 0.0] }, "minecraft:behavior.circle_around_anchor": { "priority": 7, "radius_change": 1.0, // 一直在改变,变向实现了实体在行走的效果 "radius_adjustment_chance": 0.35, "height_adjustment_chance": 0.002857, "goal_radius": 1.0, "angle_change": 15.0, "radius_range": [5.0, 15.0], "height_offset_range": [-1.0, 3.0], "height_above_target_range": [3.0, 5.0] } }, ``` ### 行走实现 有了我们的飞行,根据上节课所学的知识,加上对于攻击方式和行走 AI 组件的区分,很容易写出下列的行为: ```json "walk": { "minecraft:physics": { // 走在地上肯定是需要重力的,这里与飞行的行为对应 "has_gravity": true }, // 基础组件,还是需要符合我们上节课说的步骤:移速、方式、导航、其他AI组件的思路 // 这里的基础移速在 adult 和 baby 状态下定义 "minecraft:movement.basic": {}, "minecraft:navigation.walk": { "is_amphibious": true, "can_pass_doors": true, "can_walk": true, "can_break_doors": true }, // AI 组件 "minecraft:behavior.melee_attack": { "priority": 3, "speed_multiplier": 1 }, "minecraft:behavior.random_stroll": { "priority": 7, "interval": 20, "speed_multiplier": 1, "must_reach": false } }, ``` 这里有一个小细节:对于组件组中会互相冲突的组件,尽量都写入各自的组件组中,比如这里的 `physics` 组件,如果我们使用覆盖的方式的话,可能就会出问题,**下列就是错误示范**: ```json { // 下列是错误示范 // 省略了其他无关部分 "minecraft:entity": { "component_groups": { "walk": { // 会覆盖掉默认的 physics 组件 "minecraft:physics": { "has_gravity": true }, }, "fly": { // 会覆盖掉默认的 physics 组件 "minecraft:physics": { "has_gravity": false }, }, }, "components": { // 如果我们有两个组件组需要对同一个组件或者行为进行定义,不要采用覆盖的方式: "minecraft:physics": {}, } } } ``` ### 行为测试 上面行为组我们就定义了行走和飞行两种状态,我们要测试的话,只需要基于上节课学到的**事件序列**知识,改造一下原版出生事件就可以进行测试了,比如我们默认加入 `walk` 组: ```json "events": { "minecraft:entity_spawned": { "sequence": [ { // 55开的几率生成成年和幼年僵尸 "randomize": [ { "weight": 1, "add": { "component_groups": [ "adult" ] } }, { "weight": 1, "add": { "component_groups": [ "baby" ] } } ] }, { // 默认加入的行走行为组件组 "add": { "component_groups": ["walk"] } } ] } } ``` 进入游戏,生物拥有正常的行走行为: ![行为测试-行走](./assets/image-20231112134925911.png) 我们要测试飞行也是一样的,只需要把默认加入的组件替换成 `fly` 就行了: ```json "events": { "minecraft:entity_spawned": { "sequence": [ { // 55开的几率生成成年和幼年僵尸 // ...省略... }, { // 默认加入的行走行为组件组 "add": { "component_groups": ["fly"] } } ] } } ``` 进入游戏,观察实体,发现能够正常的进行飞行: ![行为测试-飞行](./assets/image-20231112135359365.png) ## 行为切换实战 这是我们这篇文章主要想讨论的东西,也是实现更复杂生物行为的基础。 下面我们将对如何进行行为切换进行一些讨论。 ### 行为切换的基础:事件 在讨论之前,我们需要先把基础打好,也就是通过事件来添加或者删除对应的组件组: ```json "event":{ // ..省略自带的实体生成事件.. // 自定义事件 "tutorial:convert_to_fly": { "add": { "component_groups": ["fly"] }, "remove": { "component_groups": ["walk"] } }, "tutorial:convert_to_walk": { "add": { "component_groups": ["walk"] }, "remove": { "component_groups": ["fly"] } } } ``` 那么如何**基于我们的需求**来触发这些事件呢?下面是一些思路的讨论。 ### 最容易想到的办法:Timer [`timer` 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_timer)是比较常用的用来触发事件的组件,它的作用就是在一段时间之后触发事件。一个例子: ```json "minecraft:timer":{ "looping": true, "randomInterval":true, "time": [0.0, 0.0], "time_down_event": { "event":"minecraft:times_up" } } ``` 在原版的很多地方也很看到 `timer` 的身影,比如原版的尸壳(husk)会在 30 秒后会变成僵尸: ```json "minecraft:timer": { "looping": false, "time": 30, "time_down_event": { "event": "minecraft:convert_to_zombie" } } ``` 基于此,我们就可以让我们自定义的僵尸,每隔一段时间就切换一次行走方式: ```json { // 省略了其他无关部分 "minecraft:entity": { "component_groups": { "walk": { // 10s 后切换飞行 "minecraft:timer":{ "looping": false, "randomInterval":false, "time": [10.0, 10.0], "time_down_event": { "event":"tutorial:convert_to_fly" } } }, "fly": { // 10s 后切换行走 "minecraft:timer":{ "looping": false, "randomInterval":false, "time": [0.0, 0.0], "time_down_event": { "event":"tutorial:convert_to_walk" } } }, } } } ``` Timer 虽然能够在 `[a,b]` 范围内触发我们的事件,但是总体来说生物行为还是比较呆板和固定的。 ### 更好的办法:触发器和传感器 在基岩版中,有一些特殊情况发生时,会有对应的[触发器](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/triggerlist)可以触发事件的响应: | 触发器 | 说明 | | :----------------------------------------------------------- | :----------------------------------------------------------- | | [minecraft:on_death](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_death) | 当实体死亡时触发。只能用在 `ender_dragon` 末影龙上。 | | [minecraft:on_friendly_anger](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_friendly_anger) | 当同类型的实体进入 `angry` 组件时触发。 | | [minecraft:on_hurt_by_player](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_hurt_by_player) | 被玩家攻击时触发。 | | [minecraft:on_hurt](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_hurt) | 被实体攻击时触发。 | | [minecraft:on_ignite](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_ignite) | 当实体被点燃时。 | | [minecraft:on_start_landing](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_start_landing) | 但实体开始着陆。只能用在 `ender_dragon` 末影龙上。 | | [minecraft:on_start_takeoff](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_start_takeoff) | 实体开始起飞。只能用在 `ender_dragon` 末影龙上。 | | [minecraft:on_target_acquired](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_target_acquired) | 当实体获得目标时触发。 | | [minecraft:on_target_escape](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_target_escape) | 当实体没有目标时触发。 | | [minecraft:on_wake_with_owner](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitytriggers/minecrafttrigger_on_wake_with_owner) | 当实体作为宠物时,主人睡觉起床时触发。需要通过 `tame` 组件或者命令把实体标记为宠物。 | 比如,我们可以让我们的僵尸在发现目标后切换成飞行的状态,而没有目标时恢复行走状态: ```json { // 省略了其他无关部分 "minecraft:entity": { "components": { "minecraft:on_target_acquired": { "event": "tutorial:convert_to_fly", "target": "self" }, "minecraft:on_target_escape": { "event": "tutorial:convert_to_walk", "target": "self" }, } } } ``` 效果演示: ![发现目标切换飞行](./assets/1_4.gif) 以上的触发器都是微软硬编码好的。还有一种类似的触发方式是基于组件的传感器: | 传感器 | | | :----------------------------------------------------------- | :----------------------------------------------------------- | | [minecraft:block_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_entity_sensor) | 当列表里面的方块在实体周围被破坏时触发。 | | [mincraft:damage_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_damage_sensor) | 当实体被指定实体或者指定伤害类型伤害时触发。 | | [minecraft:entity_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_entity_sensor) | 当定义范围内的其他实体满足配置条件时触发。 | | [minecraft:environment_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_environment_sensor) | 根据环境条件触发。比如夜晚、白天等。自由度较高。 | | [minecraft:rail_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_rail_sensor) | 实体在经过已激活或已停用的轨道时触发。 | | [minecraft:target_nearby_sensor](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_target_nearby_sensor) | 定义实体的范围,在该范围内,它可以查看或感知其他实体以定位它们。苦力怕检测爆炸就是使用的这一传感器。 | 比如,我们让自定义僵尸在距离目标超过 6 格距离时切换成飞行状态,否则就行走: ```json { // 省略了其他无关部分 "minecraft:entity": { "components": { "minecraft:target_nearby_sensor": { "inside_range": 5.5, "outside_range": 6.0, "must_see": true, "on_inside_range": { "event": "tutorial:convert_to_walk", "target": "self" }, "on_outside_range": { "event": "tutorial:convert_to_fly", "target": "self" }, "on_vision_lost_inside_range": { "event": "tutorial:convert_to_walk", "target": "self" } }, } } } ``` 又或者使用 `environment_sensor` 加上[过滤器](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/filterlist)来制作一个晚上飞行,白天行走的僵尸: ```json { // 省略了其他无关部分 "minecraft:entity": { "component_groups": { "walk": { "minecraft:environment_sensor": { "triggers": [ { "filters": { "test": "is_daytime", "value": false }, "event": "tutorial:convert_to_fly" } ] }, // 其他组件 }, "fly": { "minecraft:environment_sensor": { "triggers": [ { "filters": { "test": "is_daytime", "value": true }, "event": "tutorial:convert_to_walk" } ] }, // 其他组件 }, }, } } ``` 只需要我们熟悉这些传感器和过滤器,我们就可以制作行为更加丰富的实体。其他的这些传感器大家可以自行去了解和实验。 ### 进阶办法:动画控制器 + Molang 上面提到的都是基于组件的,那下面我们就来讨论一个基于动画控制器的方法。 我们在上一节课中提到,在行为包中也可以给实体使用动画控制器,只不过与资源包中的控制器功能不同(行为包是允许使用命令的)。对于动画控制器不熟悉的同学可以去官网查看相关教程。 这里举几个例子来帮助大家快速理解。 #### 例1:随机间隔计时器 新增一个空的、只用于计时的**动画文件** `tutorial_zombie.animation.json` 文件到**行为包**下的 `animations`(因为动画控制器只能读取到同目录下的动画文件): ```json { "format_version": "1.8.0", "animations": { "animation.tutorial_zombie.random_interval": { "animation_length": 100 } } } ``` 同样,在**行为包**下的 `animation_controllers` 目录下新增 `controller.animation.tutorial_zombie.json`: ```json { "format_version": "1.10.0", "animation_controllers": { // 随机间隔控制器示例 "controller.animation.zombie.random_interval": { "initial_state": "default", "states": { "default": { "transitions": [ { "walking": "query.is_on_ground" }, { "flying": "!query.is_on_ground" } ] }, "walking": { "on_entry": [ "variable.random_interval = math.random(2, 7);", "/say walking random interval started" ], "animations": [ "tutorial:random_interval" ], "transitions": [ { "flying": "query.anim_time >= variable.random_interval" } ], "on_exit": [ // 触发自定事件 "@s tutorial:convert_to_fly", "/say walking random interval finished" ] }, "flying": { "on_entry": [ "variable.random_interval = math.random(2, 7);", "/say flying random interval started" ], "animations": [ "tutorial:random_interval" ], "transitions": [ { "walking": "query.anim_time >= variable.random_interval" } ], "on_exit": [ // 触发自定事件 "@s tutorial:convert_to_walk", "/say flying random interval finished" ] } } } } } ``` > 这里需要知道的是,动画控制器文件中的 `query.` 可以缩写为 `q.`,自定义的变量 `variable.` 也可以缩写为 `v.` 此时的 `zombie.json` 文件如下: ```json { "format_version": "1.16.0", "minecraft:entity": { "description": { "identifier": "minecraft:zombie", "is_experimental": false, "is_spawnable": true, "is_summonable": true, "animations": { // 新增的用于计时的动画 "tutorial:random_interval": "animation.tutorial_zombie.random_interval", // 随机间隔计时器 "random_interval": "controller.animation.zombie.random_interval" }, "scripts": { "animate": [ "random_interval" ] } }, // 省略其他无关内容.... ``` 此时,我们就可以不加入任何组件的情况下,实现行走和飞行的互相转换: ![随机间隔计时器演示](./assets/1_5.gif) #### 例2:基于行走距离切换 我们上面所有的实现方式都是被动触发式的,比如时间到了、环境变了。如果我们想要实现行走一段距离之后切换飞行的话,组件的方式就已经不实用了。 原版有一个 Molang 变量是 `query.walk_distance`,它会返回实体行走的总长度。我们基于此实现下列控制器: ```json // 基于行走距离来切换飞行 "controller.animation.zombie.base_on_walk_distance": { "initial_state": "default", "states": { "default": { "transitions": [ { "walking": "query.is_on_ground" }, { "flying": "!query.is_on_ground" } ] }, "walking": { "on_entry": [ "/say start walking", // query.walk_distance 返回的是实体行走的总距离 "variable.distance2fly = query.walk_distance + math.random(1, 3);" ], "on_exit": [ // 触发自定事件 "@s tutorial:convert_to_fly", "/effect @s levitation 2 1 true" ], "transitions": [ { "flying": "query.walk_distance >= variable.distance2fly" } ] }, "flying": { "on_entry": [ "/say start flying", // query.time_stamp 返回的是当前世界的世界戳 // 游戏1s对应20帧,这里(* 20)是把秒转换成游戏时间戳 "variable.time2fall = query.time_stamp + math.random(4, 10) * 20;" ], "on_exit": [ // 触发自定事件 "@s tutorial:convert_to_walk", "/effect @s levitation 0", // 为了让实体更平滑的降落 "/effect @s slow_falling 5 0 true" ], "transitions": [ { "walking": "query.time_stamp >= variable.time2fall" } ] } } } ``` 此时的 `zombie.json` 如下所示: ```json "format_version": "1.16.0", "minecraft:entity": { "description": { "identifier": "minecraft:zombie", "is_experimental": false, "is_spawnable": true, "is_summonable": true, "animations": { // 基于行走距离来切换 "base_on_walk_distance": "controller.animation.zombie.base_on_walk_distance" }, "scripts": { "animate": [ "base_on_walk_distance" ] } // 省略其他无关内容.... ``` 重新进入游戏,实体就会在行走一段随机距离之后切换飞行。又会在飞行一段时间之后切换回行走。 上面两个例子,都是为了帮助我们理解这样的生物行为切换方式,主要目的是**扩展思路**。其实这样的方式,还能实现很多其他的功能,比如控制实体飞行等,这里就不做具体演示了。 ## 小结 我们想要制作复杂的生物行为,首先我们就需要对[生物组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/componentlist)、[过滤器](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/filterlist)、[AI 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/aigoallist)、[触发器](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/triggerlist)、[Molang 变量](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/molangreference/examples/molangconcepts/queryfunctions)具有基础的了解外,还需要对他们的运用有所了解(可以查看原版实体)。 另外,动画控制器 + Molang 的方式也能够让我们的生物支持更丰富的行为。 虽然原版已经有足够的组件供我们使用,但仍然有些功能无法实现,我们将会在下一节中继续说明自定义行为的实现。 ## 课后作业 本次课后作业,内容如下: - 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具备基础的飞行和行走的转换能力,并尝试以下的方法: - 基于组件的方式; - 基于触发器的方式; - 基于传感器的方式; - 基于动画控制器的方式;