2.6
This commit is contained in:
821
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/0-实体基础.md
Normal file
821
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/0-实体基础.md
Normal file
@@ -0,0 +1,821 @@
|
||||
# 实体基础
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你从零开始修改原版僵尸的行为,从而帮助你认识和了解生物行为的构成。
|
||||
|
||||
本系列不特意涉及实体资源的相关教程,如果对这一块不熟悉的同学,请自行前往前往官网查看相关教程。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅如何查看原版行为文件;
|
||||
- ✅认识和了解行为文件的基本构成;
|
||||
- ✅尝试修改原版僵尸的行为并实现僵尸的基础行为:
|
||||
- 手动还原最基础的僵尸行为;
|
||||
- 出生随机大小的僵尸;
|
||||
|
||||
请点击[这里](https://g79.gdl.netease.com/Entity.zip)下载本章节课程的教学包
|
||||
|
||||
|
||||
## 原版僵尸的行为
|
||||
|
||||
### 如何查看原版文件
|
||||
|
||||
在本地游戏测试客户端的目录中(MC Studio 安装目录下的 `\game\MinecraftPE_Netease` 可以找到),我们可以直接查看和学习生物的行为 JSON 是如何编写的:
|
||||
|
||||

|
||||
|
||||
使用打开目录之后跟我们正常编写的附加包并无差别,略微的区别就是像是 `attachables`、`biomes`、`feature_rules`、`features` 这样的定义是保存在 `definitions` 目录下的(我们自己写是需要写在 `behavior_packs` 对应目录下):
|
||||
|
||||

|
||||
|
||||
如此,我们就可以在对应行为包下找到相关的原版行为 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),这个组件允许实体分享和拾取一些道具,这直接导致了僵尸能够拾取地上物品的行为 :
|
||||
|
||||

|
||||
|
||||
### 格式概述
|
||||
|
||||
实体行为都保存在行为包的 `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)找到。官方文档不仅有详细的参数解释,还基本都配套有一些例子和原版的应用:
|
||||
|
||||

|
||||
|
||||
另一个常用的[基岩版文档网站](https://bedrock.dev/zh)也建议存储起来。这两个网站通常需要配合起来查阅。
|
||||
|
||||

|
||||
|
||||
我们也可以通过查看《我的世界》默认实体的行为文件,来了解组件以及应该在实践中如何使用(参考上方的 #如何查看原版文件)。
|
||||
|
||||
#### 核心组件
|
||||
|
||||
| 组件名称 | 选项 | 描述 |
|
||||
| :----------------------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| `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`)、是否存在优先级冲突、是否没有添加依赖的基础组件(请参考 #如何调试生物行为 查看)之外,还应该考虑这个组件是否是为某个生物准备的专属组件。
|
||||
|
||||
比如原版的末影龙就会有许多硬编码的组件供他专用:
|
||||
|
||||

|
||||
|
||||
我们查看史莱姆的相关组件也可以看到类似的说明:
|
||||
|
||||

|
||||
|
||||
如果我们仍然想要使用相关组件的话,那么就需要把 `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 比例生成成年僵尸和幼年僵尸的行为:
|
||||
|
||||

|
||||
|
||||
有了这个基础,我们再来一步一步还原僵尸的基础行为。
|
||||
|
||||
### 实体移动
|
||||
|
||||
在我的世界中,实体有能力通过行走、游泳或者飞行在世界中移动。想要自定义的实体获得这些能力,请记住按照以下步骤添加相关的组件:
|
||||
|
||||
- **设置实体移动速度**的组件;
|
||||
- 用于**设置实体移动方式**(行走、飞行等)的组件;
|
||||
- 用于**设置实体导航功能**的组件, 以便让他们可以生成路径;
|
||||
- 设置**实体移动的位置/时间**(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
|
||||
}
|
||||
```
|
||||
|
||||
进入游戏测试,就可以看见我们的僵尸行走了起来:
|
||||
|
||||

|
||||
|
||||
### 实体攻击
|
||||
|
||||
要让实体具备攻击能力,需要许多不同的组件才能正常工作:
|
||||
|
||||
- 向目标移动的**移动**和**导航**能力;
|
||||
- 自主选择攻击哪个实体的**目标选择**能力;
|
||||
- **攻击**能力,如近战或远程;
|
||||
- **攻击伤害和效果**的设置;
|
||||
|
||||
#### 选择目标
|
||||
|
||||
> ❗️**注意**
|
||||
>
|
||||
> 即时你正在制作一个**不具备移动能力**的实体(如炮塔),**仍然需要添加导航**组件,以便让你的实体能够找到要射击的实体。
|
||||
|
||||
有许多方法能够让实体产生敌意。最常见的类型,就是如下所示的 `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": {
|
||||
// ...省略...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
至此,我们就可以开始享受“跑路”了:
|
||||
|
||||

