This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,821 @@
# 实体基础
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你从零开始修改原版僵尸的行为,从而帮助你认识和了解生物行为的构成。
本系列不特意涉及实体资源的相关教程,如果对这一块不熟悉的同学,请自行前往前往官网查看相关教程。
在本教程中,您将学习以下内容。
- ✅如何查看原版行为文件;
- ✅认识和了解行为文件的基本构成;
- ✅尝试修改原版僵尸的行为并实现僵尸的基础行为:
- 手动还原最基础的僵尸行为;
- 出生随机大小的僵尸;
请点击[这里](https://g79.gdl.netease.com/Entity.zip)下载本章节课程的教学包
## 原版僵尸的行为
### 如何查看原版文件
在本地游戏测试客户端的目录中MC Studio 安装目录下的 `\game\MinecraftPE_Netease` 可以找到),我们可以直接查看和学习生物的行为 JSON 是如何编写的:
![](./assets/image-20231031095005439.png)
使用打开目录之后跟我们正常编写的附加包并无差别,略微的区别就是像是 `attachables``biomes``feature_rules``features` 这样的定义是保存在 `definitions` 目录下的(我们自己写是需要写在 `behavior_packs` 对应目录下):
![](./assets/image-20231031095254881.png)
如此,我们就可以在对应行为包下找到相关的原版行为 JSON 定义了。使用 `vanilla` 开头的包就是我们的原版资源了,后面跟随的是版本信息,通常来说,我们应该**尽可能查看高版本的行为**,因为更高的版本意味着一些改进和修复。
比如,我们查看 `zombie` 的最新 JSON 文件(在 `vanilla_1.17.20\entities` 目录)和第一版 JSON 文件(在 `vanilla\entities` 目录下)就可以发现,最新版本的 JSON 除了在行为文件使用的版本`format_version` 上有差异外,还多了一个 `minecraft:shareables` [组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_shareables),这个组件允许实体分享和拾取一些道具,这直接导致了僵尸能够拾取地上物品的行为
![僵尸拾取物品行为演示](./assets/0_1.gif)
### 格式概述
实体行为都保存在行为包的 `entities` 文件夹中。行为文件扩展名都为 `.json`,这跟游戏加载项中的许多文件都使用相同的扩展名,如果你想避免混淆,可以为行为文件使用再次扩展的扩展名 `.behavior.json`。对于其他再次扩展的扩展名,请参阅[此表](https://learn.microsoft.com/zh-cn/minecraft/creator/documents/introductiontoaddentity)。
这些文件是用 JSON 编写的,基本结构如下所示:
```json
{
"format_version": "1.17.20",
"minecraft:entity": {
"description": {
"identifier":"minecraft:zombie",
"is_spawnable":true,
"is_summonable":true,
"is_experimental": false
},
"component_groups": {},
"components": {},
"events": {}
}
}
```
`description` 标签中,一些基本属性定义了游戏如何注册实体:
| 参数名称 | 类型 | 描述 |
| :------------------- | :----- | :----------------------------------------------------------- |
| `identifier` | 字符串 | 实体的标识符。 如果这是加载项中的自定义实体,则应使用示例中所示的自定义唯一命名空间。 |
| `runtime_identifier` | 字符串 | 游戏内部使用的标识符。 这可用于从尚未作为组件提供的原版实体继承自定义机制。 每个实体只能指定一个运行时标识符。**仅在确实需要时才使用**。 如果一个原版实体的机制变成了组件,通过运行时标识符依赖这些机制可能会失去功能。 |
| `is_spawnable` | 布尔值 | 如果为 `true`,则该实体的刷怪蛋将添加到创意物品栏中。 |
| `is_summonable` | 布尔值 | 如果为 `true`,则可以使用 `/summon` 命令来召唤实体。 |
| `is_experimental` | 布尔值 | 如果为 `true`,则实体可以使用实验功能。 该实体只会在实验世界中工作。 |
| `animations` | 对象 | 行为动画或动画控制器的列表。 这些可用于在实体上运行命令或事件。 |
| `scripts` | 对象 | 脚本的工作方式类似于它们在客户端实体文件中的工作方式,并且可用于播放行为动画。 |
> ❗️**注意:**
>
> 行为目录下**也是可以使用动画控制器**的,其结构跟资源文件下的动画控制器具有相同的通用格式,不同的是,行为包属于服务端的文件,它允许您触发命令,而不是动画。我们会在后续的章节中介绍到这一部分,这有助于实现一些复杂的生物行为。
### 组件和组件组
组件是可以添加到实体的属性或机制。 添加组件有两种方式:直接添加到 **component** 标签或使用**组件组**。
- 添加到基本 **component 标签**的组件始终处于活动状态,除非通过事件中的组件组移除。
- **组件组**包含一个或多个组件,每个组件默认情况下处于非活动状态,但可以通过事件启用或禁用。 例如,这可用于创建实体的变体,例如 baby婴儿
比如我们以原版僵尸的行为 JSON 为例(精简之后):
```json
{
"format_version": "1.17.20",
"minecraft:entity": {
"description": {
"identifier": "minecraft:zombie",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"component_groups": {
"minecraft:zombie_baby": {
"minecraft:is_baby": {},
"minecraft:scale": {
"value": 0.5
},
"minecraft:movement": {
"value": 0.35
}
},
"minecraft:zombie_adult": {
"minecraft:movement": {
"value": 0.23
}
}
},
"components": {
"minecraft:physics": {}
// Zombie Components
// Zombie Behaviors
}
}
}
```
`compoents` 下的组件始终处于活动状态,而 `minecraft:zombie_baby` **组件组**中的组件只有在添加该组之后才会处于活动状态,这需要 `events` 来配合使用。
#### 如何学习组件
完整的可用组件列表可以再[此处](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/componentlist)找到。官方文档不仅有详细的参数解释,还基本都配套有一些例子和原版的应用:
![官方文档示例](./assets/image-20231031151519365.png)
另一个常用的[基岩版文档网站](https://bedrock.dev/zh)也建议存储起来。这两个网站通常需要配合起来查阅。
![基岩版文档](./assets/image-20231031151749226.png)
我们也可以通过查看《我的世界》默认实体的行为文件,来了解组件以及应该在实践中如何使用(参考上方的 #如何查看原版文件)。
#### 核心组件
| 组件名称 | 选项 | 描述 |
| :----------------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| `minecraft:physics` | `has_collision` `has_gravity` | 99% 的自定义实体都需要此组件。 它允许实体留在地面上并以您期望的方式对交互和冲击做出反应。 |
| `minecraft:scale` | `value` | 设置实体的比例。 |
| `minecraft:collision_box` | `width` `height` | 设置实体的碰撞盒。 只能更改 `width``height`。 碰撞盒始终具有一个与世界轴对齐的方形基础。 |
| `minecraft:type_family` | `family` | 设置实体所在的类型系列的列表。 类型系列可以由其他实体进行测试。 例如,测试他们对哪些生物怀有敌意。 |
| `minecraft:movement` | `value` | 设置实体的移动速度。 《我的世界》中大多数动物的常规速度为 0.25。 |
| `minecraft:movement.basic` | [查看文档](https://learn.microsoft.com/zh-cn/creator/Reference/Content/EntityReference/Examples/EntityComponents/minecraftComponent_movement.basic.md) | 允许实体在地面上移动。 |
| `minecraft:navigation.walk` | [查看文档](https://learn.microsoft.com/zh-cn/creator/Reference/Content/EntityReference/Examples/EntityComponents/minecraftComponent_navigation.walk.md) | 允许实体在世界中游走。 还有其他类型的导航,比如悬停。 |
| `minecraft:is_baby` `minecraft:is_ignited` `minecraft:is_saddled` `minecraft:is_sheared` `minecraft:is_tamed` `minecraft:is_illager_captain` | :--- | 这些组件本身不做任何事情,但可以在动画、动画控制器或渲染控制器中查询它们,允许您通过实体行为控制动画和其他视觉效果。 |
| `minecraft:variant` `minecraft:mark_variant` `minecraft:skin_id` | `value` | 这些组件的工作方式与上面的类似,但它们不仅可以存储开/关状态,还可以存储整数值。 |
#### 优先级 - priority
选项 `priority` 可用于所有行为组件AI 目标)。 `0` 所有行为组件的最高优先级和默认优先级为 0。 数字越大,优先级越低。 如果实体正忙于进行一个低优先级的行为,当出现了一个高优先级的行为时,该实体将立即切换到更高优先级的行为。
在以下示例中,`hurt_by_target` 组件具有更高的优先级。 如果实体在漫步时受到攻击,它会立即瞄准攻击者。
```json
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.random_stroll": {
"priority": 4
}
```
一种比较推荐的书写方式就是参考原版的僵尸,把**组件和行为进行区分**(并不特别需要 `//` 注释),这样可以让我们更加容易找到 AI 组件并控制他们的优先级,避免发生冲突:
```json
"components": {
"minecraft:is_hidden_when_invisible": {},
"minecraft:nameable": {},
"minecraft:physics": {}
// Zombie Components
// Zombie Behaviors
}
```
#### 专属组件-组件的隐性规则
有时候我们的组件无法使用,除了排查生物使用的版本信息(`format_version`)、是否存在优先级冲突、是否没有添加依赖的基础组件(请参考 #如何调试生物行为 查看)之外,还应该考虑这个组件是否是为某个生物准备的专属组件。
比如原版的末影龙就会有许多硬编码的组件供他专用:
![末影龙的专属组件](./assets/image-20231103120559952.png)
我们查看史莱姆的相关组件也可以看到类似的说明:
![史莱姆组件说明](./assets/image-20231103121138327.png)
如果我们仍然想要使用相关组件的话,那么就需要把 `runtime_identifier` 改成支持对应组件的实体才可以了。
### 事件
事件用于在实体中添加和移除组件组。 在此示例中,移除了一个旧的组件组,添加了两个新的组件组:
```json
"events": {
"compass:example_event": {
"remove": {
"component_groups": ["compass:group_a"]
},
"add": {
"component_groups": ["compass:group_b","compass:group_c"]
}
}
}
```
事件可以由许多组件触发,例如 `minecraft:interact``minecraft:environment_sensor`、通过行为动画或 summon 命令。 以下命令将在运行上述事件时生成实体。
```txt
/summon compass:example_entity ~ ~ ~ compass:example_event
```
#### 内置事件
《我的世界》中内置了一些事件,当实体在特定条件下生成时运行。
| 事件名称 | 描述 |
| :----------------------------- | :----------------------------------------------------------- |
| `minecraft:entity_born` | 该事件在实体通过繁殖产生时运行。 |
| `minecraft:entity_spawned` | 该事件在实体生成时运行。 请注意,它在手动执行 `/summon` 时不会运行。 |
| `minecraft:entity_transformed` | 当另一个实体转换为该实体时,该事件将运行。 |
| `minecraft:on_prime` | 该事件在实体被激活并即将爆炸时运行。 |
#### 随机化器
如果要随机指定将哪个组件组添加到实体,可以使用 randomize 函数。 您指定一个对象数组,每个对象都可以添加和移除组件组。 游戏将随机选择并运行这些对象之一。
或者,您可以向选项添加权重选项以更改每个选项的概率。 所有权重加起来为 100%。 在以下示例中,实体将以 20% (1:4) 的概率生成为 baby而在其他情况下不会添加任何组件组
```json
"events": {
"minecraft:entity_spawned": {
"randomize": [
{
"weight": 40
},
{
"weight": 10,
"add": {
"component_groups": ["baby"]
}
}
]
}
}
```
#### 序列
有时,您需要在同一事件中接连运行多个事件实例。 例如,您可能希望随机化实体的两个方面,如颜色和标记图案。 在这种情况下,您可以使用序列。 序列的结构类似于随机化器,但列表中的每个项都是按顺序执行的。 序列与过滤器结合使用也非常有用,将在下一节中解释。
序列和随机化器可以无限嵌套。
在此示例中,将在生成实体时添加组 `initial`。 之后baby 组件将像上一节一样随机化。
```json
"events": {
"minecraft:entity_spawned":{
"sequence": [
{
"add": {
"component_groups": ["initial"]
}
},
{
"randomize": [
{
"weight": 40
},
{
"weight": 10,
"add": {
"component_groups": ["baby"]
}
}
]
}
]
}
}
```
#### 过滤器
过滤器通过测试当前实体(交互实体或世界)的特定属性来调节事件或事件的一部分。 过滤器可以在任何事件中使用,但也可以直接在某些组件中使用。
一个事件最多包含五个参数。 参数 `test``value` 是必需的,其他是可选的:
| 测试类型 | 要测试的属性 |
| :--------- | :----------------------------------------------------------- |
| `value` | 要测试的值。 测试字符串时可以是字符串,测试数值时可以是数值,也可以是布尔值。 |
| `subject` | 运行测试的实体。 默认情况下为 `self`,但它也可以针对交互中涉及的其他实体。 |
| `operator` | 比较值的方式。 默认为 `equals`,但也可以比较大小(数值)或是否不相等。 |
| `domain` | 仅由少数测试使用,提供额外的上下文,例如,`has_equipment` 测试可测试物品栏的栏位。 |
事件中过滤器的最简示例可能如下所示:只有当实体具有 `event_allowed` 标签时才能添加组件组。
```json
"events": {
"compass:example_event": {
"filters": {
"test": "has_tag",
"value":"event_allowed"
},
"add": {
"component_groups": ["baby"]
}
}
}
```
如果您想使用多个过滤器,您可以使用列表 `all_of``any_of``none_of` 对它们进行分组,分别对应所有过滤器、一个过滤器或无过滤器,仅在满足相应条件时成功。 这些列表可以无限嵌套。
在以下示例中,我们将向过滤器添加第二个条件。 仅当实体具有上一个示例中的标签**并且**距离最近的玩家少于 10 个方块时,该事件才会运行。
```json
"events": {
"compass:example_event": {
"filters": {
"all_of":[
{"test": "has_tag", "value": "event_allowed"},
{"test": "distance_to_nearest_player", "operator": "<", "value": 10}
]
},
"add": {
"component_groups": ["baby"]
}
}
}
```
### 原版僵尸行为组件分析
有了以上的基本了解之后,我们阅读原版僵尸的行为就很容易一些了,大家可以自行阅读一下,会发现就只是**简单的组件堆叠**。
这里只提一个比较特殊一些的地方,那就是僵尸变成溺尸的组件 `minecraft:transformation` 使用的特殊用法:
```json
"minecraft:transformation": {
"into": "minecraft:drowned<minecraft:as_adult>",
"transformation_sound": "convert_to_drowned",
"drop_equipment": true,
"delay": {
"value": 15
}
}
```
这里的 `into` 的参数除了带有溺尸的标识符外,还用 `<>` 标识了转换之后所要触发的事件。
另一个特殊的地方是僵尸转换的时候会判断自己是否是 `baby`,会转换成对应的溺尸,这是通过事件的序列和过滤器实现的:
```json
"minecraft:convert_to_drowned": {
"sequence": [
{
"filters": {
"test": "has_component",
"operator": "!=",
"value": "minecraft:is_baby"
},
"add": {"component_groups": ["minecraft:convert_to_drowned"]},
"remove": {"component_groups": ["minecraft:start_drowned_transformation"]}
},
{
"filters": {
"test": "has_component",
"value": "minecraft:is_baby"
},
"add": {"component_groups": ["minecraft:convert_to_baby_drowned"]},
"remove": {"component_groups": ["minecraft:start_drowned_transformation"]}
}
]
}
```
## 基础僵尸行为还原
### 僵尸行为分析
原版的僵尸有非常多的特性,行为 JSON 有多达 590 多行,我们现在来尝试,自己一步一步还原出僵尸的基础行为。
我们来分析一下僵尸有哪些状态,分别有什么样的行为:
- **小僵尸**:体型小,跑得快。
- **成年僵尸**:正常体型,跑得不快。
- **通用行为**:移动、攻击玩家、在白天燃烧。
在不考虑转换成溺尸的情况下,这就是僵尸的基础行为了。根据我们的分析,就可以搭建出基础的行为 JSON 框架:
```json
{
"format_version": "1.17.20",
"minecraft:entity": {
"description": {
"identifier": "minecraft:zombie",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"component_groups": {
"minecraft:zombie_baby": {
// baby 僵尸的组件
},
"minecraft:zombie_adult": {
// 成年僵尸的组件
}
},
"components": {
// 僵尸的通用组件:行为、在白天燃烧
// 僵尸的通用行为:攻击玩家
},
"events": {
}
}
}
```
首先,根据上面所学的知识,我们让我们修改过的僵尸,能够按相同的概率来加载 `minecraft:zombie_baby` 组和 `minecraft:zombie_adult` 组,这有助于之后的开发和测试:
```json
{
"format_version": "1.17.20",
"minecraft:entity": {
"description": {
"identifier": "minecraft:zombie",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"component_groups": {
"minecraft:zombie_baby": {
"minecraft:scale": {
"value": 0.5
}
},
"minecraft:zombie_adult": {
}
},
"components": {
// 僵尸的通用组件:行为、在白天燃烧
// 僵尸的通用行为:攻击玩家
},
"events": {
"minecraft:entity_spawned": {
"randomize": [
{
"weight": 1,
"add": {
"component_groups": [
"minecraft:zombie_adult"
]
}
},
{
"weight": 1,
"add": {
"component_groups": [
"minecraft:zombie_baby"
]
}
}
]
}
}
}
}
```
`minecrat:scale` 组件是用来控制实体模型大小的组件,处于 `baby` 组的僵尸只有成年僵尸一半的体型。
这样,我们就修改了原版的僵尸行为,实现了按 1:1 比例生成成年僵尸和幼年僵尸的行为:
![随机大小演示](./assets/0_2.gif)
有了这个基础,我们再来一步一步还原僵尸的基础行为。
### 实体移动
在我的世界中,实体有能力通过行走、游泳或者飞行在世界中移动。想要自定义的实体获得这些能力,请记住按照以下步骤添加相关的组件:
- **设置实体移动速度**的组件;
- 用于**设置实体移动方式**(行走、飞行等)的组件;
- 用于**设置实体导航功能**的组件, 以便让他们可以生成路径;
- 设置**实体移动的位置/时间**AI 目标)的组件;
#### 移动速度
实体首先需要的是速度组件,这设置了实体在世界中的移动速度。
| 组件 | 说明 |
| ------------------------------------------------------------ | -------------------- |
| [minecraft:movement](https://wiki.bedrock.dev/entities/vanilla-usage-components.html#movement) | 设置移动速度(必填) |
| [minecraft:underwater_movement](https://wiki.bedrock.dev/entities/vanilla-usage-components.html#underwater-movement) | 设置水中的移动速度。 |
| [minecraft:flying_speed](https://wiki.bedrock.dev/entities/vanilla-usage-components.html#flying-speed) | 设置空中的飞行速度。 |
我们需要**始终添加**上 `minecraft:movement` 组件,其他两个按需加入。
所有像是海豚一类的能够游泳的实体,都包括了 `minecraft:underwater_movement`,而只有少部分飞行实体(蜜蜂、末影龙)包括了 `minecraft:flying_speed`,不清楚原因。
#### 移动类型
移动类型是实体在世界中移动方式的硬编码行为。
您的实体**只能包含一种移动类型**。根据您的实体选择一种最符合要求的组件就可以。一般来说,`basic``amphibious``fly` 都是非常好用且常用的组件。
| 组件 | 说明 |
| ------------------------------------------------------------ | ---------------------------------------------------- |
| [minecraft:movement.amphibious](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.amphibious) | 这种移动控制允许生物在水中游泳并在陆地上行走。 |
| [minecraft:movement.basic](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.basic) | 此组件是实体的移动基础组件。 |
| [minecraft:movement.fly](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.fly) | 这种移动控制会导致生物飞行。 |
| [minecraft:movement.generic](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.generic) | 这种移动控制允许生物飞行、游泳、攀爬等。 |
| [minecraft:movement.hover](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.hover) | 此移动控制会导致生物悬停。 |
| [minecraft:movement.jump](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.jump) | 移动控制使生物在移动时跳跃,跳跃之间有指定的延迟。 |
| [minecraft:movement.skip](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.skip) | 此移动控制会导致生物在移动时跳跃。 |
| [minecraft:movement.sway](https://bedrock.dev/docs/stable/Entities#minecraft%3Amovement.sway) | 这种移动控制使生物左右摇摆,给人的感觉就是它在游泳。 |
#### 运动修改器
这是有关实体如何在世界中移动的附加信息。这些组件对于普通实体来说基本是不需要的,但您应该了解它们。
| 组件 | 说明 |
| ------------------------------------------------------------ | ------------------------------ |
| [minecraft:water_movement](https://bedrock.dev/docs/stable/Entities#minecraft%3Awater_movement) | 设置实体在水中所经历的摩擦 |
| [minecraft:rail_movement](https://bedrock.dev/docs/stable/Entities#minecraft%3Arail_movement) | 设置实体(仅)可以在轨道上移动 |
| [minecraft:friction_modifier](https://bedrock.dev/docs/stable/Entities#minecraft%3Afriction_modifier) | 设置实体在陆地上所经历的摩擦 |
#### 导航
导航组件有非常多的参数,比如可以设置实体是否可以打开门或避免阳光照射。如何设置这些字段通常比您选择的导航组件更重要!
之所以有这么多导航组件,是因为每个组件都给出了略有不同的硬编码行为。选择其名称/描述与实体将要执行的导航类型最匹配的导航组件。
在任何情况下,您**只能有一个**导航组件。
| 组件 | 说明 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| [minecraft:navigation.climb](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.climb) | 允许此实体生成包含垂直墙的路径,就像原版蜘蛛一样。 |
| [minecraft:navigation.float](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.float) | 允许该实体像普通的 Ghast 一样通过在空中飞行来生成路径。 |
| [minecraft:navigation.generic](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.generic) | 允许此实体通过行走、游泳、飞行和攀爬以及上下跳跃来生成路径。 |
| [minecraft:navigation.fly](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.fly) | 允许该实体像鹦鹉一样在空中生成路径。 |
| [minecraft:navigation.swim](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.swim) | 允许此实体生成包含水的路径。 |
| [minecraft:navigation.walk](https://bedrock.dev/docs/stable/Entities#minecraft%3Anavigation.walk) | 允许该实体像普通生物一样通过四处走动和上下跳跃来生成路径。 |
#### 导航能力
导航组件只能告诉实体如何生成路径,但不会说明在何时何地生成路径。这就是 **AI 组件**的用途。
AI 目标以**优先级**为前缀,并遵循优先级来选择要运行的行为。将首先选择优先级较低的行为来执行。
生成路径的 AI 组件太多,无法在本文档中列出。我们将提供一些示例:
| 组件 | 说明 |
| ------------------------------------------------------------ | ----------------------------------------------------------- |
| [minecraft:behavior.random_stroll](https://bedrock.dev/docs/stable/Entities#minecraft%3Abehavior.random_stroll) | 随机漫步,需要同时拥有 `movement``look_at` 相关的组件。 |
| [minecraft:behavior.follow_owner](https://bedrock.dev/docs/stable/Entities#minecraft%3Abehavior.follow_owner) | 允许该实体跟随主人。 |
| [minecraft:behavior.move_to_water](https://bedrock.dev/docs/stable/Entities#minecraft%3Abehavior.move_to_water) | 允许实体在陆地的情况下返回水体中。 |
| [minecraft:behavior.stroll_towards_village](https://bedrock.dev/docs/stable/Entities#minecraft%3Abehavior.stroll_towards_village) | 允许该实体在搜索范围内移动到村庄的随机位置。 |
完整的 AI 组件可以参考[这个列表](https://bedrock.dev/docs/stable/Entities#AI%20Goals)。
#### 为僵尸添加基础的行走能力
按照我们上述提过的步骤,首先在 `baby``adult` 组件组中添加上**移动速度**的定义,因为僵尸始终会处于这两个组件组中的其中一个:
```json
"component_groups": {
"minecraft:zombie_baby": {
"minecraft:scale": {
"value": 0.5
},
"minecraft:movement": {
"value": 0.35
}
},
"minecraft:zombie_adult": {
"minecraft:movement": {
"value": 0.23
}
}
}
```
然后移动方式我们选择最基础的 `basic`**导航**我们选择最基础的 `navigation.walk`,我们都添加进公用的组件中去。当然别忘了最基础的 `physics` 组件以及设置血量的 `health` 组件:
```json
"minecraft:physics": {},
"minecraft:health": {
"value": 20,
"max": 20
}, // 设置血量
"minecraft:jump.static":{}, // 让僵尸拥有跳跃的能力,也就是能够上高一格方块的能力
"minecraft:movement.basic": {},
"minecraft:navigation.walk": {
"is_amphibious": true,
"can_pass_doors": true,
"can_walk": true,
"can_break_doors": true
}
```
然后再加入一个随机漫步的 **AI 组件**
```json
"minecraft:behavior.random_stroll": {
"priority": 7,
"interval": 20,
"speed_multiplier": 1
}
```
进入游戏测试,就可以看见我们的僵尸行走了起来:
![基础的行走能力](./assets/0_3.gif)
### 实体攻击
要让实体具备攻击能力,需要许多不同的组件才能正常工作:
- 向目标移动的**移动**和**导航**能力;
- 自主选择攻击哪个实体的**目标选择**能力;
- **攻击**能力,如近战或远程;
- **攻击伤害和效果**的设置;
#### 选择目标
> ❗️**注意**
>
> 即时你正在制作一个**不具备移动能力**的实体(如炮塔),**仍然需要添加导航**组件,以便让你的实体能够找到要射击的实体。
有许多方法能够让实体产生敌意。最常见的类型,就是如下所示的 `nearest_attackable_target` 组件,它通常允许您定义的实体有兴趣攻击哪些实体:
```json
"minecraft:behavior.nearest_attackable_target": {
"must_see": true, // 如果 Ture那么目标实体必须在视线范围内
"reselect_targets": true, // 如果一个目标比当前的目标更近,那么允许实体选择更近的这一个目标
"within_radius": 25.0, // 检测范围
"must_see_forget_duration": 17.0, // 如果 "must_see" = true, 这个时间就定义了忘记目标前的时间
"entity_types": [
{
"filters": {
// 要攻击的实体类型,这里是选择的玩家
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 48.0
}
]
}
```
为了实现更精细的控制,您还可以考虑下列组件:
| 组件 | 说明 |
| -------------------------------------------------------- | --------------------------------------------------- |
| minecraft:behavior.nearest_attackable_target | 目标实体满足给定要求 |
| minecraft:behavior.nearest_prioritized_attackable_target | 允许在每个过滤器后设置优先级 “priority” [integer] |
| minecraft:behavior.defend_trusted_target | 目标实体会损害筛选器中指定的任何实体 |
但其实还有一个——`minecraft:lookat`。这个组件跟上面三个略微不同,因为它是用于检测和瞄准尝试眼神接触的实体。这个有所了解就好,用得不多。它的结构是这样的:
```json
"minecraft:lookat": {
"search_radius": 64.0,
"set_target": true, // 如果为真,则成为有效目标
"look_cooldown": 5.0,
"filters": {
"all_of": [
{
"subject": "other",
"test": "is_family",
"value": "player"
},
{
"test": "has_equipment",
"domain": "head",
"subject": "other",
"operator": "not",
"value": "carved_pumpkin" // 所有没有带雕刻南瓜头的玩家都会成为有效目标
}
]
}
}
```
#### 攻击能力类型
以下是能够使用的攻击类型,我们只介绍其中最常用的两种:
| 组件 | 说明 |
| ------------------------------------------------------------ | -------------------------------------- |
| [minecraft:behavior.melee_attack](https://wiki.bedrock.dev/entities/entity-attack.html#melee) | 对单个目标造成伤害 |
| [minecraft:behavior.ranged_attack](https://wiki.bedrock.dev/entities/entity-attack.html#ranged) | 向目标发射弹射物 |
| [minecraft:area_attack](https://wiki.bedrock.dev/entities/entity-attack.html#area) | 对射程内的任何物体进行有效的近战攻击 |
| [minecraft:behavior.knockback_roar](https://wiki.bedrock.dev/entities/entity-attack.html#knockback-roar) | 与 minecraftarea_attack 类似,但更灵活 |
##### 近战攻击
近战攻击是最常见的类型,它们会造成击退:
```json
"minecraft:attack": {
"damage": 3, // 伤害值,可以设置为负数(此时为治疗效果)
"effect_name": "slowness", // 非必须参数,原版的效果都可以
"effect_duration": 20 // 非必须参数,攻击效果持续的时间
},
"minecraft:behavior.melee_attack": {
"priority": 3,
"melee_fov": 90.0, // 可以理解为攻击角度
"speed_multiplier": 1, // 有目标时移动速度的乘积
"track_target": false,
"require_complete_path": true
}
```
##### 远程攻击
以指定的时间间隔向目标发射指定的抛射物,这里造成的伤害就取决于抛射物:
```json
"minecraft:behavior.ranged_attack": {
"priority": 2,
"ranged_fov": 90.0, // 能够进行有效攻击的角度
"attack_interval_min": 1.0,
"attack_interval_max": 3.0,
"attack_radius": 15.0
},
"minecraft:shooter": {
"def": "minecraft:arrow"
}
```
#### 为僵尸添加添加基础的攻击能力
我们让成年僵尸拥有更高的攻击力2点然后省略掉其余的部分精简之后的 JSON 如下:
```json
{
"format_version": "1.17.20",
"minecraft:entity": {
"description": {
// ...省略...
},
"component_groups": {
"minecraft:zombie_baby": {
// ...省略...
"minecraft:attack": {
"damage": 1
}
},
"minecraft:zombie_adult": {
// ...省略...
"minecraft:attack": {
"damage": 2
}
}
},
"components": {
// ...省略...
"minecraft:behavior.nearest_attackable_target": {
"priority": 2,
"must_see": true, // 如果 Ture那么目标实体必须在视线范围内
"reselect_targets": true, // 如果一个目标比当前的目标更近,那么允许实体选择更近的这一个目标
"within_radius": 25.0, // 检测范围
"must_see_forget_duration": 17.0, // 如果 "must_see" = true, 这个时间就定义了忘记目标前的时间
"entity_types": [
{
"filters": {
// 要攻击的实体类型,这里是选择的玩家
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 48.0
}
]
},
"minecraft:behavior.melee_attack": {
"priority": 3,
"melee_fov": 90.0,
"speed_multiplier": 1,
"track_target": false,
"require_complete_path": true
}
// ...省略...
},
"events": {
// ...省略...
}
}
}
```
至此,我们就可以开始享受“跑路”了:
![僵尸跟着追](./assets/0_4.gif)
## 如何调试生物行为
在我们的测试客户端中,可以点击「设置」找到「调试」选项卡,下图中圈起来的就是跟生物行为相关的一些选项的(默认都是关闭的):
![测试客户端的调试设置](./assets/image-20231103114038608.png)
- 能见度边框:是否显示实体的碰撞箱;
![蓝色框就是行为 JSON 里面定义的碰撞箱](./assets/image-20231103114246018.png)
- 能见度路径:查看实体生成的路径,如果玩家有导航的能力,那么就会按照路径进行移动;
- 能见度目标状态:能够查看实体的 AI 组件列表,包含优先级、正在执行的、以及 **AI 组件依赖的基础组件**(括号里面就是依赖的组件);
![能见度目标状态演示](./assets/image-20231103114447470.png)
- 渲染生物信息状态:能够查看实体的属性列表、当前处于的**组件组**和正在执行的 AI 组件信息;
![生物信息状态演示](./assets/image-20231103114549090.png)
## 课后作业
本次课后作业,内容如下:
- 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具有基础的攻击和移动能力;
- 打开游戏客户端的调试功能,观察实体的行为;

View File

@@ -0,0 +1,772 @@
# 行为切换实战
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 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` 文件,修改原版僵尸的行为,使其具备基础的飞行和行走的转换能力,并尝试以下的方法:
- 基于组件的方式;
- 基于触发器的方式;
- 基于传感器的方式;
- 基于动画控制器的方式;

View File

@@ -0,0 +1,652 @@
# 特殊行为-拆墙
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将修改**原版僵尸**的行为,实现一个可以拆除墙体的僵尸。
在本教程中,您将学习以下内容。
- ✅自定义生物行为的实现;
- ✅拆墙行为的实现;
请点击[这里](https://g79.gdl.netease.com/DestroyWalls.zip)下载本章节课程的教学包
## 如何检测被墙体阻隔
想要实体在被墙体阻隔时,仍然保留对目标的仇恨的话,首先第一步就需要改造一下攻击目标选择的组件:
```json
"minecraft:behavior.nearest_attackable_target": {
"priority": 2,
"must_see": false, // 如果 Ture那么目标实体必须在视线范围内
```
`must_see` 组件改为 `false`,这样,就可以在被墙体阻隔的情况下仍然被实体所选择:
![被墙体阻隔仍然能选择目标](./assets/image-20231116095940409.png)
下一个问题就到了**如何检测被墙体阻隔**了。
我们检查和查阅所有的组件也好Molang 变量也好,发现并没有能够直接使用的,我们只能曲线救国了。
### break_blocks组件
我们查阅组件列表发现有一个 [`eat_blocks` 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitygoals/minecraftbehavior_eat_block?view=minecraft-bedrock-stable),但是实际测试之后并不能使用,原因就是因为这个组件是模拟山羊用来吃掉脚下方块的组件,想要达到拆墙的效果并不可行,效果演示:
![EatBlock组件演示](./assets/3_1.gif)
另外还发现了一个 [`break_blocks` 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_break_blocks?view=minecraft-bedrock-stable),这可能是一个符合要求的选择,但是如果只是单纯加入,就会表现得横冲直撞(即使在没有目标时):
![BreakBlocks组件演示1](./assets/3_2.gif)
我们想要他在发现目标之后再进行一个短暂的破坏(也就是说需要冷却),我们可以使用上节课说到的传感器和 Timer 组件来配合使用:
```json
{
// 省略了其他无关信息
"minecraft:entity": {
"component_groups": {
"destroy_block_sensor": {
// 这里不能使用传感器,是因为如果当加入传感器时已经有了目标时,就无法触发事件了
// 所以之类我们改成了使用 Timer 来 Tick 检测的方式
"minecraft:timer": {
"looping": true,
"randomInterval": true,
"time": [1, 1.5],
"time_down_event": {
"event": "tutorial:destroy_block_ready"
}
}
},
"destroy_block_ready": {
"minecraft:timer": {
"looping": false,
"randomInterval": true,
"time": [1, 1.5],
"time_down_event": {
"event": "tutorial:destroy_block"
}
}
},
"destroy_block": {
"minecraft:break_blocks": {
"breakable_blocks": [
"stone",
"glass"
]
},
"minecraft:timer": {
"looping": true,
"randomInterval": true,
// 给一个足够短的时间,让它只能破坏一堵墙
"time": [0.2, 0.2],
"time_down_event": {
"event": "tutorial:destroy_block_times_up"
}
}
}
},
"events": {
// 自定义事件
"tutorial:destroy_block_ready": {
// 如果有目标的话,则加入 "destroy_block_ready" 组件组,否则就继续执行 Timer
"filters": {
"test": "has_target"
},
"add": {
"component_groups": ["destroy_block_ready"]
},
"remove": {
"component_groups": ["destroy_block_sensor"]
}
},
"tutorial:destroy_block": {
"add": {
"component_groups": ["destroy_block"]
},
"remove": {
"component_groups": ["destroy_block_ready"]
}
},
"tutorial:destroy_block_times_up": {
"add": {
"component_groups": ["destroy_block_sensor"]
},
"remove": {
"component_groups": ["destroy_block"]
}
}
}
}
}
```
看上去有点长,但实际上这个流程还是很清晰的:
![组件循环演示](./assets/image-20231116145013558.png)
我们加入游戏之后测试:
![BreakBlocks组件演示2](./assets/3_3.gif)
可以看到,虽然也能达到类似效果,但是这个 `break_blocks` 组件是机械暴力的拆掉路径上的所有方块,并没有检测的功能。
### 动画控制器+Molang
还记得我们上节课提到过一个 `query.walk_distance` 的 Molang 变量,它会返回实体当前行走的总长度。
如果说一个实体在有目标的情况下长时间没有移动(总路程没变),是不是也可以侧面说明实体被“墙体”挡住了?
理论存在,开始实践。首先添加上控制器:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
// 破坏墙体
"controller.animation.zombie.destroy_wall": {
"initial_state": "default",
"states": {
"default": {
"on_entry": [
"/say start wall check...",
// 刚开始检测时的初始距离
"v.start_distance = query.walk_distance;",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
"v.time2check = query.time_stamp + 1 * 20;"
],
"transitions": [
{
// 移动的距离足够短并且时间到了 tick 时间,条件满足则开始破坏方块
"start_destroy_block": "query.walk_distance - v.start_distance < 1 && query.time_stamp >= v.time2check"
},
{
// 不满足条件则进入冷却重新进入检测
"cooldown": "query.walk_distance - v.start_distance >= 1 && query.time_stamp >= v.time2check"
}
]
},
"start_destroy_block": {
"on_entry": [
"/say start destroy wall",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
"v.time2check = query.time_stamp + 0.1 * 20;",
"@s tutorial:destroy_block"
],
"on_exit": [
"/say finished destroy wall",
"@s tutorial:destroy_block_finished"
],
"transitions": [
{
"cooldown": "query.time_stamp >= v.time2check"
}
]
},
"cooldown": {
"on_entry": [
"/say destroy wall in cooling",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
"v.time2cooldown = query.time_stamp + 1 * 20;"
],
"transitions": [
{
"default": "query.time_stamp >= v.time2cooldown"
}
]
}
}
}
}
}
```
此时的 `zombie.json` 如下:
```json
{
"format_version": "1.16.0",
"minecraft:entity": {
"description": {
// ...省略基础定义...
"animations": {
"destroy_block_sensor": "controller.animation.zombie.destroy_wall"
},
"scripts": {
"animate": [
{
// 只有在有目标的情况下才执行控制器的逻辑
"destroy_block_sensor": "query.has_target"
}
]
}
},
"component_groups": {
"destroy_block": {
"minecraft:break_blocks": {
"breakable_blocks": [
"stone",
"glass"
]
}
},
// ...省略 baby/ adult 和 walk 行为组
},
"components": {
// ...省略组件...
},
"events": {
// ...省略出生事件...
// 自定义事件
"tutorial:destroy_block": {
"add": {
"component_groups": ["destroy_block"]
}
},
"tutorial:destroy_block_finished": {
"remove": {
"component_groups": ["destroy_block"]
}
}
}
}
}
```
这其实跟上面第一种方式的本质区别就是用更加灵活的 Molang 变量替代了死板的 `Timer` 组件。
这样做之后的区别就是,僵尸不会再一股脑的破坏掉路径上的所有方块,而是会在检测走不动时,才会开始尝试破坏方块:
![使用动画控制器的演示](./assets/3_4.gif)
这样虽然也是模拟了检测的情况,也尽最大可能还原了拆墙的行为逻辑,但是也不是真正意义上的检测。
我们会发现原版的组件和 Molang 变量似乎并不能真正满足需求了,如果我们想要实体行更好用,这时候就需要用到我们的自定义生物行为了。
## 自定义生物行为
> 如果对这个内容不熟悉的同学,请自行前往阅读[官方文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/20-%E7%8E%A9%E6%B3%95%E5%BC%80%E5%8F%91/15-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B8%B8%E6%88%8F%E5%86%85%E5%AE%B9/3-%E8%87%AA%E5%AE%9A%E4%B9%89%E7%94%9F%E7%89%A9/01-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9F%BA%E7%A1%80%E7%94%9F%E7%89%A9.html?catalog=1)。
在最新的 2.9 版本中,[`getEntitiesOrBlockFromRay` 接口](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E4%B8%96%E7%95%8C/%E5%AE%9E%E4%BD%93%E7%AE%A1%E7%90%86.html#getentitiesorblockfromray)新增了对射线上方块获取的支持,这个 API 接口可以从指定位置发射一条射线,获取与射线相交的实体和方块信息。
我们可以以此为基础来创造一个使用**最短路径**破坏墙体来攻击目标的自定义僵尸,实现之后的演示如下:
![自定义生物行为-挖墙演示](./assets/3_5.gif)
完整代码示例如下:
```python
# coding=utf-8
import mod.server.extraServerApi as serverApi
from mod.common.utils.mcmath import Vector3
CustomGoalCls = serverApi.GetCustomGoalCls()
CompFactory = serverApi.GetEngineCompFactory()
class DestroyWall(CustomGoalCls):
def __init__(self, entityId, argsJson):
super(DestroyWall, self).__init__(entityId, argsJson)
self.mEntityId = entityId
self.mTimeCounter = 0
# region 继承函数
def CanUse(self):
if self._HasTarget() and self._IsTargetAlive() and self._HasBlockBetweenTargetAndCanDestroyBlock():
return True
return False
def CanContinueToUse(self):
return self.CanUse()
def CanBeInterrupted(self):
return True
def Start(self):
self.mTimeCounter = 0
self._FaceToTarget()
def Stop(self):
pass
def Tick(self):
self.mTimeCounter += 1
perSec = self.mTimeCounter % 20 == 0
if perSec:
# 限制僵尸 1s 只能破坏一个方块
self._DestroyFrontBlock()
# endregion
# region 类函数
def _HasTarget(self):
#是否有仇恨目标
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
hasTarget = targetId != "-1"
return hasTarget
def _IsTargetAlive(self):
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
comp = CompFactory.CreateGame(serverApi.GetLevelId())
alive = comp.IsEntityAlive(targetId)
return alive
# 跟目标之间是否存在方块并且是否能够破坏
def _HasBlockBetweenTargetAndCanDestroyBlock(self):
blockDictList = self._GetFirstBlockFromRay()
if len(blockDictList) == 0:
return False
blockDict = blockDictList[0]
blockPos = blockDict['pos']
selfPos = CompFactory.CreatePos(self.mEntityId).GetFootPos()
distance2block = (Vector3(blockPos) - Vector3(selfPos)).Length()
return distance2block < 2 # 2 是手长,也就是只有够得到的方块才能够被破坏
# 面向目标
def _FaceToTarget(self):
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
targetPosX, targetPosY, targetPosZ = CompFactory.CreatePos(targetId).GetFootPos()
entityPosX, entityPosY, entityPosZ = CompFactory.CreatePos(self.mEntityId).GetFootPos()
diffPos = (targetPosX - entityPosX, targetPosY - entityPosY, targetPosZ - entityPosZ)
CompFactory.CreateRot(self.mEntityId).SetRot(serverApi.GetRotFromDir(diffPos))
# 破坏生物面前的方块
def _DestroyFrontBlock(self):
blockDictList = self._GetFirstBlockFromRay()
if blockDictList:
blockDict = blockDictList[0]
blockPos = blockDict['pos']
blockInfoComp = CompFactory.CreateBlockInfo(self.mEntityId)
dimensionId = CompFactory.CreateDimension(self.mEntityId).GetEntityDimensionId()
blockInfoComp.SetBlockNew(blockPos, {'name': 'minecraft:air'}, 0, dimensionId)
# 获得视线范围内第一个方块信息
def _GetFirstBlockFromRay(self):
dimensionId = CompFactory.CreateDimension(self.mEntityId).GetEntityDimensionId()
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
targetPosX, targetPosY, targetPosZ = CompFactory.CreatePos(targetId).GetFootPos()
entityPosX, entityPosY, entityPosZ = CompFactory.CreatePos(self.mEntityId).GetFootPos()
# 这里获取碰撞箱的高度,因为 FootPos 是从脚底开始计算的坐标,这里加上碰撞箱 -0.2 模拟的是头部眼睛的高度
_, collisionBoxHeight = CompFactory.CreateCollisionBox(self.mEntityId).GetSize()
_, targetCollisionBoxHeight = CompFactory.CreateCollisionBox(targetId).GetSize()
targetPosY = targetPosY + targetCollisionBoxHeight - 0.2
entityPosY = entityPosY + collisionBoxHeight - 0.2
rot = (targetPosX - entityPosX, targetPosY - entityPosY, targetPosZ - entityPosZ)
distance = int(Vector3(rot).Length() - 1) # 这个地方用小数会非常卡,所以取一个 int主要是防止射线击穿实体
blockDictList = serverApi.getEntitiesOrBlockFromRay(
dimensionId, (entityPosX, entityPosY, entityPosZ), rot, distance, False,
serverApi.GetMinecraftEnum().RayFilterType.OnlyBlocks
)
return blockDictList
# endregion
```
此时的 `zombie.json` 文件如下:
```json
// ...省略其他无关信息...
"component_groups": {
"destroy_block": {
"minecraft:behavior.python_custom:destroy_wall": {
"priority": 1,
"module_path": "destroyWallScripts.destroyWall",
"class_name": "DestroyWall",
"control_flags": ["move"]
}
},
```
### 破坏时间的支持
上面的方块破坏是无差别破坏路径上的所有方块,连基岩也逃不过,这明显是不合理的。
我们可以通过配置配合 [`GetDestroyTotalTime` 接口](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E4%B8%96%E7%95%8C/%E6%96%B9%E5%9D%97%E7%AE%A1%E7%90%86.html#getdestroytotaltime)来限制自定义实体能够破坏方块的种类。我们稍微改造一下 `_HasBlockBetweenTargetAndCanDestroyBlock` 函数:
```python
# 跟目标之间是否存在方块并且是否能够破坏
def _HasBlockBetweenTargetAndCanDestroyBlock(self):
blockDictList = self._GetFirstBlockFromRay()
if len(blockDictList) == 0:
return False
blockDict = blockDictList[0]
blockPos = blockDict['pos']
selfPos = CompFactory.CreatePos(self.mEntityId).GetFootPos()
distance2block = (Vector3(blockPos) - Vector3(selfPos)).Length()
if distance2block > 2:
# 2 是手长,也就是只有够得到的方块才能够被破坏
return False
# 还需要检测手上的物品是否能够破坏掉方块
destroyTotalTime = self._GetTargetBlockDestroyTime(blockDict['identifier'])
canDestroy = destroyTotalTime > 0
return canDestroy
def _GetTargetBlockDestroyTime(self, blockIdentifier):
blockInfoComp = CompFactory.CreateBlockInfo(self.mEntityId)
carriedItem = CompFactory.CreateItem(self.mEntityId).GetEntityItem(serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
totalTime = blockInfoComp.GetDestroyTotalTime(blockIdentifier, None if not carriedItem else carriedItem['itemName'])
return totalTime
```
并且我们新增对破坏时间的支持,也就是说,实体真的需要这么多时间来破坏方块:
```python
# coding=utf-8
import mod.server.extraServerApi as serverApi
from mod.common.utils.mcmath import Vector3
CustomGoalCls = serverApi.GetCustomGoalCls()
CompFactory = serverApi.GetEngineCompFactory()
# 省略了其他无关的函数...
class DestroyWall(CustomGoalCls):
def __init__(self, entityId, argsJson):
super(DestroyWall, self).__init__(entityId, argsJson)
self.mEntityId = entityId
self.mTimeCounter = 0
self.mTargetBlockDestroyTime = 0 # 目标方块破坏时间
def Start(self):
self.mTimeCounter = 0
self._FaceToTarget()
self._ResetDestroyTime() # 开始执行时设置目标方块破坏时间
def Tick(self):
self.mTimeCounter += 1
time2destroy = self.mTimeCounter >= int(self.mTargetBlockDestroyTime * 20) # 1 秒 20 帧
perSec = self.mTimeCounter % 20 == 0
if time2destroy:
self._DestroyFrontBlock()
self._ResetDestroyTime()
elif perSec:
# 使用 /say 命令来监控实体当前的状态
leftTime = self.mTargetBlockDestroyTime - self.mTimeCounter / 20.0
CompFactory.CreateCommand(self.mEntityId).SetCommand('/say 正在破坏方块,还剩:{}s'.format(leftTime))
def _ResetDestroyTime(self):
blockDict = self._GetFirstBlockFromRay()[0]
self.mTargetBlockDestroyTime = self._GetTargetBlockDestroyTime(blockDict['identifier'])
# 使用 /say 命令在游戏中输出破坏时间
CompFactory.CreateCommand(self.mEntityId).SetCommand('/say 破坏时间:{}s'.format(self.mTargetBlockDestroyTime))
```
一番改造之后,可以进入游戏查看效果:
![自定义生物行为-挖墙演示2](./assets/3_6.gif)
### 动画支持
可以看到,虽然支持了破坏时间,但是并没有破坏的动画,显得非常呆板。我们尝试给正在破坏方块的实体加入一个破坏方块的动画。
如果是我们自定义的实体,可以很方便的使用自定义的 `query.mod.xxx` 或者直接使用播放动画的接口来实现播放自己动画的效果。
但我们这里使用的是原版资源,原版是通过 `variable.attack_time` 来实现的攻击动画,查看原版的动画文件也是使用的 Molang 来实现:
```json
"animation.humanoid.attack.rotations.v1.0" : {
"loop" : true,
"bones" : {
"body" : {
"rotation" : [ 0.0, "(math.sin(math.sqrt(variable.attack_time) * 360) * 11.46) - this", 0.0 ]
},
"leftarm" : {
"rotation" : [ "(math.sin(math.sqrt(variable.attack_time) * 360) * 11.46)", 0.0, 0.0 ]
},
"rightarm" : {
"rotation" : [ "math.sin(1.0 - math.pow(1.0 - variable.attack_time, 3.0) * 180.0) * (variable.is_brandishing_spear ? -1.0 : 1.0 )", "variable.is_brandishing_spear ? 0.0 : (math.sin(math.sqrt(variable.attack_time) * 360) * 11.46) * 2.0", 0.0 ]
}
}
},
```
`variable.attack_time` 我们无法更改。所以我们只能曲线救国了。
虽然不能触发原版的攻击动画,但是可以用玩家的 `swim` 动画来代替(模拟有动画的情况),最终效果如下:
![自定义生物行为-挖墙演示3](./assets/3_7.gif)
怎么实现的呢?首先我们需要把动画加入到实体之中去,为了不破坏原版文件,我们使用代码的方式无侵入式的加入玩家的有用动画:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
CompFactory = clientApi.GetEngineCompFactory()
class CustomEntityClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(CustomEntityClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "OnLocalPlayerStopLoading",
self, self.OnLocalPlayerStopLoading)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddEntityClientEvent",
self, self.OnAddEntityClientEvent)
def OnLocalPlayerStopLoading(self, args=None):
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
zombieEntityList = gameComp.GetEntitiesAroundByType(clientApi.GetLocalPlayerId(), 48,
clientApi.GetMinecraftEnum().EntityType.Zombie)
for _entityId in zombieEntityList:
# 这里考虑的存量的僵尸
self.RegisterZombieAnimate(_entityId)
def OnAddEntityClientEvent(self, args):
entityId = args['id']
engineTypeStr = args['engineTypeStr']
# 这里是考虑的新增的僵尸
if engineTypeStr == 'minecraft:zombie':
self.RegisterZombieAnimate(entityId)
def RegisterZombieAnimate(self, entityId=None):
if entityId is None:
entityId = clientApi.GetLevelId()
actorRender = CompFactory.CreateActorRender(entityId)
actorIdentifier = 'minecraft:zombie'
actorRender.AddActorAnimation(actorIdentifier, "custom_attack", "animation.player.swim")
actorRender.AddActorScriptAnimate(actorIdentifier, "custom_attack", "query.variant == 1.0")
res = actorRender.RebuildActorRender(actorIdentifier)
print '================RegisterZombieAnimate=========', res
```
我们需要给动画绑定一个条件,我们在第一课 `实体基础` 的时候了解过很多用于标识物体状态的组件,比如 `minecraft:variant``minecraft:mark_variant``minecraft:skin_id` 等,我们这里直接选择一个使用就行了。
对应的我们也需要在 `zombie.json` 加入组件和对应的事件:
```json
{
// 省略了其他无关内容
"format_version": "1.16.0",
"minecraft:entity": {
"component_groups": {
"destroy_block": {
"minecraft:behavior.python_custom:destroy_wall": {
"priority": 1,
"module_path": "destroyWallScripts.destroyWall",
"class_name": "DestroyWall",
"control_flags": ["move"]
}
},
"destroy_block_attack": {
"minecraft:variant": {
"value": 1
}
},
"destroy_block_attack_not_play": {
"minecraft:variant": {
"value": 0
}
},
},
"events": {
// 自定义事件
"tutorial:destroy_block_attack": {
"add": {
"component_groups": ["destroy_block_attack"]
},
"remove": {
"component_groups": ["destroy_block_attack_not_play"]
}
},
"tutorial:cancel_destroy_block_attack": {
"add": {
"component_groups": ["destroy_block_attack_not_play"]
},
"remove": {
"component_groups": ["destroy_block_attack"]
}
},
}
}
}
```
这里有一个细节是一旦我们加入了 `minecraft:variant` 这样用于标记的组件之后,除非我们主动改变数值,否则标记就会一直存在。所以我们这里还需要一个额外的 `destroy_block_attack_not_play` 组件组来控制。
然后在我们自定义行为开始和结束的时候,使用 API 对应触发事件就行了:
```python
class DestroyWall(CustomGoalCls):
def Start(self):
# ...........
self._TriggerEntityEvent("tutorial:destroy_block_attack") # 设置超远距离的攻击
def Stop(self):
self._TriggerEntityEvent("tutorial:cancel_destroy_block_attack") # 还原普通攻击
def _TriggerEntityEvent(self, eventName):
comp = CompFactory.CreateEntityEvent(self.mEntityId)
comp.TriggerCustomEvent(self.mEntityId, eventName)
# 使用 /say 命令输出打印
CompFactory.CreateCommand(self.mEntityId).SetCommand('/say 触发自定义事件:{}'.format(eventName))
```
## 小结
我们这节课使用了原版组件、动画控制器以及自定义生物行为来实现我们的拆墙。会发现,其实实现生物行为的思路有很多,虽然网易给出的自定义行为能够突破微软组件的限制,但也是需要再足够了解 API 的情况下。
另外复杂的生物行为也可能会存在性能问题。这在后续自己实现生物行为的时会更了解到这一点。
## 课后作业
本次课后作业,内容如下:
- 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具备拆墙的行为(多种方式);
- 打开客户端的调试功能,观察并测试;

View File

@@ -0,0 +1,305 @@
# 特殊行为-搭路
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将修改**原版僵尸**的行为,实现一个可以在脚下搭路追击目标的功能。
在本教程中,您将学习以下内容。
- ✅自定义生物行为的实现;
- ✅搭路行为的实现;
请点击[这里](https://g79.gdl.netease.com/BuildRoads.zip)下载本章节课程的教学包
## 需要搭路的几种情况
原版僵尸的行为十分呆板,如果玩家躲藏到高空,那么原版的僵尸对于玩家来说几乎没有任何威胁。
所以为了增加游戏难度,我们来实现一下僵尸追击的功能。首先,第一个问题就是,如何检测什么时候应该执行搭路的行为呢?我们先来分析一波,理清思路。
### 存在高度差
最容易想到的一个情况就是实体跟目标之间存在高度差。因为在原版游戏中,如果我们与目标有一定的距离之后,目标会停留在我们的脚下驻足观望站在高处的我们:
![](./assets/image-20231118140323953.png)
在这种情况下,我们需要检测存在高度差,并且已经长时间驻足的情况。长时间驻足我们在上一节课中学习了,可以使用动画控制器 + `query.walk_distance` ,来完成。问题是**高度差如何检测**。
翻看原版组件之后发现并没有什么好的办法。所以不得不**使用自定义生物行为**了。
### 没有路
还有一种情况是,目标与实体之间不存在高度差,但就是没有路给实体过去:
![](./assets/image-20231118150125763.png)
这种情况如果不选择像我们之前课程介绍的那样切换飞行状态**飞过去**,那么也就只能搭路了。
不过也要考虑是不是被墙体阻隔,这是上节课的内容。我们这里只是提一下。
## 代码实现
经过上面的分析之后,写代码逻辑就很清晰了,完整代码如下:
```python
# coding=utf-8
import mod.server.extraServerApi as serverApi
from math import floor
from mod.common.utils.mcmath import Vector3
CustomGoalCls = serverApi.GetCustomGoalCls()
CompFactory = serverApi.GetEngineCompFactory()
class PutBlock(CustomGoalCls):
def __init__(self, entityId, argsJson):
super(PutBlock, self).__init__(entityId, argsJson)
self.mEntityId = entityId
self.mTimeCounter = 0
#
self.mDimensionId = CompFactory.CreateDimension(self.mEntityId).GetEntityDimensionId()
# region 继承函数
def CanUse(self):
if self._HasTarget() and self._IsTargetAlive() and (self._CheckHeightDifferenceWithTarget() or self._CheckPathAheadForFooting()):
return True
return False
def CanContinueToUse(self):
return self.CanUse()
def CanBeInterrupted(self):
return True
def Start(self):
self.mTimeCounter = 0
CompFactory.CreateCommand(self.mEntityId).SetCommand("/say 开始搭方块")
def Stop(self):
CompFactory.CreateCommand(self.mEntityId).SetCommand("/say 结束搭方块")
def Tick(self):
self.mTimeCounter += 1
perSec = self.mTimeCounter % 20 == 0
if perSec:
self._PutBlockToTarget()
# endregion
# region 类函数
def _HasTarget(self):
#是否有仇恨目标
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
hasTarget = targetId != "-1"
return hasTarget
def _IsTargetAlive(self):
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
comp = CompFactory.CreateGame(serverApi.GetLevelId())
alive = comp.IsEntityAlive(targetId)
return alive
# 检查与目标之间是否存在高度差
def _CheckHeightDifferenceWithTarget(self):
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
targetPos = CompFactory.CreatePos(targetId).GetFootPos()
selfPos = CompFactory.CreatePos(self.mEntityId).GetFootPos()
heightDifference = abs(targetPos[1] - selfPos[1])
return heightDifference >= 1.0 # 因为是获取的 FootPos所以这里的 2 就是 2 个方块的高度
# 检查前往目标的路径,脚下是否有路
def _CheckPathAheadForFooting(self):
blockInfoComp = CompFactory.CreateBlockInfo(self.mEntityId)
aheadBlockPos = self._GetPathAheadForFootingBlockPos()
blockDict = blockInfoComp.GetBlockNew(aheadBlockPos, self.mDimensionId)
return not blockDict or blockDict['name'] == 'minecraft:air'
# 获取前方脚下的方块位置(这个是不依赖导航,前往目标的最近位置)
def _GetPathAheadForFootingBlockPos(self):
comp = CompFactory.CreateAction(self.mEntityId)
targetId = comp.GetAttackTarget()
targetPosX, targetPosY, targetPosZ = CompFactory.CreatePos(targetId).GetFootPos()
entityPosX, entityPosY, entityPosZ = CompFactory.CreatePos(self.mEntityId).GetFootPos()
# 去掉 y 方向上的不同,因为我们是要检测的是目标前方脚下一格的位置
diff = Vector3(targetPosX - entityPosX, 0, targetPosZ - entityPosZ).Normalized()
result = (entityPosX + diff[0], entityPosY - 0.5, entityPosZ + diff[2])
result = tuple(map(int, map(floor, result))) # 把实体坐标转换成方块坐标
return result
# 向目标搭建搭建方块
def _PutBlockToTarget(self):
if self._CheckHeightDifferenceWithTarget():
# 第一种情况:存在高度差,那么就需要实体自己跳一下,然后在脚下搭建一个方块
self._JumpAndPutBlockOnFoot()
else:
# 第二种情况:那就是在前方搭建一个方块
self._PutBlockToAheadFoot()
def _JumpAndPutBlockOnFoot(self):
resBlockPos = CompFactory.CreatePos(self.mEntityId).GetFootPos()
resBlockPos = tuple(map(int, map(floor, resBlockPos))) # 把实体坐标转换成方块坐标
# 先把自己弹起来
actionComp = CompFactory.CreateAction(self.mEntityId)
actionComp.SetMobKnockback(0, 0, 0.65, 0.65, 1)
# 需要等待实体跳起来之后再放置方块
CompFactory.CreateGame(self.mEntityId).AddTimer(0.3, self._PutBlock, resBlockPos)
def _PutBlockToAheadFoot(self):
resBlockPos = self._GetPathAheadForFootingBlockPos()
self._PutBlock(resBlockPos)
def _PutBlock(self, resBlockPos):
CompFactory.CreateBlockInfo(self.mEntityId).SetBlockNew(resBlockPos, {'name': "minecraft:stone"}, 0, self.mDimensionId)
# endregion
```
代码量不大,里面有几个比较容易忽略的点。
- **实体坐标转换成方块坐标**:因为实体和方块使用的坐标体系不太一样,方块在 `x``z` 方向上会有 0.5 的偏移量,所以我们这里直接使用了诸如 `resBlockPos = tuple(map(int, map(floor, resBlockPos)))` 这样的代码来进行转换,不熟悉的同学可以直接记住这个方法;
- **计算方向向量**:我们使用了 `Vector3` 模块的 `Normalized` 函数,这个方法会返回长度为 1 时的标准向量。与之类似的有一个方法是 `Normalize`,区别在于前者是返回一个标准化后的向量,而后者没有返回,而是把 `a.Normalize()` 中的 `a` 给标准化。这是比较容易混淆和搞错的地方。
另外,上面的方法并没有检测实体长时间没有移动的情况,会导致实体在发现目标之后就直接开始检测是否需要搭方块,实际效果如下:
![自定义生物行为-搭方块演示1](./assets/4_1.gif)
我们需要配合动画控制和上节课提到的 `query.walk_distance` 来让行为更合理。
### 动画控制器 + Molang 控制开始
先把我们的 `zombie.json` 文件给准备好,添加上对应的组件组和事件:
```json
{
// 省略其他无关内容...
"format_version": "1.16.0",
"minecraft:entity": {
"description": {
"animations": {
"put_block_sensor": "controller.animation.zombie.put_block"
},
"scripts": {
"animate": [
{
// 只有在有目标的情况下才执行控制器的逻辑
"put_block_sensor": "query.has_target"
}
]
}
},
"component_groups": {
"putBlock": {
"minecraft:behavior.python_custom:put_block": {
"priority": 1,
"module_path": "putBlockScripts.putBlock",
"class_name": "PutBlock",
"control_flags": ["move"]
}
},
},
"events": {
// 自定义事件
"tutorial:put_block": {
"add": {
"component_groups": ["putBlock"]
}
},
"tutorial:put_block_finished": {
"remove": {
"component_groups": ["putBlock"]
}
}
}
}
}
```
我们控制器的逻辑也很简单,就是检测在长时间没有移动的情况下,加入 `putBlock` 组件组,尝试开始搭建方块,然后在工作一次之后进入冷却时间就行了:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
// 搭建方块的检测
"controller.animation.zombie.put_block": {
"initial_state": "default",
"states": {
"default": {
"on_entry": [
"/say start put block check...",
// 刚开始检测时的初始距离
"v.start_distance = query.walk_distance;",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
"v.time2check = query.time_stamp + 2 * 20;"
],
"transitions": [
{
// 移动的距离足够短并且时间到了 tick 时间,条件满足则开始搭建方块
"start_put_block": "query.walk_distance - v.start_distance < 2 && query.time_stamp >= v.time2check"
},
{
// 不满足条件则进入冷却重新进入检测
"cooldown": "query.walk_distance - v.start_distance >= 1 && query.time_stamp >= v.time2check"
}
]
},
"start_put_block": {
"on_entry": [
"/say start put block",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
// 这里工作时间设置成跟代码中一样的 1 秒时间,这样就刚好能工作一次
"v.time2check = query.time_stamp + 1 * 20;",
"@s tutorial:put_block"
],
"on_exit": [
"@s tutorial:put_block_finished"
],
"transitions": [
{
"cooldown": "query.time_stamp >= v.time2check"
}
]
},
"cooldown": {
"on_entry": [
"/say put block in cooling",
// 需要检测的时间,游戏一帧是 20 帧,这里 x * 20 就是 x 秒
"v.time2cooldown = query.time_stamp + 1 * 20;"
],
"transitions": [
{
"default": "query.time_stamp >= v.time2cooldown"
}
]
}
}
}
}
}
```
加入之后测试,行为就正常多了:
![自定义生物行为-搭方块演示2](./assets/4_2.gif)
## 小结
原版组件的性能始终是要更好的,所以多数情况下,我们会在原版组件实在是无法满足的时候才会使用自定义行为(为了图方便也不是不可以)。
并且会尝试使用动画控制器 + Molang 的混合方式,熟悉之后也就是思路的问题了。
## 课后作业
本次课后作业,内容如下:
- 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具备搭方块的行为;
- 打开客户端的调试功能,观察并测试;