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.
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/0-教程示例下载.md
Normal file
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/0-教程示例下载.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 教程示例下载
|
||||
|
||||
本章所教学的惊变系统Demo可点击 [这里](https://g79.gdl.netease.com/ForgeLabsSystem.zip) 下载到本地。
|
||||
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/1-时间规则.md
Normal file
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/1-时间规则.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 时间规则
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解 MC 中的时间的问题,并从零开始带你搭建起一个基础的 UI 来帮助我们了解当前 MC 中的时间情况。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅MC 中的时间规则;
|
||||
- ✅简单 UI 搭建指南;
|
||||
|
||||
## MC 中的时间规则
|
||||
|
||||
> 这一部分可以查看 MC 的[官方介绍](https://zh.minecraft.wiki/w/%E6%98%BC%E5%A4%9C%E6%9B%B4%E6%9B%BF)。
|
||||
|
||||
## 时间换算
|
||||
|
||||
在我的世界中的时间正好是现实时间中**流逝速度**的 72 倍。这是因为现实世界中的 1 天有 24 * 60 = 1440 分钟,而在我的世界中,1 个完整的 Minecraft 天只有 20 分钟。1440 / 20 = 72,正好是 72 倍。
|
||||
|
||||
如果要进行时间单位的换算的话,那么可以得到下面两个表。
|
||||
|
||||
一个表是 Minecraft → 现实时间的换算表:
|
||||
|
||||
| Minecraft时间 | Minecraft 刻 | 现实时间 |
|
||||
| :--------------: | :----------: | :--------------------: |
|
||||
| 1秒 | 0.27 | 0.0138秒 |
|
||||
| 1分钟 | 16.6 | 0.83秒 |
|
||||
| 1小时 | 1,000 | 50秒 |
|
||||
| 1天 | 24,000 | 20分钟 |
|
||||
| 1周(7天) | 168,000 | 2.3小时 |
|
||||
| 1个月(30天) | 720,000 | 10小时 |
|
||||
| 1年(365.25 天) | 8,766,000 | 121.75小时(5.072916天 |
|
||||
|
||||
另一个表是现实时间 → Minecraft 时间的换算表:
|
||||
|
||||
| 现实时间 | Minecraft时间 |
|
||||
| :---------------: | :-------------------------------------: |
|
||||
| 1⁄20秒(1游戏刻) | 3.6秒 |
|
||||
| 1秒 | 1分钟12秒(72秒) |
|
||||
| 10秒 | 12分钟(720秒) |
|
||||
| 50秒 | 1小时(60分,3600秒) |
|
||||
| 1分钟 | 1小时12分钟 |
|
||||
| 1小时 | 3天 |
|
||||
| 1天 | 2.4个月 = 72天 |
|
||||
| 1周 | 约1.385年 ≈ 17个月 = 72周 = 504天 |
|
||||
| 1个月 | 6年 = 72个月 ≈ 308.5周 = 2,160天 |
|
||||
| 1年 | 72年 ≈ 876.5个月 ≈ 3,757周 ≈ 26,297.5天 |
|
||||
|
||||
## 游戏刻
|
||||
|
||||
你可以把游戏想象成一个巨大的机器,它需要不断地运转才能工作。MC 就是这样一个机器。就像时钟里的每个部件都要跟着钟摆的节奏一起动一样,游戏里的每个事情都要跟着游戏的节奏一起发生。我们把游戏的节奏叫做**游戏循环**,它就像是游戏的心跳。每次心跳,游戏就会更新一下自己的状态,比如玩家的位置,方块的变化,怪物的行动等等。我们把每次心跳的时间叫做一**刻(tick)**,它是游戏的最小时间单位。
|
||||
|
||||
游戏的一刻是指 Minecraft 的游戏循环运行一次所占用的时间。正常情况下,游戏固定以**每秒钟 20 刻**的速率运行,因此一刻的时间为 0.05 秒(50 毫秒,或一秒钟的二十分之一),使得游戏内的[一天](https://zh.minecraft.wiki/w/%E6%98%BC%E5%A4%9C%E6%9B%B4%E6%9B%BF)刚好持续 **24000 刻**,也就是 20 分钟。
|
||||
|
||||

|
||||
|
||||
## 游戏中的时间
|
||||
|
||||
有了上面我们对 MC 时间的了解,加上时间刻与现实时间的换算关系,我们就知道了游戏中的一天是如何度过的了。
|
||||
|
||||
### 白天
|
||||
|
||||

|
||||
|
||||
白天是一天周期中最长的一节,历时 10 分钟。
|
||||
|
||||
开始:0 刻(早上06:00:00.0)
|
||||
|
||||
中午:6000 刻(下午12:00:00.0)
|
||||
|
||||
结束:12000 刻(下午06:00:00.0)
|
||||
|
||||
### 日落
|
||||
|
||||

|
||||
|
||||
日落是介于白天和夜晚之间的时间段,持续 1 分半钟。
|
||||
|
||||
开始:12000 刻(下午06:00:00.0)
|
||||
|
||||
中点:12400 刻(下午06:54:00.0)
|
||||
|
||||
结束:13800 刻(下午07:48:00.0)
|
||||
|
||||
### 夜晚
|
||||
|
||||

|
||||
|
||||
夜晚持续 7 分钟。
|
||||
|
||||
开始:13800 刻(下午07:48:00.0)
|
||||
|
||||
午夜:18000 刻(早上12:00:00.0)
|
||||
|
||||
结束:22200 刻(早上04:12:00.0)
|
||||
|
||||
> 晴朗的夜晚时,玩家可以在 12542 刻(下午06:32:31.2)到 23460 刻(早上05:27:36.0)时睡觉。在雨天,玩家可以在 12010 刻(下午06:00:36.0)到 23992 刻(早上05:59:31.2)时睡觉。
|
||||
|
||||
### 日出/黎明
|
||||
|
||||

|
||||
|
||||
日出是介于夜晚和白天之间的时间段,持续 1 分半钟。
|
||||
|
||||
开始:22200 刻(早上04:12:00.0)
|
||||
|
||||
中点:23100 刻(早上05:06:00.0)
|
||||
|
||||
结束:24000(0)刻(早上06:00:00.0)
|
||||
|
||||
## 月相
|
||||
|
||||
游戏中每过一天,时间计数便会增加 24000 刻。虽然每天的交替是一样的,但[月亮](https://zh.minecraft.wiki/w/%E6%9C%88%E4%BA%AE)会经历 8 种月相。虽然没有命令直接更改月相,但`/time add 24000`命令可以快进至下一个月相。进一步而言,使用以下命令可以直接指定不同的月相:
|
||||
|
||||
| 命令 | 月相 |
|
||||
| :----------------- | :----- |
|
||||
| `/time set night` | 满月 |
|
||||
| `/time set 38000` | 亏凸月 |
|
||||
| `/time set 62000` | 下弦月 |
|
||||
| `/time set 86000` | 残月 |
|
||||
| `/time set 110000` | 新月 |
|
||||
| `/time set 134000` | 娥眉月 |
|
||||
| `/time set 158000` | 上弦月 |
|
||||
| `/time set 182000` | 盈凸月 |
|
||||
|
||||
游戏中月亮的原版贴图,自行对应:
|
||||
|
||||

|
||||
|
||||
## 昼夜更替
|
||||
|
||||
如果开启了命令。我们可以使用 `/gamerule doDaylightCycle [*true/false*]` 来控制是否开启昼夜更替。
|
||||
|
||||
当我们关闭昼夜更替之后,游戏中的时间刻虽然会继续运行,但是数值上不会有所变动了,而是**固定在某一刻**。
|
||||
|
||||
## 时间相关的 API
|
||||
|
||||
我们可以在[官方的文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/Api%E7%B4%A2%E5%BC%95%E8%A1%A8.html?catalog=1#%E6%97%B6%E9%97%B4)中查看到最新的、与时间相关的 API:
|
||||
|
||||

|
||||
|
||||
看着这么多,如果不算设置昼夜更替的 API 的话,其实总体就分为了两类:
|
||||
|
||||
- 获取时间类;
|
||||
- 设置时间类;
|
||||
|
||||
不管是维度的局部时间,还是其他任何时间,都符合上面介绍的时间规则。
|
||||
|
||||
## 实操:左上角时钟 UI 显示
|
||||
|
||||
接下来我们将带大家实操制作一个 UI,能够实时显示当前维度的时间,效果如下:
|
||||
|
||||

|
||||
|
||||
### Step1. 新增界面文件
|
||||
|
||||
首先,打开我们的 MC Studio,在界面一览选择新建一个「界面文件」:
|
||||
|
||||

|
||||
|
||||
点击下一步之后命名为「timeDisplayUI」就可以了:
|
||||
|
||||

|
||||
|
||||
我们的需求很简单,只需要在界面的左上角,使用「文本」控件显示出当前维度的时间就可以了,所以整个界面也十分简单,一个「文本」控件,设置在左上方即可:
|
||||
|
||||

|
||||
|
||||
把文本大小选择为大,并且把层级设置在 20 层以上(保证在原版的 UI 上方,不会被遮挡),这样方便我们查看。
|
||||
|
||||
OK,界面文件就此告成。
|
||||
|
||||
### Step2. 注册 UI
|
||||
|
||||
注册和创建 UI 需要监听 `UiInitFinished` 之后执行:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
self.mUINode = None
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "UiInitFinished",
|
||||
self, self.OnUiInitFinished)
|
||||
|
||||
def UnListenEVent(self):
|
||||
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "UiInitFinished",
|
||||
self, self.OnUiInitFinished)
|
||||
|
||||
def Destroy(self):
|
||||
self.UnListenEVent()
|
||||
|
||||
def OnUiInitFinished(self, args=None):
|
||||
# 注册 UI
|
||||
uiClsPath = 'timeRuleScripts.uIScripts.UIScript'
|
||||
uiScreenDef = 'timeDisplayUI.main'
|
||||
clientApi.RegisterUI('timeRuleMod', 'timeDisplayUI', uiClsPath, uiScreenDef)
|
||||
# 创建 UI
|
||||
self.mUINode = clientApi.CreateUI('timeRuleMod', 'timeDisplayUI', {"isHud": 1})
|
||||
if self.mUINode:
|
||||
self.mUINode.Init() # 调用初始化函数
|
||||
```
|
||||
|
||||
### Step3. UI 代码
|
||||
|
||||
UI 代码也非常简单,也就两个功能:1)每秒更新 `label` 的文字;2)把游戏时间转换成与现实世界对应的时间。
|
||||
|
||||
完整代码如下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class UIScript(ScreenNode):
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
self.mPlayerId = clientApi.GetLocalPlayerId()
|
||||
|
||||
# 组件注册地址
|
||||
self.mLabelPath = '/label'
|
||||
|
||||
# 界面需要使用的自定义属性
|
||||
self.mTimeCounter = 0
|
||||
|
||||
def Create(self):
|
||||
print("===== UI Create =====")
|
||||
|
||||
# 1 秒 30 帧
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
perSec = self.mTimeCounter % 30 == 0
|
||||
if perSec:
|
||||
self.UpdateLabelContent()
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def Init(self):
|
||||
print '=== UI 初始化 ==='
|
||||
self.UpdateLabelContent()
|
||||
|
||||
def UpdateLabelContent(self):
|
||||
timeComp = CompFactory.CreateTime(clientApi.GetLevelId())
|
||||
pressedTime = timeComp.GetTime()
|
||||
timeStr = self.GameTime2RealTime(pressedTime)
|
||||
self.GetLabel(self.mLabelPath).SetText(timeStr)
|
||||
|
||||
def GameTime2RealTime(self, gameTick):
|
||||
# 定义游戏中一天的刻数
|
||||
gameDayTicks = 24000
|
||||
# 定义游戏中一小时的刻数
|
||||
gameHourTicks = gameDayTicks / 24
|
||||
# 定义游戏中一分钟的刻数
|
||||
gameMinuteTicks = gameHourTicks / 60
|
||||
# 计算游戏中的天数
|
||||
gameDay = gameTick // gameDayTicks + 1
|
||||
# 计算游戏中的小时数
|
||||
gameHour = (gameTick % gameDayTicks) // gameHourTicks
|
||||
# 计算游戏中的分钟数
|
||||
gameMinute = (gameTick % gameHourTicks) // gameMinuteTicks
|
||||
# 把游戏中的小时数转换成现实中的小时数,加上6小时的偏移量
|
||||
realHour = (gameHour + 6) % 24
|
||||
# 把现实中的小时数、分钟数转换成字符串,补齐两位
|
||||
realHourStr = str(realHour).zfill(2)
|
||||
realMinuteStr = str(gameMinute).zfill(2)
|
||||
# 返回转换后的格式
|
||||
return "第{}天第{}时第{}分".format(gameDay, realHourStr, realMinuteStr)
|
||||
|
||||
def GetLabel(self, path):
|
||||
control = self.GetBaseUIControl(path)
|
||||
if control:
|
||||
return control.asLabel()
|
||||
return None
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
### Step4. 测试并验证
|
||||
|
||||
我们可以尝试使用 `/time set xxx` 命令来设置当前的时间,来验证 UI 代码的正确性。比如 `/time set 0` 界面会正确显示上面规则介绍的 `06:00:00` 这个时间:
|
||||
|
||||

|
||||
|
||||
至此,UI 就完成了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 给模组左上角新增一个当前时间显示的 UI;
|
||||
- 熟悉并测试时间相关的 API;
|
||||
242
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/2-可成长怪物体系.md
Normal file
242
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/2-可成长怪物体系.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 可成长的怪物体系
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解怪物属性的 API,并结合之前学习的课程,带你搭建一个可跟随时间成长的超级僵尸。
|
||||
|
||||
## 成果演示
|
||||
|
||||
我们这节课将配合上一节课提到的时间规则,来实现一个可以跟随时间成长的超级僵尸,这个成长不仅仅是**属性上**的成长,还包括生物行为的丰富:
|
||||
|
||||
我们的需求很简单,先来简单梳理一下:
|
||||

|
||||
|
||||
- 最开始就是普通的僵尸;
|
||||
- 到游戏进行到第 10 天时,所有僵尸都拥有**破坏墙体**的能力;
|
||||
- 到游戏进行到 20 天以后,所有僵尸除了拥有破坏墙体的能力之外,还拥有**搭路**的能力;
|
||||
|
||||
## 实践
|
||||
|
||||
要满足需求,这就需要对应改造一下我们的系统和我们的自定义僵尸,我们一步一步来。
|
||||
|
||||
### Step1. 改造 zombie 行为文件
|
||||
|
||||
我们首先要定义三种强度的僵尸事件,用来支持僵尸行为的变更,不过在此之前我们还要考虑一件事。
|
||||
|
||||
我们之前搭路的行为是直接在 `query.has_target` 之后就开始生效了,现在我们需要把这个行为做一个开关。最容易想到的办法就是像之前课程播放破坏方块动画的那样,使用一个用于设置状态的组件,比如我们使用 `minecraft:mark_variant`:
|
||||
|
||||
```json
|
||||
// 省略其他无关内容
|
||||
"minecraft:entity": {
|
||||
"description": {
|
||||
"animations": {
|
||||
"put_block_sensor": "controller.animation.zombie.put_block"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
{
|
||||
// 只有在有目标的情况下,并且能够搭建方块时才执行控制器的逻辑
|
||||
"put_block_sensor": "query.has_target && query.mark_variant == 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"component_groups": {
|
||||
// 搭建方块的能力
|
||||
"can_not_put_block": {
|
||||
"minecraft:mark_variant": {
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"can_put_block": {
|
||||
"minecraft:mark_variant": {
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
这样我们就可以使用组件组来标识该实体是否拥有该能力了。
|
||||
|
||||
然后定义好触发我们三种强度僵尸的事件:
|
||||
|
||||
```json
|
||||
"events": {
|
||||
// 自定义的强度1:就是普通的僵尸
|
||||
"tutorial:zombie_level_1": {
|
||||
"add": {
|
||||
"component_groups": ["can_not_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["destroy_block", "destroy_block_attack", "can_put_block", "put_block"]
|
||||
}
|
||||
},
|
||||
// 自定义强度2:能够拆墙,但是不能够铺路的僵尸
|
||||
"tutorial:zombie_level_2": {
|
||||
"add": {
|
||||
"component_groups": ["destroy_block", "can_not_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["can_put_block", "put_block"]
|
||||
}
|
||||
},
|
||||
// 自定义强度3:既能够拆墙,又能够搭路的僵尸
|
||||
"tutorial:zombie_level_3": {
|
||||
"add": {
|
||||
"component_groups": ["destroy_block", "can_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["can_not_put_block"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
至此,基础的 `zombie.json` 就改造完成了。
|
||||
|
||||
### Step2. 代码检测新的一天
|
||||
|
||||
核心代码如下:
|
||||
|
||||
```python
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleServerSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
# 组件缓存,避免重复创建
|
||||
self.mTimeComp = CompFactory.CreateTime(serverApi.GetLevelId())
|
||||
#
|
||||
self.mTimeCounter = 0
|
||||
self.mLastDay = 0 # 上一次的天数,用来判断是否是新的一天
|
||||
self.mCurrentDay = 0 # 今天的天数
|
||||
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
self._TriggerZombieEventIfNewDay()
|
||||
|
||||
def _TriggerZombieEventIfNewDay(self):
|
||||
# 从游戏开始经过的总帧数
|
||||
passedTime = self.mTimeComp.GetTime()
|
||||
# 从游戏开始经过的游戏天数
|
||||
day = passedTime / 24000
|
||||
if day != self.mCurrentDay:
|
||||
self.mCurrentDay = day
|
||||
isNewDay = self.mLastDay != self.mCurrentDay
|
||||
self.mLastDay = self.mCurrentDay
|
||||
|
||||
if isNewDay:
|
||||
self._ResetAllZombieLevel()
|
||||
```
|
||||
|
||||
为了配合玩家可能使用的 `/time set` 指令,所以我们这里每一秒都检测一下是否是新的一天。如果满足条件,那么重置当前世界中所有的僵尸等级。
|
||||
|
||||
### Step3. 重置僵尸等级和属性函数
|
||||
|
||||
有了检测是否进入新一天的代码了,那么我们可以着手开始考虑僵尸的属性和能力了。
|
||||
|
||||
首先需要定义好等级对应的属性值,以及游戏天数和僵尸等级的对应关系:
|
||||
|
||||
```python
|
||||
# 僵尸的标识符
|
||||
ZombieIdentifier = 'minecraft:zombie'
|
||||
# 僵尸的等级与天数的对应关系
|
||||
ZombieLevelDay = {
|
||||
1 : 1,
|
||||
10: 2,
|
||||
20: 3
|
||||
}
|
||||
# 僵尸等级与僵尸属性的对应关系
|
||||
ZombieLevelAttr = {
|
||||
1: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 10,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.25,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 1
|
||||
},
|
||||
2: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 20,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.3,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 2
|
||||
},
|
||||
3: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 30,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.4,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
由于我们保存了当前的游戏天数,所以很容易的写出重置一个僵尸的函数:
|
||||
|
||||
```python
|
||||
def _SetEntityLevelAndAttrAccordCurrentDay(self, entityId):
|
||||
# 使用事件来触发对应僵尸的等级的行为
|
||||
zombieLevel = self._GetZombieLevelByDay(self.mCurrentDay)
|
||||
eventName = 'tutorial:zombie_level_' + str(zombieLevel)
|
||||
CompFactory.CreateEntityEvent(entityId).TriggerCustomEvent(entityId, eventName)
|
||||
# 根据僵尸等级来设置僵尸的属性值
|
||||
attrComp = CompFactory.CreateAttr(entityId)
|
||||
for _attrName, _attrValue in ZombieLevelAttr[zombieLevel].items():
|
||||
attrComp.SetAttrMaxValue(_attrName, _attrValue)
|
||||
attrComp.SetAttrValue(_attrName, _attrValue)
|
||||
|
||||
# 根据天数来获取当前僵尸等级
|
||||
def _GetZombieLevelByDay(self, day):
|
||||
for threshold, level in sorted(ZombieLevelDay.items(), reverse=True):
|
||||
if day >= threshold:
|
||||
return level
|
||||
return 1 # 默认等级为1
|
||||
```
|
||||
|
||||
重置所有僵尸,我们需要借助 [`serverApi.GetEngineActor()` API](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#getengineactor) 来完成:
|
||||
|
||||
```python
|
||||
# 重置所有僵尸的等级
|
||||
def _ResetAllZombieLevel(self):
|
||||
print '=====重置所有僵尸等级===='
|
||||
for entityId, entityDict in serverApi.GetEngineActor().items():
|
||||
for _dimensionId, _identifier in entityDict.items():
|
||||
if _identifier == ZombieIdentifier:
|
||||
self._SetEntityLevelAndAttrAccordCurrentDay(entityId)
|
||||
```
|
||||
|
||||
### Step4. 考虑新增的僵尸
|
||||
|
||||
新增的僵尸也需要立刻适配当前世界天数对应的等级,这需要我们关注[实体新增的事件](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E4%BA%8B%E4%BB%B6/%E4%B8%96%E7%95%8C.html#addentityserverevent),我们需要在代码中进行监听,当新增时自动触发对应的事件:
|
||||
|
||||
```python
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "AddEntityServerEvent", self,
|
||||
self.OnAddEntityServerEvent)
|
||||
|
||||
def UnListenEvent(self):
|
||||
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "AddEntityServerEvent", self,
|
||||
self.OnAddEntityServerEvent)
|
||||
|
||||
def Destroy(self):
|
||||
self.UnListenEvent()
|
||||
|
||||
def OnAddEntityServerEvent(self, args):
|
||||
entityId = args['id']
|
||||
engineTypeStr = args['engineTypeStr']
|
||||
if engineTypeStr == ZombieIdentifier:
|
||||
self._SetEntityLevelAndAttrAccordCurrentDay(entityId)
|
||||
```
|
||||
|
||||
### Step5. 测试并验证
|
||||
|
||||
首先,我们使用命令 `/time set 0`,可以看到僵尸对于在方块内的目标并没有任何办法,属性值和组件组也跟我们设定的一样:
|
||||
|
||||

|
||||
|
||||
然后,当使用命令 `/time set 240000` 把游戏进程过渡到第 10 天之后时,这时候僵尸属性值发生了变化,并且拥有了破坏墙体的能力:
|
||||
|
||||

|
||||
|
||||
当我们使用命令 `/time set 480000` 把游戏进程过渡到 20 天之后时,僵尸已经有能力击杀在高处的目标:
|
||||
|
||||

|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 改造原版的 `zombie.json` 并配合上节课的时间管理系统来进行管理生物行为和属性值,达到一个跟随时间不断变强的僵尸。
|
||||
248
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/3-怪物生成.md
Normal file
248
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/3-怪物生成.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 怪物生成
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解生物生成的规则。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅实体生成规则;
|
||||
- ✅如何使用代码生成自定义实体;
|
||||
|
||||
## 实体生成规则
|
||||
|
||||
在原版行为包下的 `spawn_rules` 目录下,定义了许多实体的生成规则,也就是如何生成实体在世界中。当你希望自定义实体自然生成时,你就需要使用生成规则。不同的组件允许可以定义实体不同的生成时间、位置和方式。
|
||||
|
||||
一般来说,我们都会选择与原版实体非常相似的方式来生成。比如,像牛一样生成在牛群中、像僵尸一样出生在晚上,或者像鱼一样只出生在水里。
|
||||
|
||||
### 基础结构
|
||||
|
||||
当你要使用原版的生成规则时,首先,需要在行为包的 `spawn_rules` 目录下新建一个 `identifier.json` (这里的 `identifier` 代指生物标识符)的新文件。该文件的内容应该如下所示:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"minecraft:spawn_rules": {
|
||||
"description": {
|
||||
"identifier": "tutiroal:identifier",
|
||||
"population_control": "animal"
|
||||
},
|
||||
"conditions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `minecraft:spawn_rules` 内部,我们需要考虑两件事:**人口控制**和**条件**。
|
||||
|
||||
`description` description 定义文件的基本属性。 `identifier` identifier 应该匹配我们实体的标识符。 `population_control` population_control 定义游戏如何知道要生成多少生物,并且稍微复杂一些。
|
||||
|
||||
### 人口控制
|
||||
|
||||
我的世界有不同的实体池。当此处定义的池被认为已满时,游戏将不再生成该池中的生物。 共有以下几种不同的选择(一般我们使用前面三种即可):
|
||||
|
||||
- **"animal"**:被动生物,如牛和猪
|
||||
- **"water_animal"**:热带鱼、海豚等水生生物
|
||||
- **"monster"**:敌对生物,如骷髅和僵尸
|
||||
- **"villager"**:村民专属。
|
||||
- **"ambient"**:蝙蝠专属。
|
||||
- **"cat"**:猫专属。
|
||||
- **"pillager"**:掠夺者专属。
|
||||
|
||||
### 条件
|
||||
|
||||
`conditions` 是一系列允许生物在世界中生成的可能条件。 每个条件分别尝试在世界中生成生物。 每个条件都由一组组件组成,这些组件定义何时生产或不生成生物。
|
||||
|
||||
比如,我们可以查看**原版僵尸**的生成条件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"minecraft:spawn_rules": {
|
||||
"description": {
|
||||
"identifier": "minecraft:zombie",
|
||||
"population_control": "monster"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"minecraft:spawns_on_surface": {},
|
||||
"minecraft:spawns_underground": {},
|
||||
"minecraft:brightness_filter": {
|
||||
"min": 0,
|
||||
"max": 7,
|
||||
"adjust_for_weather": true
|
||||
},
|
||||
"minecraft:difficulty_filter": {
|
||||
"min": "easy",
|
||||
"max": "hard"
|
||||
},
|
||||
"minecraft:weight": {
|
||||
"default": 100
|
||||
},
|
||||
"minecraft:herd": {
|
||||
"min_size": 2,
|
||||
"max_size": 4
|
||||
},
|
||||
"minecraft:permute_type": [
|
||||
{
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"weight": 5,
|
||||
"entity_type": "minecraft:zombie_villager"
|
||||
}
|
||||
],
|
||||
"minecraft:biome_filter": {
|
||||
"test": "has_biome_tag", "operator": "==", "value": "monster"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 组件名称 | 描述 |
|
||||
| :-------------------------------- | :----------------------------------------------------------- |
|
||||
| `minecraft:spawns_on_surface` | 生物在地表生成 |
|
||||
| `minecraft:spawns_on_underground` | 生物在地底生成 |
|
||||
| `minecraft:brightness_filter` | 仅以特定亮度生成实体。 接受三个选项,`min`、`max` 和 `adjust_for_weather`。 亮度级别范围从 0 到 15。 如果 `adjust_for_weather` 设置为 `true`,则将考虑由于下雨和雷暴导致的亮度降低。 |
|
||||
| `minecraft:difficulty_filter` | 难度选择器,`min`、`max` 定义了最低和最高难度。实体仅会在定义的难度内生成。 |
|
||||
| `minecraft:weight` | 实体生成时的权重。 数字越大,生物生成的频率越高。 |
|
||||
| `minecraft:herd` | 设置在同一生成规则上一起生成的实体数。也可以理解为群体生成。 |
|
||||
| `minecraft:permute_type` | 给生成的实体一定概率变异为其他实体。 |
|
||||
| `minecraft:biome_filter` | 群系选择器。仅会在通过条件的群系生成。 |
|
||||
|
||||
### 所有已知组件
|
||||
|
||||
下列是所有已知的用于控制实体生成的条件组件:
|
||||
|
||||
```text
|
||||
minecraft:weight
|
||||
minecraft:density_limit
|
||||
minecraft:spawns_on_block_filter
|
||||
minecraft:spawns_on_block_prevented_filter
|
||||
minecraft:spawns_above_block_filter
|
||||
minecraft:herd
|
||||
minecraft:permute_type
|
||||
minecraft:brightness_filter
|
||||
minecraft:height_filter
|
||||
minecraft:spawns_on_surface
|
||||
minecraft:spawns_underground
|
||||
minecraft:spawns_underwater
|
||||
minecraft:disallow_spawns_in_bubble
|
||||
minecraft:spawns_lava
|
||||
minecraft:biome_filter
|
||||
minecraft:difficulty_filter
|
||||
minecraft:distance_filter
|
||||
minecraft:is_experimental
|
||||
minecraft:world_age_filter
|
||||
minecraft:delay_filter
|
||||
minecraft:mob_event_filter
|
||||
minecraft:is_persistent
|
||||
minecraft:player_in_village_filter
|
||||
```
|
||||
|
||||
我们仅需要对上述组件有一个大概印象就可以,除了一些比较特殊的 `minecraft:is_persistent` 之类的,我们在原版中看不到例子之外,其他的都可以在原版行为包中搜索到相关的应用。我们只对下面两个比较特殊常用的来做一些说明。
|
||||
|
||||
#### spawns_above_block_filter
|
||||
|
||||
```JSON
|
||||
"minecraft:spawns_above_block_filter": {
|
||||
"blocks": "minecraft:stone",
|
||||
"distance": 10
|
||||
}
|
||||
```
|
||||
|
||||
这个组件会垂直检测设定距离内的方块,如果条件满足,则实体生成。生成在指定的方块上。
|
||||
|
||||
#### spawns_on_block_prevented_filter
|
||||
|
||||
```json
|
||||
"minecraft:spawns_on_block_prevented_filter": [
|
||||
"minecraft:nether_wart_block",
|
||||
"minecraft:shroomlight"
|
||||
]
|
||||
```
|
||||
|
||||
正好与上面一个组件相反,该组件的作用是让实体永远不会在列表内的方块上生成。
|
||||
|
||||
## 代码生成
|
||||
|
||||
虽然原版的生成规则已经能够满足大部分的需求,但有时我们也会想要自己生成一些怪物以应对特殊情况。比如,在惊变中,血月会导致怪物的生成速度和数量大幅增加:
|
||||
|
||||

|
||||
|
||||
这可能就需要我们代码来执行额外的生成逻辑了。
|
||||
|
||||
### 一切的基础
|
||||
|
||||
生成怪物,目前官方只有一个 [API](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#createengineentitybytypestr):`CreateEngineEntityByTypeStr`,用法也很简单:
|
||||
|
||||
```python
|
||||
import mod.server.extraServerApi as serverApi
|
||||
ServerSystem = serverApi.GetServerSystemCls()
|
||||
class MyServerSystem(ServerSystem):
|
||||
def createMob(self):
|
||||
# 在主世界(0,5,0)的位置创建一个朝向为(0, 0)的尸壳
|
||||
entityId = self.CreateEngineEntityByTypeStr('minecraft:husk', (0, 5, 0), (0, 0), 0)
|
||||
```
|
||||
|
||||
### 示例:在玩家周围生成指定实体
|
||||
|
||||
API 很简单,最重要的就是对于坐标的选择了。我们想要实体生成在目标周围,但也要考虑一些特殊情况,比如当玩家在水中、空中、或者洞穴中能否支持生成。如果考虑得更加复杂一点,当前的控件是否支持实体的碰撞箱、光照强度等.....
|
||||
|
||||
这里提供一个简化之后的示例代码:
|
||||
|
||||
```python
|
||||
def SpawnEntityAround(self, targetId, spawnEntityIdentifier, num=1, minRadius=6, maxRadius=12):
|
||||
"""
|
||||
生成实体在目标周围
|
||||
:param targetId:
|
||||
:param spawnEntityIdentifier:
|
||||
:param num:
|
||||
:param minRadius:
|
||||
:param maxRadius:
|
||||
:return:
|
||||
"""
|
||||
entityList = []
|
||||
|
||||
targetX, targetY, targetZ = CompFactory.CreatePos(targetId).GetFootPos()
|
||||
dimensionId = CompFactory.CreateDimension(targetId).GetEntityDimensionId()
|
||||
|
||||
for _num in xrange(num):
|
||||
_randomPos = (
|
||||
targetX + random.randint(minRadius, maxRadius) * math.sin(random.uniform(0, 2 * math.pi)),
|
||||
targetY,
|
||||
targetZ + random.randint(minRadius, maxRadius) * math.cos(random.uniform(0, 2 * math.pi))
|
||||
)
|
||||
# 上面的 _randomPos 只是平面上的一个坐标,我们为了合理生成在地面上,需要 y 在 [-20, 20] 之间有一个合适的坐标
|
||||
_firstAirPos = self._RetFirstAirBlockPos(_randomPos, dimensionId, -20, 20)
|
||||
if _firstAirPos:
|
||||
entityId = self.CreateEngineEntityByTypeStr(spawnEntityIdentifier, _firstAirPos, (0, random.randint(-180, 180)),
|
||||
dimensionId)
|
||||
entityList.append(entityId)
|
||||
return entityList
|
||||
|
||||
# 范围内的第一个空气方块
|
||||
def _RetFirstAirBlockPos(self, basePos, dimensionId, step, maxStep):
|
||||
blockInfoComp = CompFactory.CreateBlockInfo(serverApi.GetLevelId())
|
||||
while step < maxStep:
|
||||
finalPos = (basePos[0], basePos[1] + step, basePos[2])
|
||||
blockDict = blockInfoComp.GetBlockNew(finalPos, dimensionId)
|
||||
step += 1
|
||||
if blockDict and blockDict['name'] == 'minecraft:air':
|
||||
return finalPos
|
||||
return None
|
||||
```
|
||||
|
||||
整体逻辑也很简单,画个图来解释一下:
|
||||
|
||||

|
||||
|
||||
先在平面上选择一个随机坐标,然后再从 y 方向上下手,寻找第一个空气方块,这个就是适合实体(简化)的坐标。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 使用自定义的生成规则生成僵尸;
|
||||
- 使用自己的代码来自定义生成指定的生物;
|
||||
471
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/4-随机事件.md
Normal file
471
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/4-随机事件.md
Normal file
@@ -0,0 +1,471 @@
|
||||
# 随机事件
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你在时间管理系统存在的前提(之前的课程)下,来触发我们的自定义事件,一个是血月,一个是大雾天。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅血月的实现原理;
|
||||
- ✅雾天的实现原理;
|
||||
- ✅原版粒子的进阶使用知识;
|
||||
|
||||
## 随机事件的规则
|
||||
|
||||
我们这一次课程要给我们的世界加入两种随机事件,一个是在晚上触发的血月:
|
||||
|
||||

|
||||
|
||||
另一个就是在白天触发的大雾天气:
|
||||
|
||||

|
||||
|
||||
并且他们会跟随游戏天数的增加而增加出现的概率。
|
||||
|
||||
### 触发逻辑
|
||||
|
||||
我们先来搭建最基础的部分,就是先触发这两种事件,然后再去实现具体的效果。
|
||||
|
||||
这两个事件我们可以换个角度来理解,都是需要在白天切换黑夜,黑夜切换白天的节点来判断触发的:
|
||||
|
||||
```python
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
self._TriggerRandomEventIfNewDayOrNight()
|
||||
|
||||
# 在新的白天或者夜晚触发随机事件
|
||||
def _TriggerRandomEventIfNewDayOrNight(self):
|
||||
# 从游戏开始经过的总帧数
|
||||
passedTime = self.mTimeComp.GetTime()
|
||||
previousTick = self.mCurrentTime
|
||||
self.mCurrentTime = passedTime
|
||||
|
||||
# 计算当前时间位置
|
||||
currentTime = self.mCurrentTime % self.mTicksPerDay
|
||||
previousTime = previousTick % self.mTicksPerDay
|
||||
|
||||
# 检查是否发生了昼夜交替
|
||||
if currentTime < (self.mTicksPerDay / 2) <= previousTime:
|
||||
self._HandleNightToDayTransition()
|
||||
elif currentTime >= (self.mTicksPerDay / 2) > previousTime:
|
||||
self._HandleDayToNightTransition()
|
||||
|
||||
# 更新白天/夜晚状态
|
||||
self.mCurrentTimeIsDayTime = currentTime < (self.mTicksPerDay / 2)
|
||||
```
|
||||
|
||||
## 血月的实现
|
||||
|
||||
首先,我们需要在「设置」→ 「视频」里面把「美丽的天空」选项打开:
|
||||
|
||||

|
||||
|
||||
这样才能在晚上看见月亮:
|
||||
|
||||

|
||||
|
||||
进一步的,我们想要把这个月亮给变成红色的呢,有以下几种思路。
|
||||
|
||||
### 修改原版资源
|
||||
|
||||
原版所有的资源文件,几乎都支持更改,包括不局限于:原版生物行为、动画、控制器、贴图文件等。月亮当然也不例外。
|
||||
|
||||
原版资源下的月亮位于目录:`\data\resource_packs\vanilla\textures\environment\moon_phases.png`,截图如下:
|
||||
|
||||

|
||||
|
||||
我们想要修改资源,直接复制一份原版的,然后修改保存到**现项目**资源文件下的 `textures/environment/moon_phases.png` 就行了。
|
||||
|
||||
不过直接修改有一个问题,假如我们把原版资源修改成了这样(别在意颜色...):
|
||||
|
||||

|
||||
|
||||
你会发现即使不是血月(特定时间)的晚上,也会是血月:
|
||||
|
||||

|
||||
|
||||
因为这个贴图不支持动态更改,意味着改了的话,在加载模组的情况下,就没有办法再进行动态的变更和改动了。
|
||||
|
||||
没办法,我们又只能曲线救国了。既然月亮的贴图是能变的,我们之前也介绍过**月相**的这个概念,我们的思路就有了。
|
||||
|
||||
我们只让满月的贴图变成血月:
|
||||
|
||||

|
||||
|
||||
其他的月相不变,然后让时间在某一个周期内循环,比如 24000 刻到 48000 刻(也就是第二天),只有血月的时候才在第一个周期内运行。这样就达到要求了。
|
||||
|
||||
但为此,我们需要对应改造我们的时间管理系统,也需要对天数进行对应的存档(因为存档在某一天内一直循环)。存档的天数也需要同步到客户端进行显示。
|
||||
|
||||
还可能会影响原版掠夺者的生成(原版掠夺者会在 5.5 个游戏日后距离任一玩家 24 到 48 格生成)。
|
||||
|
||||
所以我们这里只提一下思路。具体的实现,大家感兴趣的话,可以在自己的模组中,自行尝试一下。
|
||||
|
||||
### 使用特效遮住月亮
|
||||
|
||||
我们来介绍一个比较特别的方法,那就是用特效来遮住月亮:
|
||||
|
||||

|
||||
|
||||
这有助于提高我们对原版粒子的理解和认识。
|
||||
|
||||
能这么做有几个重要的前提:
|
||||
|
||||
- √ 玩家和月亮贴图之间存在相当的距离;
|
||||
- √ 我们可以获取月亮的角度;
|
||||
- √ 特效可以绑定在实体身上,跟随实体实时运动;
|
||||
- √ 我们可以通过自定义的变量来实时改变和控制特效的位置来遮挡住原版的月亮;
|
||||
|
||||
#### Step 1. 制作一个血月的贴图
|
||||
|
||||
首先,我们用软件制作一个血月的贴图:
|
||||
|
||||

|
||||
|
||||
并放置在资源文件下(`资源包/textures/particle/blood_moon.png`)备用。
|
||||
|
||||
#### Step 2. 在玩家身上绑定一个粒子动画
|
||||
|
||||
我们先来创建一个绑定在玩家身上播放的动画:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.blood_moon.play": {
|
||||
"loop": true,
|
||||
// 设置一个足够短的时间,让它以非常快的速度刷新用来遮挡原本的月亮
|
||||
"animation_length": 0.01,
|
||||
"particle_effects": {
|
||||
"0.0": [
|
||||
{
|
||||
"effect": "blood_moon",
|
||||
// 我们这里绑定一个未知的 locator 这会默认绑定在玩家的 body 骨骼组上
|
||||
"locator": "unknown"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个动画文件设置成循环播放,这样我们可以持续播放这个带有粒子的动画;
|
||||
|
||||
我们同时也需要使用 API 在客户端在玩家的渲染器中增加上述的动画:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# ...省略了其他无关内容...
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
|
||||
self, self.OnAddPlayerCreatedClientEvent)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
if playerId == clientApi.GetLocalPlayerId():
|
||||
self.InitRender()
|
||||
|
||||
# 初始化绑定,给玩家绑定上血月的相关 query.mod.xx 变量和相关动画
|
||||
def InitRender(self):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
queryVariableComp.Register("query.mod.moon_rot", 0)
|
||||
queryVariableComp.Register("query.mod.play_blood_moon", 0)
|
||||
# 测试,默认播放血月效果
|
||||
queryVariableComp.Set("query.mod.play_blood_moon", 1)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(clientApi.GetLocalPlayerId())
|
||||
actorRenderComp.AddPlayerParticleEffect("blood_moon", "tutorial:blood_moon")
|
||||
actorRenderComp.AddPlayerAnimation("play_blood_moon", "animation.blood_moon.play")
|
||||
actorRenderComp.AddPlayerScriptAnimate("play_blood_moon", "query.mod.play_blood_moon")
|
||||
res = actorRenderComp.RebuildPlayerRender()
|
||||
|
||||
```
|
||||
|
||||
#### Step 3. 特效文件
|
||||
|
||||
通过查阅官方 API,我们可以通过 `GetMoonRot` 获取到月亮的角度,那么就简单了。
|
||||
|
||||
我们通过观察可以发现,昼夜交替中的太阳月亮是只会在 x 轴上移动的,它们的移动轨迹近似于一个圆形:
|
||||
|
||||

|
||||
|
||||
那么已知了月亮的角度,那么只需要知道半径就可以计算出月亮当前的位置了:
|
||||
|
||||

|
||||
|
||||
假如月亮半径是 `variable.distance` 的话,那么很容易得出某一个具体的时间节点,月亮粒子的 x、y、z 位置应该如下:
|
||||
|
||||
```json
|
||||
"minecraft:emitter_shape_point": {
|
||||
"offset": ["math.sin(query.mod.moon_rot) * variable.distance", "math.cos(query.mod.moon_rot) * variable.distance", 0]
|
||||
}
|
||||
```
|
||||
|
||||
- x:`math.sin(query.mod.moon_rot) * variable.distance`,就是 `sin` 函数乘以半径;
|
||||
- y:`math.cos(query.mod.moon_rot) * variable.distance`,就是 `cos` 函数乘以半径;
|
||||
- z:由于没有偏移,所以始终是 0
|
||||
|
||||
所以完整的粒子文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"particle_effect": {
|
||||
"description": {
|
||||
"identifier": "tutorial:blood_moon",
|
||||
"basic_render_parameters": {
|
||||
"material": "particles_blend",
|
||||
"texture": "textures/particle/blood_moon"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:emitter_local_space": {
|
||||
"position": true,
|
||||
"rotation": false
|
||||
},
|
||||
"minecraft:emitter_initialization": {
|
||||
"creation_expression": "variable.distance = 140;"
|
||||
},
|
||||
"minecraft:emitter_rate_steady": {
|
||||
// 用一个足够快的速度来产生粒子
|
||||
"spawn_rate": 200,
|
||||
// 限制最大的粒子数,避免造成卡顿
|
||||
"max_particles": 1
|
||||
},
|
||||
"minecraft:particle_lifetime_expression": {
|
||||
"max_lifetime": 0.01
|
||||
},
|
||||
"minecraft:emitter_lifetime_once": {
|
||||
"active_time": 0.01
|
||||
},
|
||||
"minecraft:emitter_shape_point": {
|
||||
"offset": ["math.sin(query.mod.moon_rot) * variable.distance", "math.cos(query.mod.moon_rot) * variable.distance", 0]
|
||||
},
|
||||
"minecraft:particle_appearance_billboard": {
|
||||
"size": [14, 14],
|
||||
"facing_camera_mode": "lookat_xyz",
|
||||
"uv": {
|
||||
"texture_width": 128,
|
||||
"texture_height": 128,
|
||||
"uv": [0, 0],
|
||||
"uv_size": [128, 128]
|
||||
}
|
||||
},
|
||||
"minecraft:particle_appearance_tinting": {
|
||||
"color": [1, 0, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里有一个**需要注意**的点,就是 `minecraft:emitter_local_space` 组件。它定义了是否使用实体的局部空间变量,`position` 和 `ratation` 分别是对应了「是否使用实体**局部坐标**」和「是否使用实体**局部旋转**」。
|
||||
|
||||
由于我们需要以玩家为中心来播放粒子,所以 `position` 为 `true`,而局部旋转则不需要,如果我们这里的 `rotation` 为 `true` 的话,就会出现粒子跟随玩家身体朝向来转动的效果:
|
||||
|
||||

|
||||
|
||||
#### Step 4. 把月亮角度传入 `query.mod.moon_rot`
|
||||
|
||||
为了缓解客户端的压力,我们每 3 帧更新一次 `query.mod.moon_rot` 的值:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了其他无关的内容...
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.mTimeCounter = 0
|
||||
#
|
||||
self.mSkyRenderComp = CompFactory.CreateSkyRender(clientApi.GetLevelId())
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
if self.mTimeCounter % 3 == 0:
|
||||
moonRot = self.mSkyRenderComp.GetMoonRot()[2]
|
||||
self.mQueryVariableComp.Set('query.mod.moon_rot', moonRot)
|
||||
```
|
||||
|
||||
#### Step 5. 测试效果
|
||||
|
||||
一切准备好之后,我们重新打开客户端进入游戏,输入指令 `/time set 11990` (11990 是一个足够接近月亮升起的时间,可以观察月亮升起的全过程)。
|
||||
|
||||
游戏效果如下:
|
||||
|
||||

|
||||
|
||||
## 雾天的实现
|
||||
|
||||
雾天的实现比较简单,基本上就依靠 `SetFogLength()` API 就可以了。比如在调试工具中运行下列代码:
|
||||
```python
|
||||
import mod.client.extraClientApi as clientApi
|
||||
levelId = clientApi.GetLevelId()
|
||||
comp = clientApi.GetEngineCompFactory().CreateFog(levelId)
|
||||
comp.SetFogLength(0.1, 32)
|
||||
```
|
||||
|
||||
就可以实现比较好的雾天的效果:
|
||||
|
||||

|
||||
|
||||
## 服务端客户端改造
|
||||
|
||||
至此,我所有客户端的效果都改造完成了,我们只需要有对应的事件来触发客户端的渲染就可以了。
|
||||
|
||||
客户端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了无关内容....
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
self.mFogComp = CompFactory.CreateFog(clientApi.GetLevelId())
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent("timeRuleMod", "timeRuleServerSystem", "BloodMoonEvent", self, self.OnBloodMoonEvent)
|
||||
self.ListenForEvent("timeRuleMod", "timeRuleServerSystem", "FoggyWeatherEvent", self, self.OnFoggyWeatherEvent)
|
||||
|
||||
def OnBloodMoonEvent(self, args):
|
||||
flag = args['flag']
|
||||
self.mQueryVariableComp.Set('query.mod.play_blood_moon', 1 if flag else 0)
|
||||
|
||||
def OnFoggyWeatherEvent(self, args):
|
||||
flag = args['flag']
|
||||
self._FoggyWeather(flag)
|
||||
|
||||
def _FoggyWeather(self, isOpen):
|
||||
if isOpen:
|
||||
self.mFogComp.SetFogLength(0.1, 32)
|
||||
else:
|
||||
self.mFogComp.ResetFogLength()
|
||||
|
||||
```
|
||||
|
||||
服务端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了无关内容...
|
||||
import mod.server.extraServerApi as serverApi
|
||||
import random
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
|
||||
class TimeRuleServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleServerSystem, self).__init__(namespace, name)
|
||||
#
|
||||
self.mIsBloodMoon = False # 是否开启了血月
|
||||
self.mIsFoggyWeather = False # 是否开启了大雾天气
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def _HandleDayToNightTransition(self):
|
||||
self._CloseDayEvent()
|
||||
self._TriggerNightEvent()
|
||||
|
||||
def _CloseDayEvent(self):
|
||||
if self.mIsFoggyWeather:
|
||||
# 让客户端关闭大雾天气渲染
|
||||
self.BroadcastToAllClient("FoggyWeatherEvent", {'flag': False})
|
||||
self.mIsFoggyWeather = False
|
||||
|
||||
def _TriggerNightEvent(self):
|
||||
if random.random() < self._GetEventProbability():
|
||||
# 通知全部玩家
|
||||
CompFactory.CreateCommand(serverApi.GetLevelId()).SetCommand("/title @a title §c血月降临!!")
|
||||
self.mIsBloodMoon = True
|
||||
# 让客户端渲染血月
|
||||
self.BroadcastToAllClient("BloodMoonEvent", {'flag': True})
|
||||
|
||||
def _HandleNightToDayTransition(self):
|
||||
self._CloseNightEvent()
|
||||
self._TriggerDayEvent()
|
||||
|
||||
def _CloseNightEvent(self):
|
||||
if self.mIsBloodMoon:
|
||||
# 让客户端关闭血月渲染
|
||||
self.BroadcastToAllClient("BloodMoonEvent", {'flag': False})
|
||||
self.mIsBloodMoon = False
|
||||
|
||||
def _TriggerDayEvent(self):
|
||||
if random.random() < self._GetEventProbability():
|
||||
CompFactory.CreateCommand(serverApi.GetLevelId()).SetCommand("/title @a title 大雾天气!!")
|
||||
# 通知客户端渲染大雾天气
|
||||
self.BroadcastToAllClient("FoggyWeatherEvent", {'flag': True})
|
||||
self.mIsFoggyWeather = True
|
||||
|
||||
# 触发事件的概率。随天数的增加,概率逐渐增高,最多为 0.9,最少为 0.1
|
||||
def _GetEventProbability(self):
|
||||
return min(0.9, (0.1 + (self.mCurrentDay / 100.0)))
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
对于血月更多实体的生成,我们复用上一节课的代码加上一点判断就行了:
|
||||
|
||||
```python
|
||||
# 省略掉其他无关内容
|
||||
# 僵尸的数量限制
|
||||
ZombieLimit = 100
|
||||
|
||||
class TimeRuleServerSystem(serverApi.GetServerSystemCls()):
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
# 血月的时候会检测生成更多的实体
|
||||
if not self.mCurrentTimeIsDayTime and self.mIsBloodMoon:
|
||||
self.SpawnZombieAroundPlayer()
|
||||
|
||||
def SpawnZombieAroundPlayer(self):
|
||||
# 需要先检查当前世界上的僵尸数量
|
||||
if self.CurrentZombieNum() <= ZombieLimit:
|
||||
randomPlayerId = random.choice(serverApi.GetPlayerList())
|
||||
self.SpawnEntityAround(randomPlayerId, ZombieIdentifier, 10) # 一次性生成 10 个,吓死玩家
|
||||
|
||||
def CurrentZombieNum(self):
|
||||
count = 0
|
||||
for entityId, entityDict in serverApi.GetEngineActor().items():
|
||||
for _dimensionId, _identifier in entityDict.items():
|
||||
if _identifier == ZombieIdentifier:
|
||||
count += 1
|
||||
return count
|
||||
```
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 在本地还原血月和大雾天气的效果;
|
||||
- 结合时间管理系统,在昼夜更替的时候概率触发;
|
||||
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/1_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/1_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/A_sunset.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/A_sunset.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Day_Night_Clock_24h.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Day_Night_Clock_24h.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Sunrise.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Sunrise.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Survival.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Survival.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/TundraNight.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/TundraNight.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/blood_moon.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/blood_moon.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118203935199.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118203935199.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118211151267.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118211151267.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223613068.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223613068.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223659677.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223659677.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223906511.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223906511.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231119105709932.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231119105709932.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231120155538911.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231120155538911.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151314962.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151314962.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151421421.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151421421.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151719712.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151719712.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190714556.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190714556.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190808650.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190808650.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121191003030.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121191003030.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121193936537.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121193936537.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121194046552.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121194046552.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121205156004.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121205156004.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122120529133.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122120529133.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122162736911.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122162736911.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122163632497.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122163632497.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122174043335.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122174043335.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122192922106.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122192922106.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122194716435.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122194716435.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image_1.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image_1.png
LFS
Normal file
Binary file not shown.
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/0-教程示例下载.md
Normal file
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/0-教程示例下载.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 教程示例下载
|
||||
|
||||
本章所教学的武器和装备制作Demo可点击 [这里](https://g79.gdl.netease.com/tutorial_1.zip) 下载到本地。
|
||||
702
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/1-制作3D武器.md
Normal file
702
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/1-制作3D武器.md
Normal file
@@ -0,0 +1,702 @@
|
||||
# 制作3D武器
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你从零开始创建一把属于自己的 3D 武器,包含第一、第三人称动画。
|
||||
|
||||
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅3D 武器的两种实现方式和原理;
|
||||
- ✅3D 武器第一人称和第三人称动画的制作;
|
||||
|
||||
## 成果展示
|
||||
|
||||
通过本节课的学习,我们会逐步实现下列的一把自定义武器,拥有第一人称和第三人称的完整动画:
|
||||
|
||||

|
||||
|
||||
## 3D 武器的两种实现方式
|
||||
|
||||
我们先在 MC Studio 官方的内容库中“偷"一个 3D 武器的模型来使用,我这里选择的是下图所示的包:
|
||||
|
||||

|
||||
|
||||
我们随便选择其中一个模型吧,比如下面这个:
|
||||
|
||||

|
||||
|
||||
### 方法1:作为玩家的额外骨骼
|
||||
|
||||
第一种方法就是作为玩家的**额外骨骼**,挂接到玩家的渲染控制器上去。这种方法非常适合用于**仅涉及单一类型的实体**(比如玩家)模型,并且仅涉及一个装备位置的情况。这种方法也非常适合和方便在 Blockbench 中查看效果。
|
||||
|
||||
#### Step 1.对应玩家骨骼建模并对齐
|
||||
|
||||
首先我们需要对武器进行建模操作,当然我们这里直接使用了内容库中的内容,就不展开了。
|
||||
|
||||
然后需要**对应玩家的骨骼**进行**对齐**操作,如果我们不对默认的玩家骨骼进行修改,那么原版的玩家骨骼位置位于本地客户端的如下目录:`\data\skin_packs\vanilla\geometry.json`。我们如果使用 Blockbench 打开会发现该文件下有三个模型:
|
||||
|
||||

|
||||
|
||||
我们打开任一玩家模型就可以了,一般来说我们选择**纤细模型**,因为手臂模型更细,更需要对齐处理避免穿模。
|
||||
|
||||
接下来,我们只需要把我们的武器模型放在 `rightItem` 骨骼组下并对齐,这样就能完美的继承玩家的骨骼:
|
||||
|
||||

|
||||
|
||||
> 注意:我们操作的时候最好把原版骨骼复制一份,再在复制文件上进行操作。这样能避免误操作,对原版文件造成不必要的影响。
|
||||
|
||||
为了方便我们后续制作动画,我们需要把这个对齐好的模型先保存在一个临时目录下。
|
||||
|
||||
导出骨骼文件只需要把武器无关的骨骼全部删除掉就行了(删掉人物骨骼):
|
||||
|
||||

|
||||
|
||||
#### Step 2. 创建动画
|
||||
|
||||
如果我们需要自定义的攻击动作,那么我们就需要创建两个动画:一个是用于第三人称播放的人物动画,一个是用于第一人称播放的武器动画。
|
||||
|
||||
##### 第三人称动画
|
||||
|
||||
第三人称的动画很简单,就利用上面对齐好骨骼并且带有完整人物模型的直接 k 就行了,这里我们简单 k 一个只带手臂动作的简单动画:
|
||||
|
||||

|
||||
|
||||
这个动画只对 `rightArm` 和武器的根骨骼组 `sword` 进行了处理。
|
||||
|
||||
可以看到,由于我们这里有完整的骨骼和骨骼组,所以在 k 第三人称动画的时候非常方便。可以直接看到完整的效果。
|
||||
|
||||
##### 第一人称动画
|
||||
|
||||
但是第一人称就有点麻烦了。首先,我们隐藏掉所有的玩家骨骼(因为第一人称这些骨骼都是隐藏掉的)。
|
||||
|
||||
然后需要在 BlockBench **模拟游戏中第一人称的视角**。
|
||||
|
||||
第一步,在场景中右键,选择「保存相机角度」的选项:
|
||||
|
||||

|
||||
|
||||
填入下图中的数据保存:
|
||||
|
||||

|
||||
|
||||
然后我们再「角度」中选择刚才保存好的相机视角:
|
||||
|
||||

|
||||
|
||||
这是在模拟第一人称下的相机选项,但你会发现什么东西都看不见了,因为原版的游戏在第一人称时,手臂还会附加一个特殊的动画。
|
||||
|
||||
所以第二步,我们添加一个自定义的动画来模拟第一人称下手臂的位置:
|
||||
|
||||

|
||||
|
||||
该动画文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.first_person_guide.right_arm.method_one": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"rightArm": {
|
||||
"rotation": [95, -45, 115],
|
||||
"position": [13.5, -10, 12]
|
||||
},
|
||||
"rightItem": {
|
||||
"position": [0, 0, -1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
至此,我们就模拟好了第一人称下的武器位置。我们需要创建两个动画,第一个用于修正第一人称下的握持位置,第二个则是模拟第一人称下的攻击路径。
|
||||
|
||||
修正动画效果:
|
||||
|
||||

|
||||
|
||||
> 提醒:只需要点击动画右边的圆圈就可以同时播放多个动画了哦。
|
||||
|
||||
我们在 k 攻击动画的时候,需要同时把上述的「第一人称手臂模拟动画」和「第一人称握持修正动画」打开,再开始 k,这是麻烦的地方:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
至此,我们的动画就制作完成了。导出我们的动画到资源包 `animations` 目录下即可。
|
||||
|
||||
#### Step 3. 准备动画控制器和渲染控制器
|
||||
|
||||
我们的武器相当于是一个额外的骨骼,所以需要一个额外的渲染控制器来控制是否进行渲染:
|
||||
|
||||
```python
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.tutorial_custom_sword": {
|
||||
"geometry": "Geometry.custom_sword",
|
||||
"materials": [{"*": "Material.default"}],
|
||||
"textures": ["Texture.custom_sword"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
动画控制器也非常简单,不同视角的动画控制器播放不同的动画就可以了。
|
||||
|
||||
第三人称动画控制器:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.custom_sword.third_attack": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"third_person_attack1": "query.mod.custom_sword_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"third_person_attack1": {
|
||||
"animations": [
|
||||
// 对应玩家动画
|
||||
"third_person_attack1"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
第一人称动画控制器:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.custom_sword_by_addon_bones": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"first_person_attack1": "query.mod.custom_sword_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"first_person_attack1": {
|
||||
"animations": [
|
||||
// 对应代码里面配置的动画名称
|
||||
"custom_sword_first_attack"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4. 使用代码注册所有的资源
|
||||
|
||||
为了更少的侵入性,所以我们这里使用代码来把这些资源都挂接在玩家的渲染控制器下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import time
|
||||
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
|
||||
self, self.OnAddPlayerCreatedClientEvent)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
self.InitRender(playerId) # 包括其他玩家也需要被初始化
|
||||
|
||||
# 初始化绑定
|
||||
def InitRender(self, playerId):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
|
||||
|
||||
# 定义攻击的自定义变量,这里就是 query.mod.custom_sword_attack
|
||||
queryVariableComp.Register(config.AttackVarName, 0)
|
||||
# 第一种方法:作为玩家的附加骨骼添加进渲染控制器中进行控制
|
||||
self._InitMethodOne(playerId)
|
||||
|
||||
# 两种方式都通用的第三人称动画和控制器
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
actorRenderComp.AddPlayerAnimation("third_person_attack1", "animation.tutorial_custom_sword_by_attachable.player.attack1")
|
||||
actorRenderComp.AddPlayerAnimationController("custom_sword_third_attack", "controller.animation.custom_sword.third_attack")
|
||||
actorRenderComp.AddPlayerScriptAnimate("custom_sword_third_attack", "!variable.is_first_person")
|
||||
actorRenderComp.RebuildPlayerRender()
|
||||
|
||||
# 为第一个方法进行初始化渲染
|
||||
def _InitMethodOne(self, playerId):
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
# 3D武器:额外骨骼 —— 所需的就是把骨骼作为玩家的一部分添加上
|
||||
actorRenderComp.AddPlayerGeometry("custom_sword", "geometry.tutorial_custom_sword_by_addon_bones")
|
||||
actorRenderComp.AddPlayerTexture("custom_sword", "textures/models/tutorial_custom_sword")
|
||||
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_sword",
|
||||
"query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'")
|
||||
# 作为额外骨骼,还需要添加第一人称的动画文件和相关的控制器
|
||||
actorRenderComp.AddPlayerAnimation("custom_sword_first_hold", "animation.tutorial_custom_sword_by_addon_bones.hold_first_person")
|
||||
actorRenderComp.AddPlayerAnimation("custom_sword_first_attack", "animation.tutorial_custom_sword_by_addon_bones.first_attack1")
|
||||
actorRenderComp.AddPlayerAnimationController("custom_sword_first_attack_controller",
|
||||
"controller.animation.custom_sword_by_addon_bones")
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
"custom_sword_first_hold",
|
||||
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'"
|
||||
)
|
||||
actorRenderComp.AddPlayerScriptAnimate("custom_sword_first_attack_controller", "variable.is_first_person")
|
||||
|
||||
```
|
||||
|
||||
这里做几点说明:
|
||||
|
||||
- `query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'` 这里对应的条件是当玩家主手有对应物品时生效,并且后面的标识符是不带前缀的,至少我自己测试时如果写了的是 `tutorial:custom_sword_by_addon_bones` 会失效;
|
||||
- 这里判断第一人称要使用 `variable.is_first_person`。
|
||||
|
||||
#### Step 5. 处理攻击事件
|
||||
|
||||
首先客户端需要检测左键按下的事件,然后还需要把本地玩家的攻击状态同步给其他客户端:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import time
|
||||
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
#
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
self.mItemComp = CompFactory.CreateItem(clientApi.GetLocalPlayerId())
|
||||
#
|
||||
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
|
||||
self.mAttackStep = 0 # 攻击的阶段
|
||||
self.mLastAttackTime = 0 # 上一次攻击的时间
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncAttackStateEvent', self, self.OnSyncAttackStateEvent)
|
||||
# 处理攻击相关的事件,监听左键按下事件
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "TapBeforeClientEvent",
|
||||
self, self.OnLeftClick)
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "LeftClickBeforeClientEvent",
|
||||
self, self.OnLeftClick)
|
||||
|
||||
# 接受到其他客户端传来的同步事件
|
||||
def OnSyncAttackStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
value = float(args['value'])
|
||||
CompFactory.CreateQueryVariable(playerId).Set(config.AttackVarName, value)
|
||||
|
||||
def OnLeftClick(self, args=None):
|
||||
# 手持自定义武器并且上一次的攻击快要结束时,才会响应攻击
|
||||
if self._IsCarriedCustomWeapon():
|
||||
args['cancel'] = True # 不响应原版的点击事件
|
||||
if self._LastAttackWillFinished():
|
||||
self._HandleAttack()
|
||||
|
||||
def _IsCarriedCustomWeapon(self):
|
||||
self.mCarriedItem = self.mItemComp.GetCarriedItem()
|
||||
return self.mCarriedItem and self.mCarriedItem['itemName'] in config.CustomWeaponList
|
||||
|
||||
def _LastAttackWillFinished(self):
|
||||
currentTime = time.time()
|
||||
return currentTime - config.AttackInterval > self.mLastAttackTime
|
||||
|
||||
def _HandleAttack(self):
|
||||
self.mLastAttackTime = time.time()
|
||||
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
|
||||
gameComp.AddTimer(0.2, self._SetAttackStateAndSyncToOtherClients, 'will_hit')
|
||||
gameComp.AddTimer(0.5, self._SetAttackStateAndSyncToOtherClients, 'end')
|
||||
|
||||
def _SetAttackStateAndSyncToOtherClients(self, state):
|
||||
# 设置本地的自定义变量
|
||||
self.mQueryVariableComp.Set(config.AttackVarName, 1.0 if state in ['start', 'will_hit'] else 0)
|
||||
# 通知其他客户端更新数据
|
||||
self.NotifyToServer("SyncAttackStateEvent", {'playerId': clientApi.GetLocalPlayerId(), 'state': state})
|
||||
```
|
||||
|
||||
服务端就是响应攻击以及同步玩家的攻击状态就可以了:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import math
|
||||
import mod.server.extraServerApi as serverApi
|
||||
from mod.common.utils.mcmath import Vector3
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialServerSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncAttackStateEvent", self, self.OnSyncAttackStateEvent)
|
||||
|
||||
# region 监听事件
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def OnSyncAttackStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
state = args['state']
|
||||
if state == 'will_hit':
|
||||
weaponDamage = self._GetCarriedWeaponDamage(playerId)
|
||||
self._HurtFrontArea(playerId, 3, weaponDamage)
|
||||
else:
|
||||
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
|
||||
self.NotifyToMultiClients(relevantPlayers, 'SyncAttackStateEvent', {
|
||||
'playerId': playerId,
|
||||
'value' : 1.0 if state == 'start' else 0.0
|
||||
})
|
||||
|
||||
# endregion
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def _GetCarriedWeaponDamage(self, playerId):
|
||||
itemComp = CompFactory.CreateItem(playerId)
|
||||
carriedItem = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
|
||||
basicInfo = itemComp.GetItemBasicInfo(carriedItem['itemName'])
|
||||
return basicInfo['weaponDamage']
|
||||
|
||||
# 攻击前方的区域
|
||||
def _HurtFrontArea(self, attacker, radius, damage, betweenAngle=75, knocked=True):
|
||||
attackerPos = CompFactory.CreatePos(attacker).GetFootPos()
|
||||
attackerRot = CompFactory.CreateRot(attacker).GetRot()
|
||||
# 计算攻击者朝向向量
|
||||
forwardVector = serverApi.GetDirFromRot(attackerRot)
|
||||
|
||||
entityList = gameComp.GetEntitiesAround(attacker, 6, {
|
||||
'any_of': {
|
||||
'test' : 'is_family',
|
||||
'subject' : 'other',
|
||||
'operator': 'not',
|
||||
'value' : 'instabuild'
|
||||
}
|
||||
})
|
||||
|
||||
for _entityId in entityList:
|
||||
entityPos = CompFactory.CreatePos(_entityId).GetFootPos()
|
||||
delta = Vector3(entityPos) - Vector3(attackerPos)
|
||||
|
||||
# 计算角度
|
||||
angle = math.degrees(math.acos(Vector3.Dot(delta.Normalized(), Vector3(forwardVector).Normalized())))
|
||||
|
||||
# 判断是否在扇形攻击范围内
|
||||
if betweenAngle == 0.0 or (angle < betweenAngle and delta.Length() < radius):
|
||||
CompFactory.CreateHurt(_entityId).Hurt(
|
||||
damage, serverApi.GetMinecraftEnum().ActorDamageCause.EntityAttack, attacker, knocked=knocked
|
||||
)
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
#### Step 6. 进入游戏检查效果
|
||||
|
||||
进入游戏,就可以看到看到效果了:
|
||||
|
||||

|
||||
|
||||
### 方法2:绑定骨骼
|
||||
|
||||
第二种方法,就是把我们制作好的模型绑定在任何可附加的生物骨骼上。**原版**的三叉戟、望远镜、弓和盾牌都是采用的这种方法。
|
||||
|
||||
虽然这种方法能够应用在更多的生物上,但模型绑定也有一些特殊的地方。我们下面会进行说明。
|
||||
|
||||
#### Step 1. 改造模型文件
|
||||
|
||||
首先,我们需要导出我们的模型文件,然后确认模型文件的版本是 `1.16.0`,然后在根骨骼组下加入下列一行话:
|
||||
|
||||
```text
|
||||
"binding": "query.item_slot_to_bone_name(context.item_slot)"
|
||||
```
|
||||
|
||||
整个文件就会看起来像是这样:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.16.0",
|
||||
"minecraft:geometry": [
|
||||
{
|
||||
"description": {
|
||||
"identifier": "geometry.tutorial_custom_sword_by_attachable",
|
||||
"texture_width": 64,
|
||||
"texture_height": 64,
|
||||
"visible_bounds_width": 4,
|
||||
"visible_bounds_height": 2.5,
|
||||
"visible_bounds_offset": [0, 0.75, 0]
|
||||
},
|
||||
"bones": [
|
||||
{
|
||||
"name": "sword",
|
||||
"binding": "query.item_slot_to_bone_name(context.item_slot)",
|
||||
"pivot": [-6, 14, 0]
|
||||
},
|
||||
// ...............
|
||||
```
|
||||
|
||||
这时候的模型文件就不需要对齐玩家的骨骼组了,而是默认会根据物品的槽位来对骨骼进行绑定:
|
||||
|
||||
- 如果是主手则绑定 `rightItem` 骨骼;
|
||||
- 如果是副手则绑定 `leftItem` 骨骼;
|
||||
|
||||
这里我们也不对模型进行位置的修改了,只是把骨骼组的根改名为了 `sword`:
|
||||
|
||||

|
||||
|
||||
我们自己在创建模型的时候也要注意骨骼组名称不要跟原版的骨骼重名了。
|
||||
|
||||
#### Step 2. 创建动画
|
||||
|
||||
这时候我们使用 BlockBench 的动画模式,会发现菜单栏会出现几个选项供我们选择视角:
|
||||
|
||||

|
||||
|
||||
##### 第三人称修正动画
|
||||
|
||||
在选择第三人称视角的情况下,BlockBench 会自动出现一个玩家模型,这可以让我们来查看实际的情况情况:
|
||||
|
||||

|
||||
|
||||
所以我们要做的就是创建一个动画来修正位置就可以了:
|
||||
|
||||

|
||||
|
||||
##### 第三人称攻击动画
|
||||
|
||||
如果我们想要自定义的第三人称攻击动画,我们仍然需要把骨骼拖入原版的玩家骨骼之中,然后完成第三人称动画:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这时候吊诡的地方就出现了,我们先完成动画,之后再进行说明。
|
||||
|
||||
##### 第一人称动画
|
||||
|
||||
跟第一种方法一样,我们需要一个手持修正的动画和一个攻击动画,这里就演示一下成果:
|
||||
|
||||

|
||||
|
||||
#### Step 3. 准备动画控制器
|
||||
|
||||
第三人称的玩家动画控制器已经准备好了,我们只需要准备用于 attachable 里面的动画控制器就可以:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.custom_sword.first_attack": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"first_person_attack1": "query.mod.custom_sword_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"first_person_attack1": {
|
||||
"animations": [
|
||||
// 对应 attachable 里面配置的动画名称
|
||||
"first_person_attack1"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 4. attable 文件
|
||||
|
||||
我们需要在资源包下的 `attable` 文件夹下创建一个用于匹配我们自定义物品的附加物品定义文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_sword_by_attachable",
|
||||
"materials": {
|
||||
"default": "entity_alphatest",
|
||||
"enchanted": "entity_alphatest_glint"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_sword",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.tutorial_custom_sword_by_attachable"
|
||||
},
|
||||
"animations": {
|
||||
// 第一人称手持动画
|
||||
"hold_first_person": "animation.tutorial_custom_sword_by_attachable.hold_first_person",
|
||||
// 第三人称手持动画
|
||||
"hold_third_person": "animation.tutorial_custom_sword_by_attachable.hold_third_person",
|
||||
// 第一人称下的攻击动画
|
||||
"first_person_attack1": "animation.tutorial_custom_sword_by_attachable.first_attack1",
|
||||
"first_person_attack2": "animation.tutorial_custom_sword_by_attachable.first_attack2",
|
||||
// 第一人称下的攻击动画控制器
|
||||
"first_person_attack_controller": "controller.animation.custom_sword.first_attack",
|
||||
// 第三人称下的攻击动画(共用玩家的动画就可以)
|
||||
"third_person_attack1": "animation.tutorial_custom_sword_by_attachable.player.attack1",
|
||||
// 第三人称动画控制器,共用玩家的就可以
|
||||
"third_person_attack_controller": "controller.animation.custom_sword.third_attack"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
{
|
||||
"first_person_attack_controller": "c.is_first_person"
|
||||
},
|
||||
{
|
||||
"third_person_attack_controller": "!c.is_first_person"
|
||||
},
|
||||
{
|
||||
"hold_first_person": "c.is_first_person"
|
||||
},
|
||||
{
|
||||
"hold_third_person": "!c.is_first_person"
|
||||
}
|
||||
]
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.item_default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
除了正确匹配资源文件之外,吊诡和麻烦的地方就来了。
|
||||
|
||||
仔细观察,我们可以发现,这个文件的结构跟 `r\entity` 下的文件很像,本质上也是一种渲染控制器。而**控制器只能控制自身范围内的资源**。
|
||||
|
||||
我们在创建第三人称动画时,k 的动画同时涉及了玩家和物品。而玩家和物品可以说是独立的两个个体,所以说他们的控制器互相不影响,包括动画。
|
||||
|
||||
我们在第三人称的情况下,播放玩家动画时,由于玩家本身不存在 `sword` 骨骼组,所以玩家的动画对物品的骨骼没有一丝影响。
|
||||
|
||||
不在物品设置第三人称动画的情况下动画演示:
|
||||
|
||||

|
||||
|
||||
对比我们自己 k 的动画,会发现,物品相关的骨骼完全没有移动:
|
||||
|
||||

|
||||
|
||||
反过来,如果我们仅仅是在物品下(attachable 文件中)设置了第三人称动画,玩家的骨骼也不会受到影响:
|
||||
|
||||

|
||||
|
||||
这里就是方法二麻烦的地方:你需要**同时处理**玩家和物品的第三人称动画。
|
||||
|
||||
#### Step 5. 代码注入资源
|
||||
|
||||
其他就跟方法一如出一辙了,主要是注册自定义的攻击变量,以及注入第三人称动画和控制器:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import time
|
||||
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
|
||||
self, self.OnAddPlayerCreatedClientEvent)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
self.InitRender(playerId) # 包括其他玩家也需要被初始化
|
||||
|
||||
# 初始化绑定
|
||||
def InitRender(self, playerId):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
|
||||
|
||||
# 定义攻击的自定义变量
|
||||
queryVariableComp.Register(config.AttackVarName, 0)
|
||||
# 第一种方法:作为玩家的附加骨骼添加进渲染控制器中进行控制
|
||||
self._InitMethodOne(playerId)
|
||||
|
||||
# 另外一种形式的 3D 武器:附加骨骼
|
||||
# 则是通过 query.item_slot_to_bone_name 把模型绑定上了固定的位置,再用动画进行修正
|
||||
|
||||
# 两种方式都通用的第三人称动画和控制器
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
actorRenderComp.AddPlayerAnimation("third_person_attack1", "animation.tutorial_custom_sword_by_attachable.player.attack1")
|
||||
actorRenderComp.AddPlayerAnimationController("custom_sword_third_attack", "controller.animation.custom_sword.third_attack")
|
||||
actorRenderComp.AddPlayerScriptAnimate("custom_sword_third_attack", "!variable.is_first_person")
|
||||
actorRenderComp.RebuildPlayerRender()
|
||||
```
|
||||
|
||||
#### Step 6. 处理攻击事件
|
||||
|
||||
这里跟第一种方法一样,所以就不再重复了。
|
||||
|
||||
## 小结
|
||||
|
||||
两种方法各有优劣吧。不过第一种方法来说,可以不局限在某一个具体的武器上,也可以是其他任何一个附加的骨骼(包括不限于翅膀、盔甲、盾牌等)。不过缺点就是只能局限在某一个具体的实体上(毕竟模型骨骼是需要严格对齐的)。
|
||||
|
||||
方法二就是一个标准的原版的方法了,对于一般的武器装备,这也是比较推荐的方法。这样原版的僵尸一类的生物也可以像玩家一样使用武器。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 分别使用两种方法制作 3D 武器,感受两种方法的不同之处。
|
||||
251
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/2-制作3D盔甲.md
Normal file
251
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/2-制作3D盔甲.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 制作3D盔甲
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你添加一套拥有独立骨骼的 3D 盔甲,并且帮助你修改原版盔甲。(强烈建议阅读之前先阅读第一节课的内容,因为思路一样)
|
||||
|
||||
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅3D 盔甲的制作原理;
|
||||
- ✅如何修改原版盔甲;
|
||||
|
||||
## 成果展示
|
||||
|
||||
这节课我们将添加一套自定义骨骼的 3D 盔甲:
|
||||
|
||||

|
||||
|
||||
以及我们在尝试修改原版的盔甲之后,所有的原版盔甲都改变了造型(以钻石甲为例):
|
||||
|
||||

|
||||
|
||||
可以看到头盔上多了一个问号,肚子也鼓起来了,然后鞋子上多了一个骨骼。(别喷.. 我也觉得丑..)
|
||||
|
||||
## 3D 盔甲的制作方法
|
||||
|
||||
跟我们的 3D 武器是一个思路,还是存在两种方法。一种是作为额外的骨骼,用渲染控制器来条件渲染,这需要代码配合。由于盔甲一般比较简单,所以我们这里不介绍了。
|
||||
|
||||
重点还是介绍第二种 attachable 的方法。
|
||||
|
||||
### 3D 盔甲模型原理
|
||||
|
||||
我们这里先在内容库「偷」一套 3D 盔甲的模型:
|
||||
|
||||

|
||||
|
||||
这里先随便选择一套盔甲模型,打开:
|
||||
|
||||

|
||||
|
||||
我们在第一节课中介绍过,这里 3D 盔甲的关键就是**对齐原版的玩家骨骼**,如果你不熟悉的话,可以打开原版的玩家骨骼进行检查(下列是玩家模型骨骼树状图,锚点即模型格式内的 pivot):
|
||||
|
||||
```text
|
||||
-root(锚点:[0, 0, 0])
|
||||
--waist(锚点:[0, 12, 0])
|
||||
---jacket(用于persona)
|
||||
---cape(披风)
|
||||
---body(锚点:[0, 24, 0])
|
||||
----leftArm(锚点:[5, 22, 0])
|
||||
-----leftSleeve(用于persona)
|
||||
-----leftItem(锚点:[6, 15, 1])
|
||||
----rightArm(锚点:[-5, 22, 0])
|
||||
-----rightSleeve(用于persona)
|
||||
-----rightItem(锚点:[-6, 15, 1])
|
||||
--leftLeg(锚点:[1.9, 12, 0])
|
||||
---leftPants(用于persona)
|
||||
--rightLeg(锚点:[-1.9, 12, 0])
|
||||
---rightPants(用于persona)
|
||||
```
|
||||
|
||||
只要骨骼在对应的骨骼组下方,且锚点对应的,那么 3D 盔甲就可以完美继承玩家的骨骼。所以我们在一番检查之后发现这个骨骼存在以下问题:
|
||||
|
||||
- 首先是少了根骨骼组 `root`,我们给加上;
|
||||
- 其次是左右腿的继承关系错了,原版是继承在 `waist` 下,而这个模型是继承在了 `body` 下面;
|
||||
- 再然后就是左右腿的 `pivot` 反了,原版左腿(`leftLeg`)是 `1.9`,而该模型左腿是 `-1.9`,右腿也是同理。
|
||||
|
||||
一番检查之后,我们就可以按照一般装备的划分,把骨骼拆成对应的四个部分就行了:
|
||||
|
||||

|
||||
|
||||
注意上图中的骨骼对应关系。然后导出模型到资源包的 `modles\entity\` 目录下即可。
|
||||
|
||||
### attachable 文件
|
||||
|
||||
接下来新增 `attachable` 文件让物品与骨骼对应就可以,下面列举一下四个文件。除了骨骼和 `scripts` 中的内容不一样外,其余都是相同的。
|
||||
|
||||
头部 helmet:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_armor_helmet",
|
||||
"materials": {
|
||||
"default": "armor",
|
||||
"enchanted": "armor_enchanted"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_armor",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.custom_armor_helmet"
|
||||
},
|
||||
"scripts": {
|
||||
"parent_setup": "variable.helmet_layer_visible = 0.0;"
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.armor"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
盔甲 chestplate:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_armor_chestplate",
|
||||
"materials": {
|
||||
"default": "armor",
|
||||
"enchanted": "armor_enchanted"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_armor",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.custom_armor_chestplate"
|
||||
},
|
||||
"scripts": {
|
||||
"parent_setup": "variable.helmet_layer_visible = 0.0;"
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.armor"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
绑腿 leggings:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_armor_leggings",
|
||||
"materials": {
|
||||
"default": "armor",
|
||||
"enchanted": "armor_enchanted"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_armor",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.custom_armor_leggings"
|
||||
},
|
||||
"scripts": {
|
||||
"parent_setup": "variable.helmet_layer_visible = 0.0;"
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.armor"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
鞋子 boots:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_armor_boots",
|
||||
"materials": {
|
||||
"default": "armor",
|
||||
"enchanted": "armor_enchanted"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_armor",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.custom_armor_boots"
|
||||
},
|
||||
"scripts": {
|
||||
"parent_setup": "variable.helmet_layer_visible = 0.0;"
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.armor"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 进入游戏测试
|
||||
|
||||
不出意外的话,进入游戏穿戴好装备,就可以看到实际的效果了:
|
||||
|
||||

|
||||
|
||||
并且因为骨骼组跟玩家完美适配的情况下,玩家原版的动画也完美适配(比如游泳):
|
||||
|
||||

|
||||
|
||||
## 原版盔甲的修改方法
|
||||
|
||||
其实原版的盔甲模型,我们也是可以修改的。原版玩家的盔甲模型位于原版资源包 `models\entity\player_armor.json`:
|
||||
|
||||

|
||||
|
||||
我们复制一份到我们的项目对应目录中,然后使用 BlockBench 打开,发现这文件中有很多骨骼:
|
||||
|
||||

|
||||
|
||||
我们只需要处理第一个就可以了,打开:
|
||||
|
||||

|
||||
|
||||
这个就是原版盔甲的骨骼了,由于需要适配原版的贴图,所以我们先来导入一下原版的纹理,下面是目录:
|
||||
|
||||

|
||||
|
||||
这里需要导入两个,`_1` 结尾的是不包含绑腿的贴图,`_2` 结尾的是包含绑腿的贴图:
|
||||
|
||||

|
||||
|
||||
然后我们就可以在这个框架内任意的增加骨骼了,只需要注意适配原版的贴图就可以,比如我们就胡乱改成了这样:
|
||||
|
||||

|
||||
|
||||
进入游戏查看效果:
|
||||
|
||||

|
||||
|
||||
## 小结
|
||||
|
||||
不管是自定义的 3D 盔甲模型还是修改原版的模型,我们发现都是需要在原版的骨骼架构内完成。
|
||||
|
||||
所以只需要注意这一点就行了。
|
||||
|
||||
修改原版骨骼这一点,是给大家留了一点可以想象的空间,可以自由发挥一下。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 制作一套属于自己的 3D 盔甲;
|
||||
- 修改原版的盔甲模型文件,在游戏中查看效果;
|
||||
918
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/3-枪械制作.md
Normal file
918
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/3-枪械制作.md
Normal file
@@ -0,0 +1,918 @@
|
||||
# 枪械制作
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你添加一个自定义的枪械,包含了界面的相关文件。
|
||||
|
||||
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅枪械的制作;
|
||||
- ✅第一人称显示手臂的两种方式;
|
||||
- ✅枪械 UI 的搭建;
|
||||
|
||||
## 成果展示
|
||||
|
||||
我们在本节课中将要制作一个带有开火特效,第一、第三人称动画完整,并且附带人物手臂显示和界面的自定义枪械:
|
||||
|
||||

|
||||
|
||||
## 制作枪械
|
||||
|
||||
换个角度来看,枪械就是更复杂一些的 3D 武器罢了。这里比较**推荐**使用方法一(请查看「制作 3D 武器章节」)来制作枪械,因为这会方便我们制作动画,以及节省我们编写和调试 attachable 文件的时间,还有一些额外的好处。
|
||||
|
||||
以下是一个枪械所需要的基本文件目录结构(不包含 ui 和贴图文件)和相应的说明:
|
||||
|
||||
```text
|
||||
├─behavior_packs
|
||||
│ └─tutorial_b
|
||||
│ ├─entities
|
||||
│ │ tutorial_custom_gun_bullet_projectile.json → 子弹抛射物
|
||||
│ │
|
||||
│ ├─netease_items_beh
|
||||
│ │ tutorial_custom_gun.json → 自定义枪械物品
|
||||
│ │ tutorial_custom_gun_bullet.json → 子弹物品定义
|
||||
│ │
|
||||
│ └─tutorialScripts → 代码
|
||||
│
|
||||
└─resource_packs
|
||||
└─tutorial_r
|
||||
├─animations
|
||||
│ tutorial_custom_gun.animation.json → 自定义枪械动画
|
||||
│ tutorial_custom_gun_bullet_project.animation.json → 子弹抛射物的动画
|
||||
│
|
||||
├─animation_controllers
|
||||
│ tutorial_custom_gun.animation_controllers.json → 枪械动画控制器
|
||||
│
|
||||
├─entity
|
||||
│ tutorial_custom_gun_bullet_projectile.entity.json → 子弹抛射物的渲染器定义
|
||||
│
|
||||
├─models
|
||||
│ └─entity
|
||||
│ tutorial_custom_gun.geo.json → 自定义枪械骨骼
|
||||
│ tutorial_custom_gun_bullet_projectile.geo.json → 子弹抛射物骨骼
|
||||
│
|
||||
├─netease_items_res
|
||||
│ tutorial_custom_gun.json
|
||||
│ tutorial_custom_gun_bullet.json
|
||||
│
|
||||
├─particles
|
||||
│ gun_fire.particle.json → 开火特效
|
||||
│
|
||||
└─render_controllers
|
||||
player.render_controllers.json → 原版玩家渲染控制器,主要用来修改第一人称下的右臂显示
|
||||
tutorial_custom_gun.render_controllers.json → 自定义枪械控制器
|
||||
```
|
||||
|
||||
让我们开始吧。
|
||||
|
||||
### 显示手臂的两种方法
|
||||
|
||||
为什么先来介绍这个呢,因为这涉及到我们对于模型和动画的处理。总体思路有两种:
|
||||
|
||||
[第一种](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/1-%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%A9%E5%93%81/9-%E8%87%AA%E5%AE%9A%E4%B9%893D%E7%89%A9%E5%93%81/3-%E8%87%AA%E5%AE%9A%E4%B9%893D%E6%AD%A6%E5%99%A8%E6%94%BB%E5%87%BB%E6%95%88%E6%9E%9C%EF%BC%88%E4%B8%8A%EF%BC%89.html?catalog=1):为第一人称单独建一个带玩家手臂的模型,然后让手臂继承原版的 uv 继承,让手臂使用原版玩家的贴图。原理大概如下:
|
||||
|
||||

|
||||
|
||||
第二种:用动画控制原版的手臂并让其在第一人称下显示。
|
||||
|
||||
我们这里也是使用的第二种方法,毕竟第一种需要对美术有要求。
|
||||
|
||||
我们先来说一下第二种方法的原理。我们可以先了解一下原版第一人称**不显示手臂的原因**,观察原版的 `player.render_controller.json` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.player.first_person": {
|
||||
"geometry": "Geometry.default",
|
||||
"materials": [ { "*": "Material.default" } ],
|
||||
"textures": [ "Texture.default" ],
|
||||
"part_visibility": [
|
||||
{ "*": false },
|
||||
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map'" },
|
||||
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map'" },
|
||||
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
|
||||
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
可以看到,只有在不拿物品或者拿着指定物品时,左右手臂才会显示。这意味着我们可以通过修改原版的控制器,再配合动画,就可以在第一人称显示手臂了。
|
||||
|
||||
### 添加基础文件
|
||||
|
||||
首先基础物品的定义都很简单,一个子弹物品,记得设置 `max_stack_size` 为 64;另一个自定义枪械记住添加以下组件就 OK:
|
||||
|
||||
```json
|
||||
"netease:show_in_hand": {
|
||||
"value": false
|
||||
},
|
||||
```
|
||||
|
||||
我们直接进入到模型的制作。对齐原版的骨骼制作好一个基础的模型文件(当然位置也要对齐):
|
||||
|
||||

|
||||
|
||||
由于我们需要挂接开火的特效,所以模型上还需要额外添加上两个定位器:
|
||||
|
||||

|
||||
|
||||
然后准备一个简单的激光子弹模型:
|
||||
|
||||

|
||||
|
||||
### 开火特效
|
||||
|
||||
我们先使用原版的粒子贴图来制作一个开火特效,起因是我们发现原版的贴图中有一排很适合用来模拟开火特效的粒子贴图:
|
||||
|
||||

|
||||
|
||||
简单弄一下效果:
|
||||
|
||||

|
||||
|
||||
关于原版特效的创建与导入可参考[原版特效创建与导入](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/16-美术/9-特效/20-原版特效创建与导入.html?catalog=1)
|
||||
|
||||
特效文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"particle_effect": {
|
||||
"description": {
|
||||
"identifier": "tutorial:gun_fire",
|
||||
"basic_render_parameters": {
|
||||
"material": "particles_alpha",
|
||||
"texture": "textures/particle/particles"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:emitter_local_space": {
|
||||
"position": true,
|
||||
"rotation": true
|
||||
},
|
||||
"minecraft:emitter_rate_instant": {
|
||||
"num_particles": 2
|
||||
},
|
||||
"minecraft:emitter_lifetime_expression": {
|
||||
"activation_expression": 0.1
|
||||
},
|
||||
"minecraft:emitter_shape_point": {},
|
||||
"minecraft:particle_lifetime_expression": {
|
||||
"max_lifetime": 0.1
|
||||
},
|
||||
"minecraft:particle_initial_spin": {
|
||||
"rotation": "Math.random(-200, 200)",
|
||||
"rotation_rate": "Math.random(-200, 200)"
|
||||
},
|
||||
"minecraft:particle_initial_speed": 0,
|
||||
"minecraft:particle_motion_dynamic": {},
|
||||
"minecraft:particle_appearance_billboard": {
|
||||
"size": ["variable.particle_random_1 * 0.3 + variable.particle_age", "variable.particle_random_1 * 0.3 + variable.particle_age"],
|
||||
"facing_camera_mode": "lookat_xyz",
|
||||
"uv": {
|
||||
"texture_width": 128,
|
||||
"texture_height": 128,
|
||||
"flipbook": {
|
||||
"base_UV": [0, 72],
|
||||
"size_UV": [8, 8],
|
||||
"step_UV": [8, 0],
|
||||
"frames_per_second": 4,
|
||||
"max_frame": 8,
|
||||
"stretch_to_lifetime": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"minecraft:particle_appearance_tinting": {
|
||||
"color": {
|
||||
"interpolant": "variable.particle_age / variable.particle_lifetime",
|
||||
"gradient": {
|
||||
"0.0": "#FFE2D51E",
|
||||
"0.21": "#FFF7F5EC",
|
||||
"0.41": "#FFC12807",
|
||||
"1.0": "#FFB94C02"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动画文件
|
||||
|
||||
我们先来准备一个用于制作动画的模型,复制一份原版的玩家模型,然后导入我们的自定义枪械模型,删除贴图:
|
||||
|
||||

|
||||
|
||||
#### 第三人称动画
|
||||
|
||||
由于骨骼完美继承玩家,所以第三人称就不需要有对齐的动画了,只需要一个攻击动画。
|
||||
|
||||
我们来简单弄一个抬手开火的动画:
|
||||
|
||||

|
||||
|
||||
这里的特效需要用到制作模型时加入的定位器:
|
||||
|
||||

|
||||
|
||||
可以直接在 BlockBench 中看到效果,还是非常方便的。
|
||||
|
||||
另外由于我们手持物品时,原版默认有一个小小的抬起,所以动画这里要设置成覆盖:
|
||||
|
||||
```json
|
||||
// 因为原版手持物品时,手臂有一个向上抬起的偏移量,这里需要覆盖掉
|
||||
"override_previous_animation": true,
|
||||
```
|
||||
|
||||
#### 第一人称动画
|
||||
|
||||
第一人称需要两个动画:手持动画和攻击动画。
|
||||
|
||||
正如我们第一节课中说的那样,需要先模拟游戏中第一人称的视角,这需要一个动画:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.first_person_guide.right_arm.method_one": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"rightArm": {
|
||||
"rotation": [95, -45, 115],
|
||||
"position": [13.5, -10, 12]
|
||||
},
|
||||
"rightItem": {
|
||||
"position": [0, 0, -1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
和一个固定的相机视角:
|
||||
|
||||

|
||||
|
||||
然后在播放上述动画的情况下,把除了 `rightArm` 之外的骨骼全部隐藏掉,k 我们自己的第一人称手持动画就可以了:
|
||||
|
||||

|
||||
|
||||
攻击动画建议直接先**复制**第三人称攻击动画的节点,主要是对齐动画发生的时间节点,然后再在播放「手持动画」和「第一人称模拟动画」之后对着第三人称动画 k 就行了:
|
||||
|
||||

|
||||
|
||||
### 动画控制器
|
||||
|
||||
准备两个动画控制器,分别在第一人称和第三人称的情况下控制播放不同的动画,没什么好说的,除了播放的动画名称不一样,其余都相同,完整文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.custom_gun_first_person": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"first_person_attack1": "query.mod.custom_gun_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"first_person_attack1": {
|
||||
"animations": [
|
||||
"custom_gun_attack_first_person"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_gun_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.custom_gun_third_person": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"third_person_attack1": "query.mod.custom_gun_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"third_person_attack1": {
|
||||
"animations": [
|
||||
"custom_gun_attack_third_person"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_gun_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里的 `query.mod.custom_gun_attack` 是需要后续在代码中注册和设置的自定义变量。
|
||||
|
||||
### 渲染器
|
||||
|
||||
这里我们需要先把原版的 `player.render_controllers.json` 复制到我们的项目中(`_r\render__controllers\` 目录下),然后进行改造:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.player.first_person": {
|
||||
"geometry": "Geometry.default",
|
||||
"materials": [ { "*": "Material.default" } ],
|
||||
"textures": [ "Texture.default" ],
|
||||
"part_visibility": [
|
||||
{ "*": false },
|
||||
// 修改原版渲染器,让它支持在手持自定义枪械时,显示右手臂
|
||||
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun'" },
|
||||
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun'" },
|
||||
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
|
||||
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
主要的变动就是在 `rightArm` 和 `rightSleeve` 的条件中,加入了手持自定义枪械的 Molang。
|
||||
|
||||
然后就是新增一个自定义枪械的渲染器(`tutorial_custom_gun.render_controllers.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.tutorial_custom_gun": {
|
||||
"geometry": "Geometry.custom_gun",
|
||||
"materials": [{"*": "Material.default"}],
|
||||
"textures": ["Texture.custom_gun"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 子弹抛射物
|
||||
|
||||
行为包完整文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.13.0",
|
||||
"minecraft:entity": {
|
||||
"description": {
|
||||
"is_experimental": false,
|
||||
"identifier": "tutorial:custom_gun_bullet_projectile",
|
||||
"is_spawnable": false,
|
||||
"is_summonable": false
|
||||
},
|
||||
"component_groups": {
|
||||
},
|
||||
"components": {
|
||||
"minecraft:despawn": {
|
||||
"despawn_from_distance": {}
|
||||
},
|
||||
"minecraft:physics": {},
|
||||
"minecraft:projectile": {
|
||||
"on_hit": {
|
||||
"remove_on_hit": {},
|
||||
"impact_damage": {
|
||||
"catch_fire": false,
|
||||
"knockback": true,
|
||||
"damage": 4,
|
||||
"destroy_on_hit": true
|
||||
}
|
||||
},
|
||||
"gravity": 0.0,
|
||||
"power": 1.0,
|
||||
"offset": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"minecraft:collision_box": {
|
||||
"width": 0.31,
|
||||
"height": 0.31
|
||||
},
|
||||
"netease:custom_entity_type": {
|
||||
"value": "projectile_entity"
|
||||
},
|
||||
"minecraft:pushable": {
|
||||
"is_pushable_by_piston": true,
|
||||
"is_pushable": true
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重点还是添加上网易的组件,标识上这个实体是一个抛射物:
|
||||
|
||||
```json
|
||||
"netease:custom_entity_type": {
|
||||
"value": "projectile_entity"
|
||||
},
|
||||
```
|
||||
|
||||
资源包的 `.entity.json` 文件定义也很简单:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_gun_bullet_projectile",
|
||||
"materials": {
|
||||
"default": "entity_alphatest"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/entity/tutorial_custom_gun_bullet_projectile"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.tutorial_custom_gun_bullet_projectile"
|
||||
},
|
||||
"animations": {
|
||||
"move": "animation.tutorial_custom_gun_bullet_projectile.move"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
"move"
|
||||
]
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里的 `move` 动画,是一个固定的动画,让子弹抛射物的根骨骼(这里是 `body`)执行下列文件就行:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.tutorial_custom_gun_bullet_projectile.move": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"body": {
|
||||
"rotation": [
|
||||
"-query.target_x_rotation",
|
||||
"-query.target_y_rotation",
|
||||
0.0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个动画会让抛射物始终朝向速度方向。
|
||||
|
||||
### 添加相关资源
|
||||
|
||||
我们用代码把刚才的相关资源全部注入到玩家的渲染器中:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import time
|
||||
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
#
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
self.mItemComp = CompFactory.CreateItem(clientApi.GetLocalPlayerId())
|
||||
#
|
||||
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomGunStateEvent', self, self.OnSyncCustomGunStateEvent)
|
||||
# 系统事件
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
|
||||
self, self.OnAddPlayerCreatedClientEvent)
|
||||
|
||||
def OnSyncCustomGunStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
value = float(args['value'])
|
||||
CompFactory.CreateQueryVariable(playerId).Set(config.CustomGunAttackVarName, value)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
self.InitRender(playerId) # 包括其他玩家也需要被初始化
|
||||
|
||||
# 初始化绑定
|
||||
def InitRender(self, playerId):
|
||||
# 自定义枪械
|
||||
self._InitToCustomGun(playerId)
|
||||
|
||||
|
||||
# 自定义枪械初始化渲染
|
||||
def _InitToCustomGun(self, playerId):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
|
||||
queryVariableComp.Register(config.CustomGunAttackVarName, 0)
|
||||
queryVariableComp.Set(config.CustomGunAttackVarName, 0)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
actorRenderComp.AddPlayerGeometry('custom_gun', 'geometry.tutorial_custom_gun')
|
||||
actorRenderComp.AddPlayerTexture('custom_gun', 'textures/models/tutorial_custom_gun')
|
||||
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_gun",
|
||||
"query.get_equipped_item_name('main_hand') == 'custom_gun'")
|
||||
|
||||
# 定义动画和控制器名称
|
||||
animations = ['hold_first_person', 'attack_first_person', 'attack_third_person']
|
||||
controllers = ['custom_gun_first_person', 'custom_gun_third_person']
|
||||
|
||||
for anim in animations:
|
||||
animationKey = 'custom_gun_' + anim
|
||||
animationName = 'animation.tutorial_custom_gun.' + anim
|
||||
actorRenderComp.AddPlayerAnimation(animationKey, animationName)
|
||||
|
||||
for controller in controllers:
|
||||
controllerKey = controller + "_controller"
|
||||
controllerName = 'controller.animation.' + controller
|
||||
actorRenderComp.AddPlayerAnimationController(controllerKey, controllerName)
|
||||
|
||||
# 添加动画的触发条件
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_gun_hold_first_person',
|
||||
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
|
||||
)
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_gun_first_person_controller',
|
||||
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
|
||||
)
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_gun_third_person_controller',
|
||||
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
|
||||
)
|
||||
|
||||
# 添加特效
|
||||
actorRenderComp.AddPlayerParticleEffect('gun_fire', 'tutorial:gun_fire')
|
||||
# 有兴趣的同学也可以尝试加一下音效
|
||||
|
||||
```
|
||||
|
||||
### 射速的实现
|
||||
|
||||
要实现射速,除了代码上的配合外,还需要动画也跟着加速,正好原版支持这一特性。我们只需要在动画文件中添加上神秘代码就可以:
|
||||
|
||||
```json
|
||||
"animation.tutorial_custom_gun.attack_third_person": {
|
||||
// 实现射速
|
||||
"anim_time_update": "query.anim_time + query.delta_time * query.mod.custom_gun_attack_speed",
|
||||
```
|
||||
|
||||
其中,前面的 `query.anim_time + query.delta_time` 是必须的,而后面的参数是可以自定义的,比如 `query.anim_time + query.delta_time * 2` 就是 2 倍速播放,`query.anim_time + query.delta_time * 1` 就是原速播放。
|
||||
|
||||
所以我们这里又使用了另外一个自定义变量,我们也需要在初始化的时候声明好:
|
||||
|
||||
```python
|
||||
# 自定义枪械初始化渲染
|
||||
def _InitToCustomGun(self, playerId):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
|
||||
# 射速相关的定义
|
||||
queryVariableComp.Register(config.CustomGunAttackSpeedVarName, config.CustomGunDefaultAttackSpeed)
|
||||
queryVariableComp.Set(config.CustomGunAttackSpeedVarName, config.CustomGunDefaultAttackSpeed)
|
||||
```
|
||||
|
||||
3 倍射速演示:
|
||||
|
||||

|
||||
|
||||
0.5 倍速演示:
|
||||
|
||||

|
||||
|
||||
### UI 实现
|
||||
|
||||
UI 很简单,只需要在右下角显示当前背包中的子弹数量。然后在右下角固定一个射击按钮就可以了:
|
||||
|
||||

|
||||
|
||||
在客户端监听 `UiInitFinished` 事件之后进行注册:
|
||||
```python
|
||||
def OnUiInitFinished(self, args=None):
|
||||
# 注册 UI
|
||||
clientApi.RegisterUI('tutorialMod', 'tutorialCustomGunUI', 'tutorialScripts.uiScripts.UIScripts', 'tutorialGunUI.main')
|
||||
self.mCustomGunUINode = clientApi.CreateUI('tutorialMod', 'tutorialCustomGunUI', {'isHud': 1})
|
||||
```
|
||||
|
||||
### UI 代码
|
||||
|
||||
首先,我们需要在手持自定义枪械的时候显示 UI,完整代码如下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
import config
|
||||
|
||||
ViewBinder = clientApi.GetViewBinderCls()
|
||||
ViewRequest = clientApi.GetViewViewRequestCls()
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
Namespace = clientApi.GetEngineNamespace()
|
||||
SystemName = clientApi.GetEngineSystemName()
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
#ui布局绑定
|
||||
class UIScripts(ScreenNode):
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
self.mPlayerId = clientApi.GetLocalPlayerId()
|
||||
self.mClientSystem = clientApi.GetSystem('tutorialMod', 'tutorialClientSystem')
|
||||
self.mItemComp = CompFactory.CreateItem(self.mPlayerId)
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(self.mPlayerId)
|
||||
|
||||
# 组件地址
|
||||
self.mShotBtnPath = "/shotBtn"
|
||||
self.mBulletLabelPath = "/bulletNumLabel"
|
||||
|
||||
# 界面需要使用的自定义属性
|
||||
self.mFrameCnt = 0
|
||||
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
|
||||
self.mIsBtnDown = False # 按钮是否被按下
|
||||
self.mBtnDownFrame = 0 # 按钮按下帧数
|
||||
self.mShotSpeed = 0 # 攻击速度
|
||||
self.mAttackFrame = 0 # 攻击所需要的帧数
|
||||
self.mBulletNum = 0 # 子弹数量
|
||||
|
||||
def Create(self):
|
||||
print("===== Tutorial Custom Gun UI Create Finished =====")
|
||||
# 注册按钮的事件
|
||||
control = self.GetBaseUIControl(self.mShotBtnPath).asButton()
|
||||
control.AddTouchEventParams({"isSwallow": True})
|
||||
control.SetButtonTouchDownCallback(self.OnShotBtnDown)
|
||||
control.SetButtonTouchUpCallback(self.OnShotBtnUp)
|
||||
control.SetButtonTouchMoveOutCallback(self.OnShotBtnUp)
|
||||
|
||||
# 关注事件
|
||||
namespace, systemName = clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName()
|
||||
self.mClientSystem.ListenForEvent(namespace, systemName, "OnCarriedNewItemChangedClientEvent", self, self.OnCarriedNewItem)
|
||||
|
||||
# 刚创建时也自动触发一次
|
||||
self.OnCarriedNewItem({'itemDict': self.mItemComp.GetCarriedItem()})
|
||||
|
||||
def Update(self):
|
||||
self.mFrameCnt += 1
|
||||
if self.mBtnDownFrame > 0:
|
||||
if self.mBtnDownFrame + self.mAttackFrame <= self.mFrameCnt:
|
||||
if self.mIsBtnDown:
|
||||
# 还处于按下的状态,那么继续攻击
|
||||
self.mBtnDownFrame = self.mFrameCnt + self.mAttackFrame
|
||||
self._HandleAttack()
|
||||
else:
|
||||
# 还原 query 变量
|
||||
self.mQueryVariableComp.Set(config.CustomGunAttackVarName, 0.0)
|
||||
|
||||
# region 按钮事件
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def OnShotBtnDown(self, args):
|
||||
self._FreshBagBullet()
|
||||
if self.mBulletNum == 0:
|
||||
# 没有子弹不响应
|
||||
return
|
||||
self.mIsBtnDown = True
|
||||
if self.mFrameCnt >= self.mBtnDownFrame + self.mAttackFrame:
|
||||
self.mBtnDownFrame = self.mFrameCnt
|
||||
self._HandleAttack()
|
||||
|
||||
def OnShotBtnUp(self, args):
|
||||
self.mIsBtnDown = False
|
||||
|
||||
# endregion
|
||||
|
||||
# region 事件监听
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def OnCarriedNewItem(self, args):
|
||||
self.mCarriedItem = args['itemDict']
|
||||
if self._IsCarriedCustomGun():
|
||||
self._SetUIVisible(True)
|
||||
self._FreshUIData()
|
||||
else:
|
||||
self._SetUIVisible(False)
|
||||
|
||||
# endregion
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def _IsCarriedCustomGun(self):
|
||||
if self.mCarriedItem and self.mCarriedItem['itemName'] == 'tutorial:custom_gun':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _FreshUIData(self):
|
||||
# 读取攻击速度
|
||||
self.mShotSpeed = self.mQueryVariableComp.Get(config.CustomGunAttackSpeedVarName)
|
||||
self.mAttackFrame = self._GetAttackFrame()
|
||||
# 读取背包的信息,查看子弹信息
|
||||
self._FreshBagBullet()
|
||||
|
||||
def _FreshBagBullet(self):
|
||||
bulletNum = 0
|
||||
allItems = self.mItemComp.GetPlayerAllItems(clientApi.GetMinecraftEnum().ItemPosType.INVENTORY)
|
||||
for _itemDict in allItems:
|
||||
if not _itemDict:
|
||||
continue
|
||||
if _itemDict and _itemDict['itemName'] == 'tutorial:custom_gun_bullet':
|
||||
bulletNum += _itemDict['count']
|
||||
self.mBulletNum = bulletNum
|
||||
self.GetBaseUIControl(self.mBulletLabelPath).asLabel().SetText("背包剩余子弹:" + str(self.mBulletNum))
|
||||
|
||||
def _SetUIVisible(self, flag):
|
||||
self.SetScreenVisible(flag)
|
||||
|
||||
def _GetAttackFrame(self):
|
||||
framePerSecond = 30
|
||||
return int(config.CustomGunAttackAnimDuration * (1 / float(self.mShotSpeed)) * framePerSecond)
|
||||
|
||||
def _HandleAttack(self):
|
||||
animTotalTime = config.CustomGunAttackAnimDuration * (1 / float(self.mShotSpeed))
|
||||
# 原版的的等式转换一下而已
|
||||
# fireTime / float(animTotalTime) = config.CustomGunAttackAnimAttackFrame / float(config.CustomGunAttackAnimDuration)
|
||||
fireTime = config.CustomGunAttackAnimAttackFrame / float(config.CustomGunAttackAnimDuration) * float(animTotalTime)
|
||||
|
||||
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
|
||||
gameComp.AddTimer(fireTime, self._SetAttackStateAndSyncToOtherClients, 'fire')
|
||||
gameComp.AddTimer(animTotalTime, self._SetAttackStateAndSyncToOtherClients, 'end')
|
||||
|
||||
# 刷新背包的子弹数
|
||||
gameComp.AddTimer(animTotalTime, self._FreshBagBullet)
|
||||
|
||||
def _SetAttackStateAndSyncToOtherClients(self, state):
|
||||
# 设置本地自定义变量
|
||||
self.mQueryVariableComp.Set(config.CustomGunAttackVarName, 1.0 if state in ['start', 'fire'] else 0.0)
|
||||
# 通知其他客户端
|
||||
self.mClientSystem.NotifyToServer('SyncCustomGunAttackStateEvent', {'state': state, 'playerId': self.mPlayerId})
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
对代码稍微做一些解释:
|
||||
|
||||
- 这里面我们监听了 `OnCarriedNewItemChangedClientEvent` 而用来控制整个界面的显示与否;
|
||||
- 还有一个 `_GetAttackFrame` 函数用来转换射速和游戏帧数,因为界面文件中的 Tick 函数中,一秒是 30 帧,所以这里要配合射速进行转换;
|
||||
- 我们攻击状态除了设置本地之外,还需要使用事件来同步到其他客户端;
|
||||
- 连发的实现思路是:1)按下之后记录按下的帧数;2)当按下之后立马处理攻击,然后等待到下一次检测帧,也就是按下帧数+攻击动画所需的帧数;3)如果此时还处于按下的状态,再次处理攻击,并重置按下的帧数;4)以此循环;
|
||||
|
||||
### 处理攻击
|
||||
|
||||
服务端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import math
|
||||
import mod.server.extraServerApi as serverApi
|
||||
from mod.common.utils.mcmath import Vector3
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialServerSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomGunAttackStateEvent", self,
|
||||
self.OnSyncCustomGunAttackStateEvent)
|
||||
|
||||
def OnSyncCustomGunAttackStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
state = args['state']
|
||||
if state == 'fire':
|
||||
self._ShotBullet(playerId)
|
||||
else:
|
||||
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
|
||||
self.NotifyToMultiClients(relevantPlayers, 'SyncCustomGunStateEvent', {
|
||||
'playerId': playerId,
|
||||
'value' : 1.0 if state == 'start' else 0.0
|
||||
})
|
||||
|
||||
# 发射子弹
|
||||
def _ShotBullet(self, playerId):
|
||||
rot = CompFactory.CreateRot(playerId).GetRot()
|
||||
footPos = CompFactory.CreatePos(playerId).GetFootPos()
|
||||
shotPos = (footPos[0], footPos[1] + 1.25, footPos[2])
|
||||
shotPos = self._MoveForward(shotPos, rot, 1.3)
|
||||
param = {
|
||||
'power' : 1.2,
|
||||
'gravity' : 0,
|
||||
'position' : shotPos,
|
||||
'direction': serverApi.GetDirFromRot(rot)
|
||||
}
|
||||
|
||||
projectileComp = CompFactory.CreateProjectile(playerId)
|
||||
projectileComp.CreateProjectileEntity(playerId, 'tutorial:custom_gun_bullet_projectile', param)
|
||||
|
||||
self._ReduceItem(playerId, 'tutorial:custom_gun_bullet', 1)
|
||||
|
||||
# 向前移动坐标
|
||||
def _MoveForward(self, pos, rot, distance):
|
||||
_rot = (0, rot[1]) # 第一个参数是上下角度,第二个是左右角度
|
||||
rx, ry, rz = serverApi.GetDirFromRot(_rot)
|
||||
return (pos[0] + rx * distance, pos[1] + ry * distance, pos[2] + rz * distance)
|
||||
|
||||
def _ReduceItem(self, playerId, itemName, count):
|
||||
itemComp = CompFactory.CreateItem(playerId)
|
||||
allItems = itemComp.GetPlayerAllItems(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY)
|
||||
remainingCount = count # 剩余要减少的数量
|
||||
|
||||
for slotPos, itemDict in enumerate(allItems):
|
||||
if itemDict and itemDict['itemName'] == itemName:
|
||||
item_count = itemDict['count']
|
||||
if item_count >= remainingCount:
|
||||
itemComp.SetInvItemNum(slotPos, item_count - remainingCount)
|
||||
return True # 减少成功
|
||||
else:
|
||||
itemComp.SetInvItemNum(slotPos, 0)
|
||||
remainingCount -= item_count
|
||||
|
||||
return False # 减少失败,物品数量不足
|
||||
```
|
||||
|
||||
由于我们在本地的界面中已经判断了物品数量是否足够,所以服务端不做特殊的处理。
|
||||
|
||||
唯一需要处理的就是子弹发出的初始位置,是玩家的 `footPos` 在 `y` 方向上 `+1.25` 之后,再向朝向的方向向前移动 `1.3` 的距离。
|
||||
|
||||
客户端唯一需要做的就是监听事件,并设置自定义变量:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomGunStateEvent', self, self.OnSyncCustomGunStateEvent)
|
||||
|
||||
def OnSyncCustomGunStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
value = float(args['value'])
|
||||
CompFactory.CreateQueryVariable(playerId).Set(config.CustomGunAttackVarName, value)
|
||||
```
|
||||
|
||||
### 进入游戏测试
|
||||
|
||||
至此,自定义枪械就基本完成了,就可以进入游戏进行测试了。主要是测试一下联机时候的表现如何:
|
||||
|
||||

|
||||
|
||||
可以看到完全没问题。
|
||||
|
||||
## 小结
|
||||
|
||||
这篇文章带大家制作了一个简单的自定义枪械,主要的精力都花在了如何「开火」和「手臂显示」这两个部分。
|
||||
|
||||
但其实枪械还有很多地方可以优化的地方,比如:奔跑动画、换弹动画等。感兴趣的小伙伴可以自行尝试。思路完全是一样的。
|
||||
|
||||
另外比较重要的是学习了「**射速**」的实现,这一点特性大家可以发挥想象,其实可以运用在很多地方。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 制作一个第一人称、第三人称动画完整,有自定义界面的自定义枪械。
|
||||
- 并且支持配置「射速」这一个特性;
|
||||
527
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/4-消耗类武器.md
Normal file
527
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/4-消耗类武器.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# 消耗类武器
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你添加一个可以投掷出去的燃烧瓶 3D 武器。(强烈建议阅读之前先阅读第一节课的内容,因为思路一样)
|
||||
|
||||
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅制作一个燃烧瓶,可投掷并造成伤害。
|
||||
|
||||
## 成果展示
|
||||
|
||||
还是一个动画完整,附带一个简单交互界面的燃烧瓶,可以扔出去造成伤害:
|
||||
|
||||

|
||||
|
||||
## 燃烧瓶制作
|
||||
|
||||
所有的 3D 武器都是有两种制作思路 ,由于燃烧瓶几乎是只提供给玩家使用的道具,所以我们这里直接采取第一种额外骨骼的方式。
|
||||
|
||||
### 模型制作
|
||||
|
||||
先来一个骨骼对应好的模型:
|
||||
|
||||

|
||||
|
||||
除此之外,我们还需要一个用于投掷出去的抛射物模型:
|
||||
|
||||

|
||||
|
||||
制作好之后导出到响应文件下就可以。
|
||||
|
||||
### 动画制作
|
||||
|
||||
我们先来准备一个用于制作动画的模型,复制一份原版的玩家模型,然后导入我们的自定义燃烧瓶模型,删除贴图:
|
||||
|
||||

|
||||
|
||||
#### 第三人称动画
|
||||
|
||||
由于骨骼是与玩家严格对齐,所以第三人称手持动画就不需要制作了。直接制作一个抬手仍出去的攻击动画:
|
||||
|
||||

|
||||
|
||||
由于第三人称的情况下手臂默认会有一个抬起的动画,所以第三人称的动画需要勾选上动画的「覆盖」模式。
|
||||
|
||||
#### 第一人称动画
|
||||
|
||||
还是按照之前的方法,先模拟出游戏中的第一人称视角。加入我们的模拟动画:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.first_person_guide.right_arm.method_one": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"rightArm": {
|
||||
"rotation": [95, -45, 115],
|
||||
"position": [13.5, -10, 12]
|
||||
},
|
||||
"rightItem": {
|
||||
"position": [0, 0, -1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再来一个模拟第一人称相机的视角:
|
||||
|
||||

|
||||
|
||||
再把除了右手之外的其他骨骼给隐藏掉,然后修正第一人称视角下的位置:
|
||||
|
||||

|
||||
|
||||
然后我们可以直接考虑复制第三人称攻击动画的帧,加入到第一人称攻击动画中,这样做主要是为了对齐关键动作的时间,然后再做一些修改,就可以得到我们第一人称的攻击动画了 :
|
||||

|
||||
|
||||
#### 抛射物的飞行动画
|
||||
|
||||
为了稍微「精致」一点儿,我们也要为投掷出去的抛射物制作一个旋转动画:
|
||||
|
||||

|
||||
|
||||
### 动画控制器
|
||||
|
||||
动画控制器很简单,一个第一人称使用,一个第三人称使用,除了播放的动画不一样之外,没有区别:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.custom_fire_bottle_first_person": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"first_person_attack1": "query.mod.custom_fire_bottle_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"first_person_attack1": {
|
||||
"animations": [
|
||||
"custom_fire_bottle_attack_first_person"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_fire_bottle_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.custom_fire_bottle_third_person": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"third_person_attack1": "query.mod.custom_fire_bottle_attack == 1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"third_person_attack1": {
|
||||
"animations": [
|
||||
"custom_fire_bottle_attack_third_person"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.any_animation_finished && query.mod.custom_fire_bottle_attack == 0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 渲染器
|
||||
|
||||
为了在第一人称下显示手臂,我们也需要对应修改原版的 `player.render_controllers.json` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.player.first_person": {
|
||||
"geometry": "Geometry.default",
|
||||
"materials": [ { "*": "Material.default" } ],
|
||||
"textures": [ "Texture.default" ],
|
||||
"part_visibility": [
|
||||
{ "*": false },
|
||||
// 修改原版渲染器,让它支持在手持自定义枪械和燃烧瓶时,显示右手臂
|
||||
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun' || query.get_equipped_item_name(0, 1) == 'custom_fire_bottle'" },
|
||||
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun' || query.get_equipped_item_name(0, 1) == 'custom_fire_bottle'" },
|
||||
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
|
||||
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
然后还有燃烧瓶的渲染控制器(`tutorial_custom_fire_bottle.render_controllers.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"render_controllers": {
|
||||
"controller.render.tutorial_custom_fire_bottle": {
|
||||
"geometry": "Geometry.custom_fire_bottle",
|
||||
"materials": [{"*": "Material.default"}],
|
||||
"textures": ["Texture.custom_fire_bottle"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 抛射物的定义
|
||||
|
||||
我们还需要新建一个实体用来当做燃烧瓶的抛射物:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.13.0",
|
||||
"minecraft:entity": {
|
||||
"description": {
|
||||
"is_experimental": false,
|
||||
"identifier": "tutorial:custom_fire_bottle_projectile",
|
||||
"is_spawnable": false,
|
||||
"is_summonable": false
|
||||
},
|
||||
"component_groups": {
|
||||
},
|
||||
"components": {
|
||||
"minecraft:despawn": {
|
||||
"despawn_from_distance": {}
|
||||
},
|
||||
"minecraft:physics": {},
|
||||
"minecraft:projectile": {
|
||||
"on_hit": {
|
||||
"remove_on_hit": {},
|
||||
"impact_damage": {
|
||||
"catch_fire": true,
|
||||
"knockback": false,
|
||||
"damage": 1,
|
||||
"destroy_on_hit": true
|
||||
}
|
||||
},
|
||||
"gravity": 0.0,
|
||||
"power": 1.0,
|
||||
"offset": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"minecraft:collision_box": {
|
||||
"width": 0.31,
|
||||
"height": 0.31
|
||||
},
|
||||
"netease:custom_entity_type": {
|
||||
"value": "projectile_entity"
|
||||
},
|
||||
"minecraft:pushable": {
|
||||
"is_pushable_by_piston": true,
|
||||
"is_pushable": true
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
资源包下的实体定义:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "tutorial:custom_fire_bottle_projectile",
|
||||
"materials": {
|
||||
"default": "entity_alphatest"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_fire_bottle"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.tutorial_custom_fire_bottle_projectile"
|
||||
},
|
||||
"animations": {
|
||||
"move": "animation.tutorial_custom_fire_bottle_projectile.move"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
"move"
|
||||
]
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注入相关资源
|
||||
|
||||
把我们上面制作好的资源通过代码注入到玩家的渲染器下,并且监听了服务端传回来的状态同步事件:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
import time
|
||||
|
||||
import config
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomFireBottleStateEvent', self,
|
||||
self.OnSyncCustomFireBottleStateEvent)
|
||||
|
||||
def OnSyncCustomFireBottleStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
value = float(args['value'])
|
||||
CompFactory.CreateQueryVariable(playerId).Set(config.FireBottleAttackVarName, value)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
self.InitRender(playerId) # 包括其他玩家也需要被初始化
|
||||
|
||||
# 初始化绑定
|
||||
def InitRender(self, playerId):
|
||||
# 燃烧瓶
|
||||
self._InitToFireBottle(playerId)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
actorRenderComp.RebuildPlayerRender()
|
||||
|
||||
# 燃烧瓶渲染器
|
||||
def _InitToFireBottle(self, playerId):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
|
||||
queryVariableComp.Register(config.FireBottleAttackVarName, 0)
|
||||
queryVariableComp.Set(config.FireBottleAttackVarName, 0)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
# 控制器
|
||||
actorRenderComp.AddPlayerGeometry('custom_fire_bottle', 'geometry.tutorial_custom_fire_bottle')
|
||||
actorRenderComp.AddPlayerTexture('custom_fire_bottle', 'textures/models/tutorial_custom_fire_bottle')
|
||||
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_fire_bottle",
|
||||
"query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'")
|
||||
# 定义动画和控制器名称
|
||||
animations = ['hold_first_person', 'attack_first_person', 'attack_third_person']
|
||||
controllers = ['custom_fire_bottle_first_person', 'custom_fire_bottle_third_person']
|
||||
|
||||
for anim in animations:
|
||||
animationKey = 'custom_fire_bottle_' + anim
|
||||
animationName = 'animation.tutorial_custom_fire_bottle.' + anim
|
||||
actorRenderComp.AddPlayerAnimation(animationKey, animationName)
|
||||
|
||||
for controller in controllers:
|
||||
controllerKey = controller + "_controller"
|
||||
controllerName = 'controller.animation.' + controller
|
||||
actorRenderComp.AddPlayerAnimationController(controllerKey, controllerName)
|
||||
|
||||
# 添加动画的触发条件
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_fire_bottle_hold_first_person',
|
||||
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
|
||||
)
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_fire_bottle_first_person_controller',
|
||||
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
|
||||
)
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_fire_bottle_third_person_controller',
|
||||
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
|
||||
)
|
||||
```
|
||||
|
||||
### 编写 UI 文件
|
||||
|
||||
我们只需要一个拥有绝对定位的按钮的简单 UI:
|
||||
|
||||

|
||||
|
||||
界面也很简单:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
import config
|
||||
|
||||
ViewBinder = clientApi.GetViewBinderCls()
|
||||
ViewRequest = clientApi.GetViewViewRequestCls()
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
Namespace = clientApi.GetEngineNamespace()
|
||||
SystemName = clientApi.GetEngineSystemName()
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
#ui布局绑定
|
||||
class FireBottleUIScripts(ScreenNode):
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
self.mPlayerId = clientApi.GetLocalPlayerId()
|
||||
self.mClientSystem = clientApi.GetSystem('tutorialMod', 'tutorialClientSystem')
|
||||
self.mItemComp = CompFactory.CreateItem(self.mPlayerId)
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(self.mPlayerId)
|
||||
|
||||
# 组件地址
|
||||
self.mBtnPath = "/button"
|
||||
|
||||
# 界面所需的变量
|
||||
self.mCarriedItem = None
|
||||
|
||||
def Create(self):
|
||||
print("===== Tutorial Custom Gun UI Create Finished =====")
|
||||
# 注册按钮的事件
|
||||
control = self.GetBaseUIControl(self.mBtnPath).asButton()
|
||||
control.AddTouchEventParams({"isSwallow": True})
|
||||
control.SetButtonTouchUpCallback(self.OnButtonUp)
|
||||
|
||||
# 关注事件
|
||||
namespace, systemName = clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName()
|
||||
self.mClientSystem.ListenForEvent(namespace, systemName, "OnCarriedNewItemChangedClientEvent", self, self.OnCarriedNewItem)
|
||||
|
||||
# 刚创建时也自动触发一次
|
||||
self.OnCarriedNewItem({'itemDict': self.mItemComp.GetCarriedItem()})
|
||||
|
||||
# region 按钮事件
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def OnButtonUp(self, args):
|
||||
self._HandleThrow()
|
||||
# 按了一次之后就直接隐藏界面,避免误操作
|
||||
self._SetUIVisible(False)
|
||||
|
||||
# endregion
|
||||
|
||||
# region 事件监听
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def OnCarriedNewItem(self, args):
|
||||
self.mCarriedItem = args['itemDict']
|
||||
if self._IsCarriedCustomGun():
|
||||
self._SetUIVisible(True)
|
||||
else:
|
||||
self._SetUIVisible(False)
|
||||
|
||||
# endregion
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def _IsCarriedCustomGun(self):
|
||||
if self.mCarriedItem and self.mCarriedItem['itemName'] == 'tutorial:custom_fire_bottle':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _SetUIVisible(self, flag):
|
||||
self.SetScreenVisible(flag)
|
||||
|
||||
def _HandleThrow(self):
|
||||
throwTime = 0.25
|
||||
animTotalTime = 0.5
|
||||
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
|
||||
gameComp.AddTimer(throwTime, self._SetAttackStateAndSyncToOtherClients, 'throw')
|
||||
gameComp.AddTimer(animTotalTime, self._SetAttackStateAndSyncToOtherClients, 'end')
|
||||
|
||||
def _SetAttackStateAndSyncToOtherClients(self, state):
|
||||
# 设置本地自定义变量
|
||||
self.mQueryVariableComp.Set(config.FireBottleAttackVarName, 1.0 if state in ['start', 'throw'] else 0.0)
|
||||
# 通知其他客户端
|
||||
self.mClientSystem.NotifyToServer('SyncCustomFireBottleStateEvent', {'state': state, 'playerId': self.mPlayerId})
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
- 我们监听了 `OnCarriedNewItemChangedClientEvent` 事件,会在切换到该物品时显示按钮,也会在切换到其他物品时隐藏按钮;
|
||||
- 另外我们在响应点击之后,立马就隐藏了界面,防止多次点击;
|
||||
|
||||
### 处理投掷事件
|
||||
|
||||
我们已经在界面文件中,在响应点击之后发送了事件给服务端,所以服务端只需要监听事件做出响应就可以:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
|
||||
|
||||
|
||||
class TutorialServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialServerSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomFireBottleStateEvent", self,
|
||||
self.OnSyncCustomFireBottleStateEvent)
|
||||
|
||||
def OnSyncCustomFireBottleStateEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
state = args['state']
|
||||
if state == 'throw':
|
||||
self._ThrowFireBottle(playerId)
|
||||
else:
|
||||
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
|
||||
self.NotifyToMultiClients(relevantPlayers, 'SyncCustomFireBottleStateEvent', {
|
||||
'playerId': playerId,
|
||||
'value' : 1.0 if state == 'start' else 0.0
|
||||
})
|
||||
|
||||
# 向前投掷燃烧瓶
|
||||
def _ThrowFireBottle(self, playerId):
|
||||
rot = CompFactory.CreateRot(playerId).GetRot()
|
||||
# 默认是向上抬一点
|
||||
rot = (rot[0] - 30, rot[1])
|
||||
param = {
|
||||
'power' : 1.2,
|
||||
'gravity' : 0.125,
|
||||
'direction': serverApi.GetDirFromRot(rot)
|
||||
}
|
||||
|
||||
projectileComp = CompFactory.CreateProjectile(playerId)
|
||||
projectileComp.CreateProjectileEntity(playerId, 'tutorial:custom_fire_bottle_projectile', param)
|
||||
|
||||
self._ReduceCarriedItemNum(playerId, 1)
|
||||
|
||||
def _ReduceCarriedItemNum(self, playerId, reduceNum):
|
||||
itemComp = CompFactory.CreateItem(playerId)
|
||||
selectSlotId = itemComp.GetSelectSlotId()
|
||||
itemDict = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY, selectSlotId)
|
||||
return itemComp.SetInvItemNum(selectSlotId, itemDict['count'] - reduceNum)
|
||||
```
|
||||
|
||||
这里默认的投掷方向是玩家当前朝向向上偏移 30° 作为初始的燃烧瓶的速度方向。
|
||||
|
||||
### 进入游戏测试
|
||||
|
||||
完成上面的步骤,我们就可以进入游戏中愉快的测试了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 制作一个可投掷出去的 3D 道具,需要有一个简单可交互的界面、完整的第一、第三人称动画。
|
||||
458
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/5-投掷类武器.md
Normal file
458
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/5-投掷类武器.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# 投掷类武器
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将帮助你添加一个类似于原版三叉戟的可投掷武器。(强烈建议阅读之前先阅读前面一节课的内容,因为思路一样)
|
||||
|
||||
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅类似于原版三叉戟的投掷类武器。
|
||||
|
||||
## 成果展示
|
||||
|
||||
类似于原版的三叉戟的自定义投掷类武器:
|
||||
|
||||

|
||||
|
||||
## 投掷类武器制作
|
||||
|
||||
还是一样的思路,先去官方的内容库中「偷」一个模型文件:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 原版三叉戟
|
||||
|
||||
我们没有制作投掷类武器的经验,所以我们直接去原版查看三叉戟的 `attachable` 文件(位于 `definitionsattachables\trident.entity.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:attachable": {
|
||||
"description": {
|
||||
"identifier": "minecraft:trident",
|
||||
"materials": {
|
||||
"default": "entity_alphatest",
|
||||
"enchanted": "entity_alphatest_glint"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/entity/trident",
|
||||
"enchanted": "textures/misc/enchanted_item_glint"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.trident"
|
||||
},
|
||||
"animations": {
|
||||
"wield": "controller.animation.trident.wield",
|
||||
"wield_first_person": "animation.trident.wield_first_person",
|
||||
"wield_first_person_raise": "animation.trident.wield_first_person_raise",
|
||||
"wield_first_person_raise_shake": "animation.trident.wield_first_person_raise_shake",
|
||||
"wield_first_person_riptide": "animation.trident.wield_first_person_riptide",
|
||||
"wield_third_person": "animation.trident.wield_third_person",
|
||||
"wield_third_person_raise": "animation.trident.wield_third_person_raise"
|
||||
},
|
||||
"scripts": {
|
||||
"pre_animation": [
|
||||
"variable.charge_amount = math.clamp((query.main_hand_item_max_duration - (query.main_hand_item_use_duration - query.frame_alpha + 1.0)) / 10.0, 0.0, 1.0f);"
|
||||
],
|
||||
"animate": [
|
||||
"wield"
|
||||
]
|
||||
},
|
||||
"render_controllers": [ "controller.render.item_default" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模型文件
|
||||
|
||||
那思路就很简单了,直接仿制一个三叉戟的模型文件,其余的东西都是可以通用的。所以我们把模型稍微改一下:
|
||||
|
||||

|
||||
|
||||
这里原版三叉戟的 `pivot` 影响了动画的对齐和命中效果,我们要保持**绝对一致**(也就是说这个 24 不能改):
|
||||
|
||||

|
||||
|
||||
我们稍微观察一下三叉戟就大概明白了这个 `pivot` 是在什么位置:
|
||||
|
||||

|
||||
|
||||
如果打开「调试」中的「能见度边界框」的话,也能够发现这个锚点实际上就是命中的位置:
|
||||
|
||||

|
||||
|
||||
所以我们稍微更改一下我们的模型:
|
||||
|
||||

|
||||
|
||||
上面是用于手持的物品模型,对于处理用于**投掷出去的抛射物模型**,我们这里有两种方式处理:
|
||||
|
||||
- 不添加额外的模型,通过动画来修正投掷出去的动画。好处是不需要增加额外的动画,但。
|
||||
- 添加额外的抛射物模型。好处是动画简单,而且能够复用,坏处就是要多处理一遍模型。
|
||||
|
||||
如果我们想要采用第一种方式的话,就需要把模型往下移,把锚点移动到矛的尖上:
|
||||
|
||||

|
||||
|
||||
但此时游戏中的握持方式就会很奇怪,因为我们自己的模型长度跟原版的三叉戟不一致:
|
||||
|
||||

|
||||
|
||||
所以我们这里不采用第一种方式,而是额外增加一个单独的用于投掷物实体的模型。
|
||||
|
||||
不过这里还是先放一下采用第一种方式时的动画,提供给需要的同学:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.tutorial_thrown_custom_throw_weapon.move": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"pole": {
|
||||
// 下列是采用第一种方式时采用的动画
|
||||
"rotation": [
|
||||
"-query.target_x_rotation + 90", // 这里需要旋转 90 正对 N 方向
|
||||
"-query.target_y_rotation",
|
||||
0.0
|
||||
],
|
||||
"position": [
|
||||
// 这里的 -24 对应了 pivot 偏移的 24
|
||||
0, -24, 0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
OK,那我们采用第二种方式只需要新增一个锚点在原点,并且朝向 N 方向的模型就可以:
|
||||
|
||||

|
||||
|
||||
此时的动画文件,可以通用,就是让实体朝向运动方向:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.tutorial_thrown_custom_throw_weapon.move": {
|
||||
"loop": true,
|
||||
"bones": {
|
||||
"pole": {
|
||||
// 下列是教程中采用的第二种方法的动画
|
||||
"rotation": [
|
||||
"-query.target_x_rotation",
|
||||
"-query.target_y_rotation",
|
||||
0.0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 基础物品定义
|
||||
|
||||
行为包下的物品定义文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"category": "Equipment",
|
||||
"identifier": "tutorial:custom_throw_weapon",
|
||||
"custom_item_type": "ranged_weapon"
|
||||
},
|
||||
"components": {
|
||||
"netease:show_in_hand": {
|
||||
"value": false
|
||||
},
|
||||
"minecraft:max_damage": 10,
|
||||
// 保证使用足够长,否则动画和视角会重新开始
|
||||
"minecraft:use_duration": 99999
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当我们把 `custom_item_type` 定义为 `ranged_weapon` 时,并且组件中拥有 `minecraft:use_duration` 时,我们在手持物品的情况下右键(手机是长按),就自动会有镜头缩放的效果:
|
||||
|
||||

|
||||
|
||||
### 抛射物实体文件
|
||||
|
||||
我们还需要额外创建一个抛射物实体,直接复制粘贴原版的三叉戟就好,只不过需要额外添加两个组件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.12.0",
|
||||
"minecraft:entity": {
|
||||
"description": {
|
||||
"identifier": "tutorial:thrown_custom_throw_weapon",
|
||||
"is_spawnable": false,
|
||||
"is_summonable": false,
|
||||
"is_experimental": false
|
||||
},
|
||||
"components": {
|
||||
"minecraft:collision_box": {
|
||||
"width": 0.25,
|
||||
"height": 0.35
|
||||
},
|
||||
"minecraft:projectile": {
|
||||
"on_hit": {
|
||||
"impact_damage": {
|
||||
"damage": 8,
|
||||
"knockback": true,
|
||||
"semi_random_diff_damage": false,
|
||||
"destroy_on_hit": false
|
||||
},
|
||||
"stick_in_ground": {
|
||||
"shake_time": 0
|
||||
}
|
||||
},
|
||||
"liquid_inertia": 0.99,
|
||||
"hit_sound": "item.trident.hit",
|
||||
"hit_ground_sound": "item.trident.hit_ground",
|
||||
"power": 4,
|
||||
"gravity": 0.10,
|
||||
"uncertainty_base": 1,
|
||||
"uncertainty_multiplier": 0,
|
||||
"stop_on_hurt": true,
|
||||
"anchor": 1,
|
||||
"should_bounce": true,
|
||||
"multiple_targets": false,
|
||||
"offset": [0, -0.1, 0]
|
||||
},
|
||||
"minecraft:physics": {
|
||||
},
|
||||
"minecraft:pushable": {
|
||||
"is_pushable": true,
|
||||
"is_pushable_by_piston": true
|
||||
},
|
||||
"netease:custom_entity_type": {
|
||||
"value": "projectile_entity"
|
||||
},
|
||||
"netease:pick_up": {
|
||||
"item_name": "tutorial:custom_throw_weapon"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们需要额外添加 `netease:custom_entity_type` 来标识这个实体是抛射物,以及 `netease:pick_up` 组件,用来在玩家接触时拾取变成物品。
|
||||
|
||||
行为包下 `\entity` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "tutorial:thrown_custom_throw_weapon",
|
||||
"materials": {
|
||||
"default": "entity_alphatest"
|
||||
},
|
||||
"textures": {
|
||||
"default": "textures/models/tutorial_custom_throw_weapon"
|
||||
},
|
||||
"geometry": {
|
||||
"default": "geometry.tutorial_thrown_custom_throw_weapon"
|
||||
},
|
||||
"animations": {
|
||||
"move": "animation.tutorial_thrown_custom_throw_weapon.move"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
"move"
|
||||
]
|
||||
},
|
||||
"render_controllers": [
|
||||
"controller.render.default"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码注入第三人称动画
|
||||
|
||||
当我们把这些文件都准备好之后,你会发现第三人称并不会把手抬起来:
|
||||
|
||||

|
||||
|
||||
这就是我们在「自定义枪械」那一节课中说的,attachable 中的动画,只会影响武器,而不会反作用于玩家。
|
||||
|
||||
所以我们还需要在玩家手持投掷武器时,播放原版的投掷动画。
|
||||
|
||||
问题是我们并不知道原版的投掷动画是哪一个,我们要么去原版的文件中找(还是挺好找的),要么,就按 F3 直到出现下列的界面:
|
||||
|
||||

|
||||
|
||||
然后打开动画编辑器:
|
||||

|
||||
|
||||
然后我们就可以在手持三叉戟的情况下,通过不断右键触发动画,来找到到底播放的是哪一个动画:
|
||||
|
||||

|
||||
|
||||
很快,我们就找到了播放的 `brandish_spear` 动画,一番搜索,就定位到了动画名称:
|
||||
|
||||
```json
|
||||
animation.humanoid.brandish_spear
|
||||
```
|
||||
|
||||
我们使用代码注入到玩家的控制器中:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
|
||||
self, self.OnAddPlayerCreatedClientEvent)
|
||||
|
||||
def OnAddPlayerCreatedClientEvent(self, args):
|
||||
playerId = args['playerId']
|
||||
self.InitRender(playerId) # 包括其他玩家也需要被初始化
|
||||
|
||||
# 初始化绑定
|
||||
def InitRender(self, playerId):
|
||||
# 投掷武器
|
||||
self._InitToThrowWeapon(playerId)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
actorRenderComp.RebuildPlayerRender()
|
||||
|
||||
# 投掷武器的渲染
|
||||
def _InitToThrowWeapon(self, playerId):
|
||||
actorRenderComp = CompFactory.CreateActorRender(playerId)
|
||||
# 使用原版的动画
|
||||
actorRenderComp.AddPlayerAnimation('custom_throw_weapon_third_person_raise', 'animation.humanoid.brandish_spear')
|
||||
actorRenderComp.AddPlayerScriptAnimate(
|
||||
'custom_throw_weapon_third_person_raise',
|
||||
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_throw_weapon' && query.main_hand_item_use_duration > 0"
|
||||
)
|
||||
```
|
||||
|
||||
这样,我们在第三人称的情况下,就可以播放原版的抬手动作了:
|
||||
|
||||

|
||||
|
||||
### 处理投掷事件
|
||||
|
||||
客户端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TutorialClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
self.mTimeCounter = 0
|
||||
#
|
||||
self.mIsUsingItem = False # 是否正在使用投掷类武器
|
||||
self.mStartUsingFrame = 0 # 开始使用物品的时间
|
||||
|
||||
def ListenEvent(self):
|
||||
# 投掷武器相关的事件
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemTryUseEvent", self,
|
||||
self.OnClientItemTryUseEvent)
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ItemReleaseUsingClientEvent", self,
|
||||
self.OnItemReleaseUsingClientEvent)
|
||||
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
|
||||
def OnClientItemTryUseEvent(self, args):
|
||||
if args['itemName'] == 'tutorial:custom_throw_weapon':
|
||||
self.mIsUsingItem = True
|
||||
self.mStartUsingFrame = self.mTimeCounter
|
||||
|
||||
def OnItemReleaseUsingClientEvent(self, args):
|
||||
itemDict = args['itemDict']
|
||||
if itemDict and itemDict['itemName'] == 'tutorial:custom_throw_weapon':
|
||||
# 最大蓄力 2s,也就是说威力最多为 2 倍
|
||||
power = min(2.0, (self.mTimeCounter - self.mStartUsingFrame) / 30.0)
|
||||
self.NotifyToServer('ThrowCustomWeapon', {'playerId': args['playerId'], 'power': power}
|
||||
|
||||
```
|
||||
|
||||
服务端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
|
||||
class TutorialServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TutorialServerSystem, self).__init__(namespace, name)
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomGunAttackStateEvent", self,
|
||||
self.OnSyncCustomGunAttackStateEvent)
|
||||
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "ThrowCustomWeapon", self, self.OnThrowCustomWeapon)
|
||||
|
||||
def OnThrowCustomWeapon(self, args):
|
||||
playerId = args['playerId']
|
||||
power = args['power']
|
||||
|
||||
param = {
|
||||
'power' : 4 * power,
|
||||
'damage' : 3 + power * 3, # 最高伤害为 3+2*3=9 点伤害
|
||||
'direction': serverApi.GetDirFromRot(CompFactory.CreateRot(playerId).GetRot())
|
||||
}
|
||||
projectileComp = CompFactory.CreateProjectile(playerId)
|
||||
projectileEntityId = projectileComp.CreateProjectileEntity(playerId, 'tutorial:thrown_custom_throw_weapon', param)
|
||||
|
||||
if projectileEntityId != '-1':
|
||||
self._ReduceCarriedItemNum(playerId, 1)
|
||||
|
||||
def _ReduceCarriedItemNum(self, playerId, reduceNum):
|
||||
itemComp = CompFactory.CreateItem(playerId)
|
||||
selectSlotId = itemComp.GetSelectSlotId()
|
||||
itemDict = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY, selectSlotId)
|
||||
return itemComp.SetInvItemNum(selectSlotId, itemDict['count'] - reduceNum)
|
||||
```
|
||||
|
||||
### 进入游戏测试
|
||||
|
||||
一切准备好之后,就可以进入游戏测试了。然后你就得到了一个自定义的投掷类 3D 武器。
|
||||
|
||||
## 小结
|
||||
|
||||
我们这一节课是高度利用了原版的三叉戟物品的动画,如果你想要完全自定义的投掷动画,也可以参照自定义枪械那样,完全定制化动画。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 实现自己的类似于原版三叉戟的投掷类 3D 武器;
|
||||
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_5.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_5.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_6.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/1_6.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/2_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/2_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_5.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_5.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_6.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_6.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_7.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/3_7.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_4.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_4.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_5.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/assets/4_5.gif
LFS
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user