|
||||
|
||||
## 如何调试生物行为
|
||||
|
||||
在我们的测试客户端中,可以点击「设置」找到「调试」选项卡,下图中圈起来的就是跟生物行为相关的一些选项的(默认都是关闭的):
|
||||
|
||||

|
||||
|
||||
- 能见度边框:是否显示实体的碰撞箱;
|
||||

|
||||
- 能见度路径:查看实体生成的路径,如果玩家有导航的能力,那么就会按照路径进行移动;
|
||||
- 能见度目标状态:能够查看实体的 AI 组件列表,包含优先级、正在执行的、以及 **AI 组件依赖的基础组件**(括号里面就是依赖的组件);
|
||||

|
||||
|
||||
- 渲染生物信息状态:能够查看实体的属性列表、当前处于的**组件组**和正在执行的 AI 组件信息;
|
||||

|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具有基础的攻击和移动能力;
|
||||
- 打开游戏客户端的调试功能,观察实体的行为;
|
||||
772
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/1-行为切换实战.md
Normal file
772
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/1-行为切换实战.md
Normal file
@@ -0,0 +1,772 @@
|
||||
# 行为切换实战
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将修改**原版僵尸**的行为,实现一个可在**行走**和**飞行**之间切换的进化版僵尸,来扩展你对生物行为实现方式的理解。
|
||||
|
||||
注意,本文仅仅是借用这个简单的例子来进行说明,学习重点还是在行为的实现思路上。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅行为包动画控制器实现更复杂的生物行为的**思路**;
|
||||
- ✅飞行生物的实现;
|
||||
- ✅切换组件组的几种方式;
|
||||
|
||||
请点击[这里](https://g79.gdl.netease.com/FlyingZombies.zip)下载本章节课程的教学包
|
||||
|
||||
## 成果展示
|
||||
|
||||
僵尸在行走一段距离之后会自动切换成飞行模式:
|
||||
|
||||

|
||||
|
||||
而在飞行一段时间之后又会自动切换成行走模式:
|
||||
|
||||

|
||||
|
||||
这样既能提高僵尸搜寻怪物的效率,又能提高玩家在生存时的游戏难度 :
|
||||
|
||||

|
||||
|
||||
## 基础行为编写实战
|
||||
|
||||
根据生物的状态,我们很自然的可以把生物组件组分成两个部分 `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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
进入游戏,生物拥有正常的行走行为:
|
||||
|
||||

|
||||
|
||||
我们要测试飞行也是一样的,只需要把默认加入的组件替换成 `fly` 就行了:
|
||||
|
||||
```json
|
||||
"events": {
|
||||
"minecraft:entity_spawned": {
|
||||
"sequence": [
|
||||
{
|
||||
// 55开的几率生成成年和幼年僵尸
|
||||
// ...省略...
|
||||
},
|
||||
{
|
||||
// 默认加入的行走行为组件组
|
||||
"add": {
|
||||
"component_groups": ["fly"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
进入游戏,观察实体,发现能够正常的进行飞行:
|
||||
|
||||

|
||||
|
||||
## 行为切换实战
|
||||
|
||||
这是我们这篇文章主要想讨论的东西,也是实现更复杂生物行为的基础。
|
||||
|
||||
下面我们将对如何进行行为切换进行一些讨论。
|
||||
|
||||
### 行为切换的基础:事件
|
||||
|
||||
在讨论之前,我们需要先把基础打好,也就是通过事件来添加或者删除对应的组件组:
|
||||
|
||||
```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"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
效果演示:
|
||||
|
||||

|
||||
|
||||
以上的触发器都是微软硬编码好的。还有一种类似的触发方式是基于组件的传感器:
|
||||
|
||||
| 传感器 | |
|
||||
| :----------------------------------------------------------- | :----------------------------------------------------------- |
|
||||
| [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"
|
||||
]
|
||||
}
|
||||
},
|
||||
// 省略其他无关内容....
|
||||
```
|
||||
|
||||
此时,我们就可以不加入任何组件的情况下,实现行走和飞行的互相转换:
|
||||
|
||||

|
||||
|
||||
#### 例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` 文件,修改原版僵尸的行为,使其具备基础的飞行和行走的转换能力,并尝试以下的方法:
|
||||
- 基于组件的方式;
|
||||
- 基于触发器的方式;
|
||||
- 基于传感器的方式;
|
||||
- 基于动画控制器的方式;
|
||||
652
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/2-特殊行为-拆墙.md
Normal file
652
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/2-特殊行为-拆墙.md
Normal 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`,这样,就可以在被墙体阻隔的情况下仍然被实体所选择:
|
||||
|
||||

|
||||
|
||||
下一个问题就到了**如何检测被墙体阻隔**了。
|
||||
|
||||
我们检查和查阅所有的组件也好,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),但是实际测试之后并不能使用,原因就是因为这个组件是模拟山羊用来吃掉脚下方块的组件,想要达到拆墙的效果并不可行,效果演示:
|
||||
|
||||

|
||||
|
||||
另外还发现了一个 [`break_blocks` 组件](https://learn.microsoft.com/zh-cn/minecraft/creator/reference/content/entityreference/examples/entitycomponents/minecraftcomponent_break_blocks?view=minecraft-bedrock-stable),这可能是一个符合要求的选择,但是如果只是单纯加入,就会表现得横冲直撞(即使在没有目标时):
|
||||
|
||||

|
||||
|
||||
我们想要他在发现目标之后再进行一个短暂的破坏(也就是说需要冷却),我们可以使用上节课说到的传感器和 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
看上去有点长,但实际上这个流程还是很清晰的:
|
||||
|
||||

|
||||
|
||||
我们加入游戏之后测试:
|
||||
|
||||

|
||||
|
||||
可以看到,虽然也能达到类似效果,但是这个 `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` 组件。
|
||||
|
||||
这样做之后的区别就是,僵尸不会再一股脑的破坏掉路径上的所有方块,而是会在检测走不动时,才会开始尝试破坏方块:
|
||||
|
||||

|
||||
|
||||
这样虽然也是模拟了检测的情况,也尽最大可能还原了拆墙的行为逻辑,但是也不是真正意义上的检测。
|
||||
|
||||
我们会发现原版的组件和 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 接口可以从指定位置发射一条射线,获取与射线相交的实体和方块信息。
|
||||
|
||||
我们可以以此为基础来创造一个使用**最短路径**破坏墙体来攻击目标的自定义僵尸,实现之后的演示如下:
|
||||
|
||||

|
||||
|
||||
完整代码示例如下:
|
||||
|
||||
```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))
|
||||
|
||||
```
|
||||
|
||||
一番改造之后,可以进入游戏查看效果:
|
||||
|
||||

|
||||
|
||||
### 动画支持
|
||||
|
||||
可以看到,虽然支持了破坏时间,但是并没有破坏的动画,显得非常呆板。我们尝试给正在破坏方块的实体加入一个破坏方块的动画。
|
||||
|
||||
如果是我们自定义的实体,可以很方便的使用自定义的 `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` 动画来代替(模拟有动画的情况),最终效果如下:
|
||||
|
||||

|
||||
|
||||
怎么实现的呢?首先我们需要把动画加入到实体之中去,为了不破坏原版文件,我们使用代码的方式无侵入式的加入玩家的有用动画:
|
||||
|
||||
```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` 文件,修改原版僵尸的行为,使其具备拆墙的行为(多种方式);
|
||||
- 打开客户端的调试功能,观察并测试;
|
||||
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/3-特殊行为-搭路.md
Normal file
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/3-特殊行为-搭路.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 特殊行为-搭路
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将修改**原版僵尸**的行为,实现一个可以在脚下搭路追击目标的功能。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅自定义生物行为的实现;
|
||||
- ✅搭路行为的实现;
|
||||
|
||||
请点击[这里](https://g79.gdl.netease.com/BuildRoads.zip)下载本章节课程的教学包
|
||||
|
||||
## 需要搭路的几种情况
|
||||
|
||||
原版僵尸的行为十分呆板,如果玩家躲藏到高空,那么原版的僵尸对于玩家来说几乎没有任何威胁。
|
||||
|
||||
所以为了增加游戏难度,我们来实现一下僵尸追击的功能。首先,第一个问题就是,如何检测什么时候应该执行搭路的行为呢?我们先来分析一波,理清思路。
|
||||
|
||||
### 存在高度差
|
||||
|
||||
最容易想到的一个情况就是实体跟目标之间存在高度差。因为在原版游戏中,如果我们与目标有一定的距离之后,目标会停留在我们的脚下驻足观望站在高处的我们:
|
||||
|
||||

|
||||
|
||||
在这种情况下,我们需要检测存在高度差,并且已经长时间驻足的情况。长时间驻足我们在上一节课中学习了,可以使用动画控制器 + `query.walk_distance` ,来完成。问题是**高度差如何检测**。
|
||||
|
||||
翻看原版组件之后发现并没有什么好的办法。所以不得不**使用自定义生物行为**了。
|
||||
|
||||
### 没有路
|
||||
|
||||
还有一种情况是,目标与实体之间不存在高度差,但就是没有路给实体过去:
|
||||
|
||||

|
||||
|
||||
这种情况如果不选择像我们之前课程介绍的那样切换飞行状态**飞过去**,那么也就只能搭路了。
|
||||
|
||||
不过也要考虑是不是被墙体阻隔,这是上节课的内容。我们这里只是提一下。
|
||||
|
||||
## 代码实现
|
||||
|
||||
经过上面的分析之后,写代码逻辑就很清晰了,完整代码如下:
|
||||
|
||||
```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` 给标准化。这是比较容易混淆和搞错的地方。
|
||||
|
||||
另外,上面的方法并没有检测实体长时间没有移动的情况,会导致实体在发现目标之后就直接开始检测是否需要搭方块,实际效果如下:
|
||||
|
||||

|
||||
|
||||
我们需要配合动画控制和上节课提到的 `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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
加入之后测试,行为就正常多了:
|
||||
|
||||

|
||||
|
||||
## 小结
|
||||
|
||||
原版组件的性能始终是要更好的,所以多数情况下,我们会在原版组件实在是无法满足的时候才会使用自定义行为(为了图方便也不是不可以)。
|
||||
|
||||
并且会尝试使用动画控制器 + Molang 的混合方式,熟悉之后也就是思路的问题了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 尝试在 `_b/entities` 新增一个 `zombie.json` 文件,修改原版僵尸的行为,使其具备搭方块的行为;
|
||||
- 打开客户端的调试功能,观察并测试;
|
||||
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/0_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_5.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/1_5.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_5.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_5.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_6.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_6.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_7.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/3_7.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/4_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/4_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/4_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/4_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031095005439.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031095005439.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031095254881.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031095254881.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031151519365.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031151519365.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031151749226.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231031151749226.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114038608.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114038608.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114246018.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114246018.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114447470.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114447470.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114549090.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103114549090.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103120559952.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103120559952.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103121138327.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231103121138327.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231112134925911.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231112134925911.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231112135359365.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231112135359365.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231116095940409.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231116095940409.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231116145013558.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231116145013558.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231118140323953.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231118140323953.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231118150125763.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/0-怪物AI制作/assets/image-20231118150125763.png
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user