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

View File

@@ -0,0 +1,20 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章中,我们将一起更详细地学习**Molang**了解Molang在实体中的应用并一起在实体中实践Molang。
- 在第一节(*认识Molang*我们将一起认识Molang了解Molang语言的词法、结构和基本用法。
- 在第二节(*Molang在自定义实体中的常用场景*我们将一起来学习Molang在实体中的应用了解Molang的应用场景。
- 在第三节(*使用配置功能创建基础实体*我们将使用编辑器创建一个基础的松鼠实体作为Molang学习的演示实体。
- 在第四节(*自定义松鼠实体资源*我们将一起使用Molang自定义松鼠的资源。
- 在第五节(*自定义松鼠实体行为*)中,我们将一起定义松鼠的行为。
- 在第六节(*将实体动画与行为结合*)中,我们将把实体的动画和行为相结合,制作一个松鼠的攻击动画。
- 在第七节(*联动生物事件与行为*)中,我们将把实体的事件和行为相结合,制作一个逃跑机制。
- 在最后一节(*挑战:制作一辆卡丁车*)中,我们将进行一个挑战,制作一辆卡丁车。
本章关键词Molang 表达式 子表达式 简单表达式 复杂表达式 查询函数 数学函数 实体变量 临时变量 上下文变量 松鼠 变体 简谐振动 宿存 导航 移动 跳跃 AI意向 属性 特性 内置事件 触发器 过滤器 目标 卡丁车 偏航角 钳制 定位器

View File

@@ -0,0 +1,109 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 20分钟
---
# 认识Molang
**Molang**是一种基于表达式的类脚本语言旨在使用简单的类脚本语言在较低层次的系统中不脱离数据驱动实现复杂的行为。当然在脚本中我们依旧可以使用Molang这有助于我们轻松获取一些内部成员或旗标的值也可以实现和数据驱动的更复杂联动。Molang常用于实体的资源控制或世界生成器相关的操作中支持单一的简单表达式和多行的复杂表达式的运算。
## 基本概念
Molang在数据驱动的JSON文件中经常作为一个字段的值而出现这往往是一个字符串。字符串中的表达式便是Molang表达式。比如
```json
"some_field": "math.sin(query.anim_time * 1.23)"
```
其中`math.sin(query.anim_time * 1.23)`便是一个Molang表达式。根据国际版最新的概念标准一个Molang**表达式****Expression**指一个被引号包裹住的全部Molang语句的总和。如果一个表达式中存在多个语句那么每个语句都被称为一个**子表达式****Sub-expression**)。比如:
```cpp
temp.moo = math.sin(query.anim_time * 1.23);
temp.baa = math.cos(query.life_time + 2.0);
return temp.moo * temp.moo + temp.baa;
```
其中有三个语句两个赋值语句和一个返回语句。这三条语句的每条语句都被称为一个Molang子表达式。在最新的标准中我们不用“脚本”一词来称呼Molang。
通过上面的例子我们可以看出Molang表达式存在两种形态一种是只包含一个语句的表达式并且结尾不存在`;`作为“结束标识“,这种表达式称为**简单表达式****Simple Expression**)。另一种是包含多个子表达式(即多个语句)的表达式,每个子表达式的结尾必须存在一个`;`作为“结束标识“,这种表达式称为**复杂表达式****Complex Expression**。两种表达式均可作为Molang参数写在JSON字段的值中。复杂表达式在作为字符串值时所有的语句可以写在同一行中也可以换行且保留缩进类似于
```json
{
"some_field": "
temp.moo = math.sin(query.anim_time * 1.23);
temp.baa = math.cos(query.life_time + 2.0);
return temp.moo * temp.moo + temp.baa;
",
"some_other_field": "something"
// ...
}
```
我的世界可以解析这种换行的Molang字段也仅Molang字段可以如此这般地解析。如果某个字段只接受普通的字符串则引擎无法像上面的例子一样换行且保留缩进地解析。
每个Molang表达式都必须返回一个值。简单表达式将返回该语句本身计算的值。如果该语句是一个布尔校验则返回布尔校验的结果值`true`等价于`1.0``false`等价于`0.0`。如果该语句没有产生值,将返回`0.0`
如果是复杂表达式,则需要使用`return`的子表达式来返回值。如果不存在这种返回语句,则不论中间计算多么“激烈”,最后都将只返回一个`0.0`
## 运算符和关键字
Molang中有多种运算符和关键字下面列出了全部的运算符和关键字。值得注意的是除了字符串内的内容外其他地方Molang中的字母都是大小写不敏感的也就是说同一个字母的大写和小写没有任何分别。
| 运算符、关键字或字面量 | 描述 |
| --------------------------------- | ------------------------------------------------------------ |
| `1.23` | 一个数值常量值。 |
| `! && || < <= >= > == !=` | 逻辑运算符,分别是非、与、或、小于、小于或等于、大于或等于、大于、等于、不等于运算符。 |
| `* / + -` | 基本数学运算符,分别是乘、除、加、减运算符。 |
| `(` `)` | 用于控制表达式中的项求值用的圆括号,也用于带参查询函数的传参。 |
| `[` `]` | 用于访问数组的方括号。 |
| `{` `}` | 用于控制执行作用域的花括号。 |
| `??` | 空合并运算符,用于处理丢失的变量和过时的活动对象引用。 |
| `<test> ? <if true>` | 二元条件运算符。 |
| `<test> ? <if true> : <if false>` | 三元条件运算符。 |
| `->` | 箭头运算符,用于访问来自另一个不同实体的数据。 |
| `geometry.geometry_name` | 一个在实体定义文件中命名的几何的短名称引用。 |
| `material.material_name` | 一个在实体定义文件中命名的材质的短名称引用。 |
| `texture.texture_name` | 一个在实体定义文件中命名的纹理的短名称引用。 |
| `math.function_name` | 用于访问各种数学函数。 |
| `query.function_name` | 用于访问一个实体的属性。 |
| `variable.variable_name` | 读写一个活动对象的存储器。 |
| `temp.variable_name` | 读写暂时存储器。 |
| `context.variable_name` | 访问在某些场景下游戏提供的只读存储。 |
| `this` | 该表达式(在指定上下文的情况下)最终写入值的当前值。 |
| `return` | 为复杂表达式设计,这将计算紧跟着该关键词的语句并停止整个表达式的执行,返回计算出的值。 |
| `loop` | 用于重复执行一个或多个命令。 |
| `for_each` | 用于遍历迭代一个实体数组。 |
| `break` | 用于提前退出`loop``for_each`的作用域。 |
| `continue` | 用于跳过`loop``for_each`迭代的语句集的其余部分,并移动到下一次迭代。 |
上述运算符或关键字的详细用法可以参考[bedrock.dev上托管的Molang文档](https://bedrock.dev/zh/b/Molang)。
## 查询函数
在上面列出的关键字中,存在一种比较特殊的变量类型,被称为**查询函数****Query Function**),它的语法是`query.function_name`,其中`function_name`为某个查询函数名。顾名思义,查询函数是用于查询一个属性的值的函数。包括**全局参数****Global Parameter**)、**实体成员****Entity Member**)、**实体旗标****Entity Flag**)在内的各种各样的值都可以被查询函数所查询。
查询函数分为**无参查询函数**和**带参查询函数**。无参查询函数就是不具有参数表的查询函数。对于这种查询函数,直接写出其函数名即可获得对应属性的返回。比如本节最开头例子中的`query.anim_time`,便是用来查询一个全局参数`anim_time`的无参查询函数。
带参查询函数指的是比如先传入一些参数作为基础来查询特定的值的查询函数。这样的查询函数末尾需要紧跟一对圆括号(`(` `)`),然后在圆括号内写入参数表。比如下面的例子中的`query.get_nearby_entities`函数,他的第一个参数接受一个数字值,第二个参数接受一个字符串。
```cpp
v.x = 0;
for_each(v.pig, query.get_nearby_entities(4, 'minecraft:pig'), {
v.x = v.x + v.pig->query.get_relative_block_state(0, 1, 0, 'flammable');
});
```
具体的查询函数列表依旧可以在[bedrock.dev上托管的Molang文档](https://bedrock.dev/zh/b/Molang)中找到。在版本列表中选择与当前我的世界中国版相吻合的版本即可查看相对应版本的查询函数列表。
## 自定义变量
Molang中具备各种**变量****Variable**。变量可以用来存储一个值以供后续访问。Molang的变量存在三种类型分别是实体变量、临时变量和上下文变量。
**实体变量****Entity Variable**是一种存储在实体上的变量这里的实体指的是广义的实体指的是ECS框架中的实体包括了方块、物品、粒子、世界生成器等。实体变量的生存周期与实体相一致当实体在内存中被销毁时比如实体在世界中消失它的实体变量也将随之销毁而变得无法访问。这种变量我们使用`variable.variable_name`语法定义。
**临时变量****Temp Variable**)是一种存储在暂存器中的变量,这种变量在他们所定义的作用域中是有效的,在作用域的运算结束后将被销毁。不过,由于一些缺陷,目前的临时变量生命周期依然是全局的,所以在给临时变量命名时格外小心。这种变量使用`temp.variable_name`语法定义。
**上下文变量****Context Variable**)是一种只读变量,只能由硬编码的游戏引擎定义。这种变量是依赖于游戏运行状态的上下文而存在的。比如在铁砧上下文中,存在`context.other`代表铁砧的第二个输入槽位。如果脱离了铁砧的环境,将不存在该变量。这种变量使用`context.variable_name`语法定义。
开发者可以灵活地自定义实体变量和临时变量,在合适的实机改写或访问各种变量的值,从而使开发事倍功半。

View File

@@ -0,0 +1,96 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 10分钟
---
# Molang在自定义实体中的常用场景
Molang在附加包各处都有应用但是如果要说应用次数最多的场景那当自定义实体莫属。本节中我们一起来了解Molang在自定义实体中的应用。
## 控制实体渲染
在第八章中我们便了解了实体的渲染控制器。在实体的渲染控制器中我们可以通过Molang来控制实体的几何、材质和纹理的选用。一般来说我们存在两种比较常见的控制方法。第一种是使用数组第二种是使用三元条件运算符`?:`
```json
"geometry": "array.crossbow_geo_frames[query.get_animation_frame]",
"materials": [
{ "*": "variable.is_enchanted ? material.enchanted : material.default" }
],
"textures": [
"array.crossbow_texture_frames[query.get_animation_frame]",
"texture.enchanted"
]
```
在上面的例子中,我们可以看到几何和纹理都是使用了数组,通过`query.get_animation_frame`查询的值作为数组下标索来得到对应的几何和纹理。而材质则是使用了三元条件运算符,通过对变量`variable.is_enchanted`的值进行布尔校验来决定是使用附魔的材质和默认的材质。
## 参与动画表现力
我们之前便了解到动画中也可以使用Molang表达式来控制一个骨骼的某个通道的值。比如我们之前水鸭的移动动画。
```json
"animation.teal.move": {
"loop": true,
"anim_time_update": "query.modified_distance_moved",
"bones": {
"leg0": {
"rotation": ["math.cos(query.anim_time*38.17)*80.0", 0, 0]
},
"leg1": {
"rotation": ["math.cos(query.anim_time*38.17)*-80.0", 0, 0]
}
}
}
```
我们可以看到动画中使用了$\mathrm{cos}(query.anim\_time \times38.17)\times80.0$和$\mathrm{cos}(query.anim\_time\times38.17)\times-80.0$分别控制了`leg0``leg1`在$yOz$平面的旋转角度。`query.anim_time`是当前动画的播放时间默认单位为秒s不过我们这里使用了`anim_time_update`字段更改了动画时间流逝速度,因此变为了使用移动速度来控制动画流速。且需要注意的是,`math.cos`等三角数学函数只接受角度制的值作为输入,并输出对应的三角函数值。
## 行为与动画结合
我们可以通过在动画中调用一些Molang变量使得动画和行为相结合。当一个实体的行为包组件中定义了攻击的AI意向时我们便可以在其动画中访问到一个正确的`variable.attack_time`变量,这个变量对于非玩家实体代表着其攻击时间。当该实体不攻击时,则返回`0.0`。对于玩家来说则会返回一个攻击动画完成的百分比,取值为`0.0``1.0`
我们以僵尸的徒手攻击动画为例:
```json
"animation.zombie.attack_bare_hand" : {
"loop" : true,
"bones" : {
"leftarm" : {
"rotation" : [ "-90.0 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) - (math.sin(query.life_time * 76.776372) * 2.865) - this", "5.73 - ((math.sin(variable.attack_time * 180.0) * 57.3) * 0.6) - this", "math.cos(query.life_time * 103.13244) * -2.865 - 2.865 - this" ]
},
"rightarm" : {
"rotation" : [ "90.0 * (variable.is_brandishing_spear - 1.0) - ((math.sin(variable.attack_time * 180.0) * 57.3) * 1.2 - (math.sin((1.0 - (1.0 - variable.attack_time) * (1.0 - variable.attack_time)) * 180.0) * 57.3) * 0.4) + (math.sin(query.life_time * 76.776372) * 2.865) - this", "(math.sin(variable.attack_time * 180.0) * 57.3) * 0.6 - 5.73 - this", "math.cos(query.life_time * 103.13244) * 2.865 + 2.865 - this" ]
}
}
},
```
可以看到`variable.attack_time`作为一个类似于查询函数作用的变量传入了旋转通道的各个分量中。
## 行为包实体组件和事件
实体的行为包定义文件中常常会定义一些组件和事件。Molang可以用来控制组件中相关变量的计算和事件的触发。比如几乎所有的实体都会定义一个经验奖励组件用于计算掉落的经验值。
```json
"minecraft:experience_reward": {
"on_bred": "Math.Random(1,7)",
"on_death": "query.last_hit_by_player?Math.Random(1,3):0"
}
```
## 联动模组SDK
在模组SDK中可以使用`queryVariable`引擎组件来注册一个自定义查询函数。比如,以下代码便可以在客户端中注册一个`query.mod.is_custom_material`查询。
```python
import mod.client.extraClientApi as clientApi
compFactory = clientApi.GetEngineCompFactory()
# 注册一个自定义材质切换的查询函数
comp = compFactory.CreateQueryVariable(clientApi.GetLevelId())
result = comp.Register('query.mod.is_custom_material', 0.0)
# 更改该查询函数的值
comp.Set('query.mod.is_custom_material', 1.0)
```
之后我们便可以在动画控制器中用`query.mod.is_custom_material`来控制实体的材质,而`query.mod.is_custom_material`的值则可以由Python脚本来控制从而做到脚本使能资源控制。

View File

@@ -0,0 +1,107 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 使用配置功能创建基础实体
在接下来的几个小节中我们将一起制作一个松鼠的演示实体。我们将一起通过松鼠实体的制作来学习Molang的各种高级功能。
## 使用配置创建松鼠实体
我们打开编辑器创建一个新的AddOn组件将其命名为“松鼠演示实体”。然后打开编辑器将命名空间更改为`tutorial_demo`。预备工作完成后,我们开始使用配置功能新建实体。
![](./images/12.3_config_entity.png)
![](./images/12.3_squirrel_entity.png)
我们为松鼠进行命名,然后点击“创建”,便可以成功创建松鼠实体。创建结束后,我们可以打开松鼠的行为包和资源包定义文件来查看效果。
资源包定义文件:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel"
}
}
}
```
行为包定义文件:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
"minecraft:persistent": {
}
},
"events": {
}
}
}
```
我们可以看到,由于我们使用的是空白数据模板,所以这两个文件内容较为“干瘪”。
我们对资源包定义文件稍加补充,以便我们之后添加资源:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",/*
"materials": {
},
"textures": {
},
"geometry": {
},
"animations": {
},
"animation_controllers": [
],
"render_controllers": [
],
"spawn_egg": {
}*/
}
}
}
```
由于编辑器自动创建的是`1.8.0`的格式版本,所以动画和动画控制器是分开定义的。我们也为其创建`animation_controllers`数组,以便定义动画控制器。与`1.10.0`的格式版本不同,`1.8.0`的格式版本无法进行条件控制的动画播放,即没有`scripts/animate`字段。不过这不要紧,因为在`1.8.0`格式版本下,虽然动画本身不能直接通过实体的资源包定义文件播放,但是所有的动画控制器都是自动开始播放,我们只需要通过动画控制器来控制动画即可。
我们为松鼠添加一个默认的材质。因为我们希望松鼠的各方便表现得和兔子差不多,自然也希望松鼠像兔子一样渲染,所以我们为松鼠添加原版兔子默认的材质。
```json
"materials": {
"default": "rabbit"
}
```
这样,我们便创建了一个新的尚未添加模型和行为的实体,我们将在下面两节中集中完成这些工作。

View File

@@ -0,0 +1,674 @@
---
front: https://nie.res.netease.com/r/pic/20220408/710ecfce-57e5-4f5d-88b9-0eda8c2d1c45.gif
hard: 高级
time: 40分钟
selection: true
---
# 自定义松鼠实体资源
在本节中,我们将一起完成松鼠的资源部分。有关[松鼠模型](https://g79.gdl.netease.com/addonguide-12.zip)的资源可以点击链接下载。
## 挂接几何体和贴图纹理资源
类似以第八章中的步骤我们在Blockbench中创建松鼠模型的几何和纹理。
![](./images/12.4_squirrel_geometry.png)
在这里,我们创建了一个松鼠的几何,同时为这个几何创建了两套纹理,一套红色纹理,一套灰色纹理。我们希望在世界中生成的有些松鼠是红色松鼠,有些松鼠是灰色松鼠,因此两套纹理便可以满足我们的需求。
我们将松鼠的几何`squirrel.geo.json`导出到资源包的`models\entity`文件夹下,将纹理`red.png``gray.png`分别导出到资源包的`textures\entity\squirrel`文件夹下。我们展示一下松鼠导出的几何JSON文件
```json
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.squirrel",
"texture_width": 64,
"texture_height": 64,
"visible_bounds_width": 4,
"visible_bounds_height": 2,
"visible_bounds_offset": [0, 1, 0]
},
"bones": [
{
"name": "root",
"pivot": [0, 7, 3],
"rotation": [-20, 0, 0],
"mirror": true
},
{
"name": "body",
"parent": "root",
"pivot": [0, 5.83072, 3.3825],
"cubes": [
{"origin": [-3, 8.83072, 1.3825], "size": [6, 1, 7], "uv": [22, 0]},
{"origin": [-3, 3.83072, -1.6175], "size": [6, 5, 10], "uv": [0, 0]}
]
},
{
"name": "head",
"parent": "body",
"pivot": [0, 6.83072, -0.6175],
"rotation": [20, 0, 0],
"mirror": true,
"cubes": [
{"origin": [-2.5, 6.83072, -5.6175], "size": [5, 4, 5], "uv": [26, 26], "mirror": false},
{"origin": [-2.5, 10.83072, -4.6175], "size": [5, 1, 4], "uv": [40, 21], "mirror": false},
{"origin": [-1, 8.33072, -6.1175], "size": [2, 1, 1], "uv": [0, 19], "mirror": false}
]
},
{
"name": "bone",
"parent": "head",
"pivot": [0, 11.83072, -1.6175],
"rotation": [25, 0, 0],
"cubes": [
{"origin": [-2.5, 11.83072, -2.6175], "size": [1, 2, 2], "uv": [0, 15]},
{"origin": [1.5, 11.83072, -2.6175], "size": [1, 2, 2], "uv": [0, 15], "mirror": true}
]
},
{
"name": "frontLegLeft",
"parent": "body",
"pivot": [-3, 5.83072, 0.3825],
"mirror": true,
"cubes": [
{"origin": [-4, -1.16928, -0.6175], "size": [2, 7, 2], "uv": [0, 44], "mirror": false}
]
},
{
"name": "frontLegRight",
"parent": "body",
"pivot": [3, 5.83072, 0.3825],
"mirror": true,
"cubes": [
{"origin": [2, -1.16928, -0.6175], "size": [2, 7, 2], "uv": [0, 44]}
]
},
{
"name": "tail",
"parent": "body",
"pivot": [0, 5.83072, 7.3825],
"mirror": true,
"cubes": [
{"origin": [-1.5, 4.33072, 7.3825], "size": [3, 14, 3], "uv": [32, 8], "mirror": false},
{"origin": [-3.5, 7.33072, 10.3825], "size": [7, 12, 6], "uv": [0, 15], "mirror": false}
]
},
{
"name": "haunchRight",
"parent": "body",
"pivot": [3, 7.33072, 7.0825],
"rotation": [-20, 0, 0],
"mirror": true,
"cubes": [
{"origin": [-4, 3.92839, 4.80079], "size": [2, 5, 6], "uv": [0, 33], "mirror": false}
]
},
{
"name": "rearFootRight",
"parent": "haunchRight",
"pivot": [0, 3.92839, 9.80079],
"rotation": [40, 0, 0],
"mirror": true,
"cubes": [
{"origin": [-4, 2.42839, 4.10079], "size": [2, 2, 7], "inflate": 0.05, "uv": [16, 35], "mirror": false}
]
},
{
"name": "haunchLeft",
"parent": "body",
"pivot": [3, 7.33072, 7.0825],
"rotation": [-20, 0, 0],
"mirror": true,
"cubes": [
{"origin": [2, 3.92839, 4.80079], "size": [2, 5, 6], "uv": [0, 33]}
]
},
{
"name": "rearFootLeft",
"parent": "haunchLeft",
"pivot": [0, 3.92839, 9.80079],
"rotation": [40, 0, 0],
"mirror": true,
"cubes": [
{"origin": [2, 2.42839, 4.10079], "size": [2, 2, 7], "inflate": 0.05, "uv": [16, 35]}
]
}
]
}
]
}
```
然后我们就可以开始在资源包定义中挂接几何和纹理了。
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},/*
"animations": {
},
"animation_controllers": [
],
"render_controllers": [
],
"spawn_egg": {
}*/
}
}
}
```
这样,我们的纹理和几何便挂接完毕了,接下来我们需要使用渲染控制器来将材质、纹理和几何应用到实体上,同时为红色和灰色纹理制作单独的变体渲染。
## 使用渲染控制器制作变体
我们在资源包的`render_controllers`文件夹中创建`squirrel.render_controllers.json`文件。然后在先其中写入我们的纹理数组、几何和材质。
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.squirrel": {
"arrays": {
"textures": {
"Array.skins": [
"Texture.red",
"Texture.brown"
]
}
},
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [
// texture list
]
}
}
}
```
最重要的便是如何使用变体制作纹理。事实上,原版的兔子便提供了一种非常好的变体方案。那便是在行为包定义中使用`minecraft:variant`组件。`minecraft:variant`组件只接受一个整数值这个值可以作为实体的“变体ID”从而定义实体的一个**变体****Variant**。而实体的变体ID又可以通过Molang查询函数`query.variant`在客户端得到。所以我们可以使用该ID来引用纹理数组。我们假设我们已经在行为包中定义了红色的变体ID是0灰色的变体ID是1这是和我们的纹理数组索引相一致的定义。这样我们就可以使用`Array.skins[query.variant]`这样的Molang表达式来做到实时根据变体来变换纹理。补全的控制器如下
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.squirrel": {
"arrays": {
"textures": {
"Array.skins": [
"Texture.red",
"Texture.gray"
]
}
},
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [
"Array.skins[query.variant]"
]
}
}
}
```
我们将该控制器定义到实体资源包定义文件中,得到如下定义文件:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},/*
"animations": {
},
"animation_controllers": [
],*/
"render_controllers": [
"controller.render.squirrel"
],/*
"spawn_egg": {
}*/
}
}
}
```
## 自定义实体蛋贴图
接下来我们自定义实体的刷怪蛋贴图。我们可以仿照之前水鸭的定义,通过两个颜色**基色****Base Color**)和**覆盖色****Overlay Color**来来定义刷怪蛋的颜色。颜色使用的是16进制颜色码。开发者可以使用网上很多公开的颜色预览软件来预览颜色并使用。我们也可以使用Blockbench的Minecraft Entity Wizard插件来快速预览这是一种取巧的办法。
![](./images/12.4_squirrel_egg.png)
我们将刷怪蛋颜色添加到客户端定义文件中:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},/*
"animations": {
},
"animation_controllers": [
],*/
"render_controllers": [
"controller.render.squirrel"
],
"spawn_egg": {
"base_color":"#1778D2",
"overlay_color":"#1778D2"
}
}
}
}
```
![](./images/12.4_squirrel_static_in-game.png)
至此我们初步制作了一个“不会动”的松鼠。我们可以先进入游戏自测来查看这个松鼠的渲染情况了。接下来我们为松鼠制作动画。由于我们还没有为松鼠定义行为因此我们的动画目前还不能在游戏中预览因此我们打算采用Blockbench可视化地制作动画。
## 利用Molang制作闲置动画
![](./images/12.4_squirrel_blockbench_animation_mode.png)
我们再次打开我们之前制作的松鼠模型的Blockbench项目文件。点击右上角的“**动画模式**”进入动画制作模式。
![](./images/12.4_squirrel_blockbench_animation_mode_full_screen.png)
我们可以看到,整个界面的各个窗格发生了改变。左侧原来是纹理贴图的窗格变为了“**动画**”窗格,这里存放着我们为该实体添加的动画。动画窗格的下方是“**关键帧**”窗格和“**变量占位符**”窗格。关键帧窗格将显示当前通道当前关键帧的各个轴向的值。而变量占位符则是用于定义一些只有在游戏内才能获取的变量或查询的“占位”值。因为这些变量或查询是需要在游戏中有上下文的而在Blockbench中是没有值的。为了预览效果我们需要在这里给他们定义值。最下方是动画的**时间轴**,我们可以通过时间轴进行调整时间、预览动画等操作。
![](./images/12.4_squirrel_create.png)
我们在动画窗格中右键,点击“**添加动画**”。
![](./images/12.4_squirrel_idle_create.png)
然后将名称改为`animation.squirrel.idle`,循环改为“循环播放”。因为我们希望我们的`idle`动画在松鼠的闲置AI意向触发时持续播放所以我们使用循环播放。这等价于在对应的JSON文件中为其添加`"loop": true`。这样我们就添加了一个新动画。
![](./images/12.4_squirrel_save.png)
![](./images/12.4_squirrel_save_as.png)
为了防止我们的制作成果丢失我们先及时将其保存为JSON文件。点击文件上的保存按钮并定位到我们的资源包中的`animations`文件夹中,然后在另存为对话框中点击“**保存(S)**”。
![](./images/12.4_squirrel_idle_start.png)
下面,我们开始制作待机动画。我们希望松鼠在待机时整体身子轻微前后移动。所以我们在右侧先选中`body`骨骼组。
![](./images/12.4_squirrel_body_selected.png)
我们可以看到,此时预览窗中`body`的轮廓已经显示出来了,同时下方时间轴中加入了`body`的三个通道。由于我们希望其仅仅是前后摇动,所以我们目前只关注“**旋转**”通道。
![](./images/12.4_squirrel_add_key_frame_rotation.png)
我们点击旋转通道右侧的“**+**”按钮,这个按钮可以在当前时间轴的位置上添加一个关键帧。由于我们的时间轴目前停留在`0.0`的初始位置,此时如果只在此处加入一个关键帧就相当于之前我们讲过的不使用关键帧的情形。其实这种情形就是等价于只在`0.0`处有一个关键帧。我们在这个关键帧中加入Molang表达式同时由于这是循环动画这样就能保证Molang表达式每帧进行运算并将每帧算出的不同的值应用到实体上而实体就会根据Molang运算的结果进行移动。
![](./images/12.4_squirrel_key_frame.png)
加入关键帧后我们可以在时间轴上发现一个菱形标记。这意味着一个关键帧。同时我们可以在左侧“关键帧”窗格中看到当前的值。此时松鼠上的坐标轴变为球形坐标轴。我们可以看到我们希望的”前后摇摆“动画可以由球形坐标轴中红色代表的面进行旋转而得到。而我们又可以在“关键帧”窗格中定位到红色这是X轴。所以我们在X轴上进行Molang撰写。事实上X轴是模型空间的东西方向所以X轴的垂面便是我们在预览窗中看到的红色的面。
那么我们到底如何才能通过Molang实现身体随着时间的流逝前后摇摆呢这需要通过Molang的查询函数`query.anim_time`来做到。`query.anim_time`是一个全局参数代表着一个动画自开始播放以来过去的时间单位为秒s。那么我们来回忆以时间为自变量什么函数能够做到“来回摆动”呢我们可能很快能想到单摆、弹簧、简谐运动。没错我们只需要像简谐运动那样使用一个`cos``sin`函数来控制,就可以做到随着时间骨骼在一定方向上来回的摆动。
对于“旋转”通道来说,就是摆动的角度随着时间做简谐振动;对于“位置”通道来说,就是骨骼在某个轴向上的位移随着时间做简谐振动。简谐振动公式为:
$$
x=A\mathrm{cos}(\omega t+\varphi)+x_0
$$
其中,$A$为**振幅**,即摆动或振动的幅度,其值越大,骨骼就会“越摆”或者“越振”。$\omega$是振动的**角速度**或**角频率**,代表摆动或振动的快慢。$\varphi$代表振动的初相位,即在振动的一个循环中位于哪个初始位置。$x_0$代表初位移,一般来说,我们使用余弦函数时,不指定初相位的情况下,$t$为0时就会出现最后的计算值为1或者为其振幅$A$的情况,此时代表着动画从振动的一个端点开始,而非原点开始。如果我们希望动画从静止状态开始,即从原点开始,那么就必须令$x_0=-A$,比如,使用$x=A\mathrm{cos}(\omega t)-A$这种表达式。
在我的世界中,`query.anim_time`便代表时间$t$,我们使用`math.cos`来做到余弦函数的计算。比如我们在Blockbench中使用这种表达
![](./images/12.4_squirrel_molang.png)
这代表着$x=5\mathrm{cos}(450t)$。当然其中的角速度450是我们摸索出来的比较合适的速度。你可以通过时间轴上的播放按钮来播放预览然后根据自己的感觉再修改这里的值。
![](./images/12.4_squirrel_play.png)
![](./images/12.4_molang_playing.gif)
依据这个原理,我们为所有骨骼添加`0.0`的关键帧并在X轴向上添加简谐运动的动画。注意为了保持所有的骨骼运动频率一致我们需要所有骨骼全部使用相同的角速度否则运动着运动着就会出现“四肢不协调”的丑态。
![](./images/12.4_idle_anim.gif)
我们同时将其完成后的JSON展示如下
```json
{
"format_version": "1.8.0",
"animations": {
"animation.squirrel.idle": {
"loop": true,
"bones": {
"tail": {
"rotation": ["math.sin(query.anim_time * 450) * 15", 0, 0]
},
"head": {
"rotation": ["math.sin(query.anim_time * 450) * 15", 0, 0]
},
"frontLegLeft": {
"rotation": ["math.cos(query.anim_time * 450) * 65 - 65", 0, 0]
},
"frontLegRight": {
"rotation": ["math.cos(query.anim_time * 450) * 45 - 45", 0, 0]
},
"body": {
"rotation": ["math.cos(query.anim_time * 450) * 5", 0, 0]
}
}
}
}
}
```
事实上在编辑动画的过程中我们可以在时间轴中点击左上角的“切换图像编辑器”以打开函数图像视图。这个视图会渲染我们的Molang表达式所运算的函数图像这将更有助于我们理解动画的过程。
![](./images/12.4_squirrel_graph.png)
而且,如果开发者有充足的数学功底,我们还可以使用多个简谐运动叠加成一个更复杂的运动,这被称为傅里叶级数的叠加。通过傅里叶叠加的运动将更加真实,不过,也将涉及到更多的数学技巧,所以这不属于我们今天的讨论范围。
## 利用Molang制作移动动画
移动动画和闲置动画原理是一致的,不过,除了各个骨骼的旋转之外,这里统控所有骨骼的`body`骨骼组需要进行一个平移。也就是说,我们需要修改`body`的位置通道。
![](./images/12.4_squirrel_move.png)
由于我们是要前后移动而X轴代表东西反向也就是松鼠的左右所以我们的X轴不懂而其余两个轴以相同的频率前后或上下振动。
![](./images/12.4_move_body.gif)
在整体平移的过程中我们再为其他各个骨骼添加同角频率旋转效果。因此,我们得到了一个移动的动画,我们命名为`move`并放置到同一个JSON文件中。
![](./images/12.4_move_all.gif)
我们查看JSON文件中该动画的部
```json
{
"format_version": "1.8.0",
"animations": {
"animation.squirrel.idle": {
// ...
},
"animation.squirrel.move": {
"loop": true,
"bones": {
"body": {
"position": [0, "math.cos(query.anim_time * 500) * -1.4 + 1.4", "math.cos(query.anim_time * 500) * 3 - 3"]
},
"head": {
"rotation": ["math.sin(query.anim_time * 500) * 10", 0, 0]
},
"haunchLeft": {
"rotation": ["math.cos(query.anim_time * 500) * -30 + 30", 0, 0]
},
"rearFootLeft": {
"rotation": ["math.cos(query.anim_time * 500) * -15 + 15", 0, 0]
},
"frontLegLeft": {
"rotation": ["math.cos(query.anim_time * 500) * 20 - 20", 0, 0]
},
"frontLegRight": {
"rotation": ["math.cos(query.anim_time * 500) * 35 - 35", 0, 0]
},
"tail": {
"rotation": ["math.sin(query.anim_time * 500) * 20", 0, 0]
},
"haunchRight": {
"rotation": ["math.cos(query.anim_time * 500) * -35 + 35", 0, 0]
},
"rearFootRight": {
"rotation": ["math.cos(query.anim_time * 500) * -17.5 + 17.5", 0, 0]
}
}
}
}
}
```
## 利用Molang制作头部旋转动画
我们知道原版的很多实体在玩家靠近时都会看向玩家这是由于实体具备看向玩家的AI意向同时具备一个`look_at_target`的动画配合完成的。那么,我们如何制作看向玩家的动画呢?这需要用到`query.target_x_rotation``query.target_y_rotation`。这两个Molang变量代表实体看向玩家旋转的角度因此我们只需要将实体的`head`骨骼的旋转通道设定为这两个查询函数即可。
![](./images/12.4_squirrel_look_at_player.png)
```json
{
"format_version": "1.8.0",
"animations": {
"animation.squirrel.idle": {
// ...
},
"animation.squirrel.move": {
// ...
},
"animation.squirrel.look_at_target": {
"loop": true,
"bones": {
"head": {
"rotation": ["query.target_x_rotation", "query.target_y_rotation", 0]
}
}
}
}
}
```
## 将动画挂接到实体上
由于`1.8.0`的实体客户端格式版本无法做到直接条件控制播放动画,我们使用动画控制器来控制动画的播放。我们在资源包的`animation_controllers`文件夹下新建一个`squirrel.animation_controllers.json`文件,用于控制实体的动画播放。
在编辑动画控制器之前,我们先定义动画的短名称到实体客户端定义文件中。
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},
"animations": {
"move": "animation.squirrel.move",
"idle": "animation.squirrel.idle",
"look_at_target": "animation.squirrel.look_at_target"
},/*
"animation_controllers": [
],*/
"render_controllers": [
"controller.render.squirrel"
],
"spawn_egg": {
"base_color":"#1778D2",
"overlay_color":"#1778D2"
}
}
}
}
```
然后我们便可以在动画控制器中写入动画的播放条件了,比如,我们按照如下格式来书写:
```json
{
"format_version" : "1.10.0",
"animation_controllers" : {
"controller.animation.squirrel.general" : {
"initial_state" : "default",
"states" : {
"default" : {
"variables": {
"move_speed": {
"input": "query.modified_move_speed",
"remap_curve": {
"0.01": 0.0,
"0.1": 1.0
}
},
"is_idling": {
"input": "query.modified_move_speed",
"remap_curve": {
"0.0": 1.0,
"0.01": 0.0
}
}
},
"animations" : [
{
"idle" : "variable.is_idling && query.is_on_ground"
},
"look_at_target",
{
"move" : "!query.is_on_ground || variable.move_speed"
}
]
}
}
}
}
}
```
其中`variables`字段用于定义后面可能会用到的变量。这里我们使用了`remap_curve`字段,即**曲线重映射****Curve Remap**)功能。根据`query.modified_move_speed`的值当其在0.01到0.1之间变动时`variable.move_speed`将线性重映射到0.0至1.0之间低于0.01则重映射到0.0大于0.1将重映射到1.0同时当其在0.0到0.01之间变动时,`variable.is_idling`将线性重映射到1.0至0.0之间当其大于0.01时将始终重映射到0.0。
`"idle" : "variable.is_idling && query.is_on_ground"``"move" : "!query.is_on_ground || variable.move_speed"`的意思便是冒号后面两个Molang表达式的值分别传入`idle``move`动画,作为`query.anim_time`的流逝速度取值来源。本来`query.anim_time`的取值来源是计算机的时间计时器流逝速度,但是一旦更改为我们修改过的速度,比如`!query.is_on_ground || variable.move_speed`,由于`||`的短路性,这便相当于`query.anim_time`变成了实体在生成后走过的实际距离,同时只有在实体离地时`query.anim_time`才会流逝。这是更加合理的。
我们将动画控制器也挂接在实体上:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},
"animations": {
"move": "animation.squirrel.move",
"idle": "animation.squirrel.idle",
"look_at_target": "animation.squirrel.look_at_target"
},
"animation_controllers": [
{ "general": "controller.animation.squirrel.general" },
{ "move": "controller.animation.squirrel.move" }
],
"render_controllers": [
"controller.render.squirrel"
],
"spawn_egg": {
"base_color":"#1778D2",
"overlay_color":"#1778D2"
}
}
}
}
```
至此,我们完成了基本的资源控制,最后,为了后续讲述方便,我们再为实体添加一个功能。
## 为松鼠打开装备渲染
我们让松鼠具备穿戴**附着物****Attachable*****挂件***)的功能,这样我们便可以在行为组件中使用装备组件为其添加装备,比如一个头盔。注意,各种装备的挂接骨骼名称都是硬编码的,比如头盔只能挂接在`head`骨骼上。如果你的实体几何中没有`head`骨骼,那么挂接了头盔的附着物也无法正常显示。
我们使用`enable_attachables`启用附着物:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},
"animations": {
"move": "animation.squirrel.move",
"idle": "animation.squirrel.idle",
"look_at_target": "animation.common.look_at_target"
},
"animation_controllers": [
{ "general": "controller.animation.squirrel.general" }
],
"render_controllers": [
"controller.render.squirrel"
],
"spawn_egg": {
"base_color":"#1778D2",
"overlay_color":"#1778D2"
},
"enable_attachables": true
}
}
}
```
接下来,我们便需要将关注点从资源包转移到行为包了。毕竟,我们的松鼠目前还是没有任何行为,如同一个“呆鼠”,我们需要使用行为包为其赋予生命活力!

View File

@@ -0,0 +1,669 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 30分钟
---
# 自定义松鼠实体行为
下面,我们一起来制作松鼠的行为。我们打开松鼠的行为包定义文件,依次向其中加入一些必要的行为组件。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
"minecraft:persistent": {
}
},
"events": {
}
}
}
```
我们可以看到,已经有一个组件随着编辑器创建而加入了,即`minecraft:persistent``minecraft:persistent`组件用于控制生物是否是**宿存的****Persistent*****持久化保存***)。宿存的生物不会因距离过远而被引擎销毁。事实上,我们是不需要这个组件的。因为这不是类似于画之类的必须宿存的实体,因而其存在只会徒增引擎负担,所以我们将其删去。
## 添加组件
下面我们开始添加一系列组件。在这里,我们仅仅为了演示,所以并不打算介绍所有的组件,也不欲纠结于一个生物的组件搭配是否非常完美。
### 添加常规组件
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
"minecraft:hurt_on_condition": {
"damage_conditions": [
{
"filters": {
"test": "in_lava",
"subject": "self",
"operator": "==",
"value": true
},
"cause": "lava",
"damage_per_tick": 4
}
]
},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"minecraft:experience_reward": {
"on_death": "query.last_hit_by_player ? Math.Random(0,1) : 0"
},
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": 0
},
"minecraft:physics": {}
},
"events": {
}
}
}
```
我们添加一些大部分生物都应该具有的组件。`minecraft:hurt_on_condition`是依照条件收到伤害,这里我们设置为在熔岩中会受到伤害。我们知道在熔岩中我们会同时受到两种伤害,一个是火焰灼伤,一个是熔岩灼伤,这里便控制着熔岩的灼伤。`minecraft:pushable`代表着我们的实体是否会被推动。我们把被实体推动和被活塞推动全部设置为`true``minecraft:experience_reward`代表击杀时掉落的经验奖励Molang表达式用于计算掉落的经验值。`minecraft:breathable`为实体在水中的可呼吸性。`minecraft:physics`代表该实体受到物理引擎的影响。
### 添加导航组件
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": 0
},
"minecraft:physics": {},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"avoid_water": true
}
},
"events": {
}
}
}
```
我们加入一个`navigation.`前缀的组件,这类组件被称为导航组件。每个实体都必须有一个导航组件才能正常运行其寻路算法。不同的导航组件将执行不同的寻路逻辑。实体不可以拥有多个导航组件,即每个实体只能拥有一种导航组件。这里的`minecraft:navigation.walk`组件意味着实体使用“步行”的寻路算法。
### 添加移动组件
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:physics": {},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"avoid_water": true
},
"minecraft:movement.skip": {}
},
"events": {
}
}
}
```
我们接着添加一个`movement.`前缀的组件,这类组件称为移动组件。移动组件控制着实体“如何进行移动”。这里的移动并不是指如何寻路,而是如何“沿着寻路算法给出的路径到达目的地”。说白了,就是实体“走路”的方式,只不过这里的“走路”不一定真的指代行走。当然,如果实体缺失了该类型的组件,它将不具备移动的能力。这里我们期待松鼠采用兔子一样边走边跳跃的方式来“走路”,即“跳着走”。所以我们使用了`minecraft:movement.skip`。注意,史莱姆和岩浆怪采用了另一种移动方式组件,被称为`minecraft:movement.jump`,这代表只用跳跃来走路,和兔子的“蹿行”的跳跃方式是不同的。
### 添加跳跃组件
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:navigation.walk": {
"can_path_over_water": true,
"avoid_water": true
},
"minecraft:movement.skip": {},
"minecraft:jump.static": {}
},
"events": {
}
}
}
```
还有一种组件是以`jump.`为前缀的组件,我们称为跳跃组件。跳跃组件即控制实体跳跃的方式。注意,这种组件仅仅是控制实体跳跃的方式,并不控制实体何时跳跃,也不控制实体是否在跳跃。但是,如果缺失该组件,实体将不具备跳跃的能力。目前只有两种跳跃组件,一种是静态跳跃`minecraft:jump.static`,代表每次跃起都保持完全一致,另一种是动态跳跃`minecraft:jump.dynamic`,代表每次跃起可能会根据实体的速度修饰符进行改变,比如跃起的高度将随速度发生变化。
### 添加AI意向组件
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:movement.skip": {},
"minecraft:jump.static": {},
"minecraft:behavior.float": {
"priority": 0
},
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"speed_multiplier": 0.8,
"xz_dist": 2,
"y_dist": 1
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
}
},
"events": {
}
}
}
```
接下来,我们添加实体的**AI意向****AI Goal**。实体的AI意向全都以`behavior.`为前缀代表实体对某种类型的动作的“钟意”程度。每种AI意向都有一个**优先级****Priority**优先级是一个整数。当意向之间发生冲突时优先级的值越低的AI意向越可能优先执行即**优先级的值越低代表着优先级越高**。我们为这个实体加入了一个优先级最高的浮动意向`minecraft:behavior.float`,这代表只要实体在水中,一定会优先在水面上浮动。然后是一个由伤害锁定目标的意向`minecraft:behavior.hurt_by_target`,这意味着只要有实体对松鼠造成伤害,松鼠就会将其锁定为自己的目标。然后添加了一个优先级并不那么高的随意漫步的意向`minecraft:behavior.random_stroll`。接着添加了一个`minecraft:behavior.random_look_around`用于随机向四周看。最后添加了一个优先级最低的`minecraft:behavior.look_at_player`,用于看向玩家。
### 添加属性组件
**属性****Property**)组件是一种特殊的组件,一般用于控制一个实体实例的属性,往往和实体在内存中存在的结构体中的一些属性相对应,代表着一个实体具备的“能力”或“性质”。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
},
"minecraft:type_family": {
"family":["squirrel", "mob"]
},
"minecraft:can_climb": {},
"minecraft:collision_box": {
"width": 0.7,
"height": 0.7
}
},
"events": {
}
}
}
```
属性组件一般没有特殊的前缀,所以不易与常规组件区分。我们添加了三个属性组件。`minecraft:type_family`代表该实体的类型的族的属性,常常和命令相配合。`minecraft:can_climb`代表该实体具备攀爬属性,遇到梯子时该实体能够登梯而上。`minecraft:collision_box`定义了该实体的碰撞箱属性,用于支持该实体的碰撞。
### 添加特性组件
**特性****Attribute**组件是另一种特殊的组件往往用于指定实体所具备的特定性质的或进行特定操作时该操作的“数值”比如攻击数值、生命值、具备的护甲值等。特性是和实体NBT中的`Attributes`列表相绑定的,定义的特性组件最终都会作用到这个实体的`Attributes`列表中定义的特性中。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
},
"components": {
// ...
"minecraft:can_climb": {},
"minecraft:collision_box": {
"width": 0.7,
"height": 0.7
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:health": {
"value": 30,
"max": 30
}
},
"events": {
}
}
}
```
特性组件一般也没有特殊的前缀。这里我们添加了两个特性组件`minecraft:movement``minecraft:health`,分别用于定义实体的基础移动速度的值、基础和最大的生命值。
至此,我们便完成了松鼠的组件定义,我们可以进入游戏查看我们的行为效果。
![](./images/12.5_behavior_in-game.gif)
### 使用组件组和事件来制作变体
到此为止,我们已经为松鼠添加了组件。事实上,这时候的松鼠已经可以正常表现了。但是,我们依然记得我们曾经为松鼠绘制了灰色的纹理贴图。我们现在使用组件组和事件来控制灰色纹理变体。
#### 添加变体组件组
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"tutorial_demo:color_red": {
"minecraft:variant": {
"value": 0
}
},
"tutorial_demo:color_gray": {
"minecraft:variant": {
"value": 1
}
}
},
"components": {
// ...
},
"events": {
}
}
}
```
我们使用`minecraft:variant`组件来添加变体组件组。`minecraft:variant`组件是一个专门用于控制变体的组件。在游戏中,它定义的值可以同步到客户端,因此,我们可以在客户端用`query.variant`获取到它的值。
我们定义两个组件组,一个` tutorial_demo:color_red`变体ID为0另一个` tutorial_demo:color_gray`变体ID为1。不管哪个组件组当它被加入到全局组件中时都会带入一个`minecraft:variant`组件,用于确定`query.variant`获取到的值。
#### 定义变体生成事件
我们希望定义内置事件`minecraft:entity_spawned`。这个事件会在松鼠自然生成或者刷怪蛋生成时被引擎自主触发。因此我们在这个事件中分别定义生成两种变体的几率。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"minecraft:color_red": {
"minecraft:variant": {
"value": 0
}
},
"minecraft:color_gray": {
"minecraft:variant": {
"value": 1
}
}
},
"components": {
// ...
},
"events": {
"minecraft:entity_spawned": {
"sequence": [
{
"randomize": [
{
"weight": 1,
"add": {
"component_groups": [
" tutorial_demo:color_red"
]
}
},
{
"weight": 1,
"add": {
"component_groups": [
" tutorial_demo:color_gray"
]
}
}
]
}
]
}
}
}
}
```
我们将两种松鼠的权重全部设为1这样可以做到两种松鼠1:1生成。
![](./images/12.5_one_to_one.png)
## 增加松鼠的自然生成
我们自定义了新生物之后,就需要为其写入除了刷怪蛋生成之外新的自然生成机制。否则,玩家在生存模式下边无法体验到实体的生成。自然生成机制需要使用我们的生成规则定义文件来制作。我们在行为包中新建一个`spawn_rules`文件夹并在其中新建一个`squirrel.json`文件,作为我们的生成规则定义文件。
```json
{
"format_version": "1.8.0",
"minecraft:spawn_rules": {
"description": {
"identifier": "tutorial_demo:squirrel",
"population_control": "animal"
},
"conditions": [
{
"minecraft:spawns_on_surface": {},
"minecraft:brightness_filter": {
"min": 0,
"max": 15,
"adjust_for_weather": true
},
"minecraft:weight": {
"default": 12
},
"minecraft:herd": {
"min_size": 8,
"max_size": 12
},
"minecraft:biome_filter": [
{"test": "has_biome_tag", "operator":"==", "value": "giant"}
]
}
]
}
}
```
我们希望其生成于带有`giant`标签的生物群系的地表,并且成兽群生成,以`animal`的方式控制种群数目,并根据天气调整生成比率。我们便可以编写上述内容,然后将其写入到我们的生成规则定义文件中。
## 添加装备组件
还记得我们之前为我们的松鼠开启了附着物么?我们现在为松鼠添加一个头盔装备。为此,我们需要在行为包中使用`minecraft:equipment`组件。`minecraft:equipment`组件接受一个战利品表,为此,我们需要制作一个只有头盔的战利品表。我们不妨制作一个只有锁链头盔的战利品表。
在行为包中找到`loot_tables`文件夹并在其中新建一个`entities`文件夹,然后在其中创建`squirrel_equipment.json`文件,作为我们战利品表。我们向其中写入如下内容以代表只有一个锁链头盔。
```json
{
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "item",
"name": "minecraft:chainmail_helmet",
"weight": 1
}
]
}
]
}
```
然后,我们在实体行为包定义中加入`minecraft:equipment``minecraft:equipment`是一个属性组件,我们将其和`minecraft:type_family``minecraft:can_climb``minecraft:collision_box`放置在一起,方便我们之后调试。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
// ...
},
"components": {
// ...
"minecraft:behavior.look_at_player": {
"priority": 11
},
"minecraft:equipment": {
"table": "loot_tables/entities/squirrel_equipment.json"
},
"minecraft:type_family": {
"family":["squirrel", "mob"]
},
"minecraft:can_climb": {},
"minecraft:collision_box": {
"width": 0.7,
"height": 0.7
},
"minecraft:movement": {
"value": 0.3
}
// ...
},
"events": {
// ...
}
}
}
```
这些都完成以后,我们便可以进入游戏自测了。
![](./images/12.5_armor_in-game.png)
可以看到,我们的松鼠现在都带有一个头盔了!当然,头盔有点“不合身”,不过这不要紧,这只是一个演示。在实际操作中,我们可以通过自定义附着物的方式自定义一个头盔。在第九章的挑战中,我们就制作过自定义盔甲。参考那个步骤,稍微调整模型的大小,便可以依据松鼠的“头型”打造出量身定制的头盔了。作为练习,这一要点就交给开发者们自行操作了。
最后,我们将截止到目前我们已经在行为包定义文件中编写的内容做一个展示,方便开发者们宏观地感受一个带有相对完整的组件、组件组和事件的实体:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"minecraft:color_red": {
"minecraft:variant": {
"value": 0
}
},
"minecraft:color_gray": {
"minecraft:variant": {
"value": 1
}
}
},
"components": {
"minecraft:hurt_on_condition": {
"damage_conditions": [
{
"filters": {
"test": "in_lava",
"subject": "self",
"operator": "==",
"value": true
},
"cause": "lava",
"damage_per_tick": 4
}
]
},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"minecraft:experience_reward": {
"on_death": "query.last_hit_by_player ? Math.Random(0,1) : 0"
},
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": 0
},
"minecraft:physics": {},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"avoid_water": true
},
"minecraft:movement.skip": {},
"minecraft:jump.static": {},
"minecraft:behavior.float": {
"priority": 0
},
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"speed_multiplier": 0.8,
"xz_dist": 2,
"y_dist": 1
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
},
"minecraft:equipment": {
"table": "loot_tables/entities/squirrel_equipment.json"
},
"minecraft:type_family": {
"family":["squirrel", "mob"]
},
"minecraft:can_climb": {},
"minecraft:collision_box": {
"width": 0.7,
"height": 0.7
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:health": {
"value": 30,
"max": 30
}
},
"events": {
"minecraft:entity_spawned": {
"sequence": [
{
"randomize": [
{
"weight": 1,
"add": {
"component_groups": [
"minecraft:color_red"
]
}
},
{
"weight": 1,
"add": {
"component_groups": [
"minecraft:color_gray"
]
}
}
]
}
]
}
}
}
}
```

View File

@@ -0,0 +1,299 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 30分钟
---
# 将实体动画与行为结合
在实体的行为包组件中,有些组件可以做到和实体的渲染帧相结合。本节我们以实体的攻击组件为例,探索如何将实体动画与行为相结合。
## 利用关键帧动画制作攻击动画
与之前几个动画使用Molang不同我们现在使用关键帧制作一个攻击动画。我们在Blockbench中切换到动画模式新添加一个名为`attack`的动画。
![](./images/12.6_key_frame_1.png)
我们像之前一样先为整个body制作一个向前平移的动画然后再在各个骨骼上制作旋转动画。我们可以使用“+”按钮多添加几个关键帧。
![](./images/12.6_key_frame_2.png)
与使用Molang不同这里我们直接指定该关键帧处的值。我们可以定位到该关键帧后拉动预览窗中的坐标轴来实时改变某个轴向上的值。在拉动坐标轴的时候左侧关键帧窗格里的值会随之改变。
![](./images/12.6_key_frame_3.png)
![](./images/12.6_body.gif)
最后我们添加一个关键帧用于骨骼的位置归位,这是为了使该攻击和下一次攻击相衔接。接下来我们依照相同的原理制作其他骨骼的旋转。
![](./images/12.6_all.gif)
我们可以看到经过一段时间的制作我们的松鼠具备了基本的攻击动画。我们将该动画保存以备之后制作行为时联动使用。下面是保存之后的动画JSON文件
```json
{
"format_version": "1.8.0",
"animations": {
"animation.squirrel.idle": {
// ...
},
"animation.squirrel.move": {
// ...
},
"animation.squirrel.look_at_target": {
// ...
},
"animation.squirrel.attack": {
"animation_length": 0.54167,
"bones": {
"body": {
"position": {
"0.0": [0, 0, 0],
"0.2917": [0, 0, -8],
"0.4167": [0, 0, 0]
}
},
"head": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [40, 0, 0],
"0.4167": [0, 0, 0]
}
},
"haunchLeft": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [20, 0, 0],
"0.4167": [0, 0, 0]
}
},
"rearFootLeft": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [17.5, 0, 0],
"0.4167": [0, 0, 0]
}
},
"frontLegLeft": {
"rotation": {
"0.0833": [0, 0, 0],
"0.3333": [-32.5, 0, 0],
"0.5417": [0, 0, 0]
}
},
"frontLegRight": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [-32.5, 0, 0],
"0.4167": [0, 0, 0]
}
},
"tail": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [35, 0, 0],
"0.4167": [0, 0, 0]
}
},
"haunchRight": {
"rotation": {
"0.0833": [0, 0, 0],
"0.3333": [17.5, 0, 0],
"0.5": [0, 0, 0]
}
},
"rearFootRight": {
"rotation": {
"0.0833": [0, 0, 0],
"0.3333": [17.5, 0, 0],
"0.5": [0, 0, 0]
}
}
}
}
}
}
```
## 结合Molang与行为将攻击动画实装
我们希望松鼠的攻击能够在动画播放到特定的时间时再打出,所以我们需要使用一种“延迟”攻击的组件。事实上,我们确实有这种组件。我们可以在劫掠兽的行为定义中看到一个`minecraft:behavior.delayed_attack`的组件,这个组件便控制着劫掠兽的延迟攻击,使劫掠兽的攻击能够在开始攻击动画一定时间后再打出。原版的劫掠兽延迟攻击组件如下:
```json
"minecraft:behavior.delayed_attack": {
"priority": 4,
"reach_multiplier": 1.5,
"attack_duration": 0.75,
"hit_delay_pct": 0.5,
"track_target": true,
"sound_event": "attack.strong"
}
```
其中`hit_delay_pct`字段便是攻击造成伤害时动画已播放的时间。根据我们刚才制作的动画,我们将其修改为`0.25`,也就是我们的头部动画达到最大旋转角的关键帧所在的时间点。然后,我们将其加入到我们的松鼠行为包定义中。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
// ...
},
"components": {
// ...
"minecraft:behavior.float": {
"priority": 0
},
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.delayed_attack": {
"priority": 4,
"reach_multiplier": 1.5,
"attack_duration": 0.75,
"hit_delay_pct": 0.25,
"track_target": true,
"sound_event": "attack.strong"
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"speed_multiplier": 0.8,
"xz_dist": 2,
"y_dist": 1
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
}
// ...
},
"events": {
// ...
}
}
}
```
这个`minecraft:behavior.delayed_attack`AI意向可以使松鼠延迟攻击自己的**目标****Target**)。而我们知道,`minecraft:behavior.hurt_by_target`AI意向可以将攻击松鼠的实体设置为松鼠的目标。这样我们的松鼠便具备了受到攻击后对攻击自己的实体进行延迟攻击的能力。但是此时我们的松鼠仅仅是“会攻击”了但还“攻击不动”。这是为什么呢因为我们还没有为松鼠设定攻击力所以此时松鼠的攻击就算打出也只能算“攻击了个寂寞”。我们通过查阅文档找到设置攻击力的组件`minecraft:attack`。它是一个特性组件所以为了方便维护我们将其加入到特性那部分的组件中。为了方便调试我们将其设置为1以免我们两下便被打死造成测试的不便。
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
// ...
},
"components": {
// ...
"minecraft:attack": {
"damage": 1.0
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:health": {
"value": 30,
"max": 30
}
},
"events": {
// ...
}
}
}
```
现在我们已经为松鼠设置好了攻击的能力接下来只需要将攻击动画正确绑定到实体资源包定义文件中便可以使松鼠在攻击时正确播放动画了。此时我们便需要使用Molang查询函数`query.is_delayed_attacking`。该查询函数在`minecraft:behavior.delayed_attack`AI意向处于激活状态时返回1.0处于未激活状态时返回0.0。因此,我们可以用这个查询函数控制动画的播放。由于我们使用的实体资源定义是旧版的`1.8.0`,所以没有办法直接条件控制动画播放。不过不用担心,我们可以使用一个动画控制器:
```json
{
"format_version" : "1.10.0",
"animation_controllers" : {
"controller.animation.squirrel.general" : {
// ...
},
"controller.animation.squirrel.attack" : {
"initial_state" : "default",
"states" : {
"attacking" : {
"animations" : [ "attack" ],
"transitions" : [
{
"default" : "query.is_delayed_attacking == 0"
}
]
},
"default" : {
"transitions" : [
{
"attacking" : "query.is_delayed_attacking == 1"
}
]
}
}
}
}
}
```
我们将动画和动画控制器绑定在实体资源包定义上:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"materials": {
"default": "rabbit"
},
"textures": {
"red": "textures/entity/squirrel/red",
"gray": "textures/entity/squirrel/gray"
},
"geometry": {
"default": "geometry.squirrel"
},
"animations": {
"walk": "animation.squirrel.move",
"general": "animation.squirrel.idle",
"look_at_target": "animation.squirrel.look_at_target",
"attack": "animation.squirrel.attack"
},
"animation_controllers": [
{ "general": "controller.animation.squirrel.general" },
{ "attack": "controller.animation.squirrel.attack" }
],
"render_controllers": [
"controller.render.squirrel"
],
"spawn_egg": {
"base_color":"#1778D2",
"overlay_color":"#1778D2"
},
"enable_attachables": true
}
}
}
```
这样,我们便可以进入游戏自测查看效果了。我们可以通过`/aigoals`命令打开“AI意向显示”以清晰地查看AI意向的激活情况。
![](./images/12.6_in-game.gif)
我们可以看到,松鼠的攻击动画正常播放了,而且我们确实是在动画播放接近一半时被攻击掉血。这说明我们的自定义攻击动画和攻击行为成功结合了。

View File

@@ -0,0 +1,524 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 25分钟
---
# 联动生物事件与行为
最后,我们来探索生物的触发器组件。在本节中,我们欲通过**触发器****Trigger**)来做到一个松鼠的“避难”机制。
## 事件和触发器
通过之前的章节,我们已经了解到**事件**是实体定义文件中一种非常重要的机制。我们可以通过定义不同的事件来触发不同的效果、添加或移除组件组。而事件的触发机制也是多种多样的。不过,除了内置事件的硬编码触发以外,最多的触发手段应该当属**触发器**组件了。
触发器组件也是一种实体行为包组件。不同于其他类型的组件触发器组件主要用于触发一个事件。换句话说触发器组件就是为了事件而生的。触发器的原理与模组SDK中的事件监听类似都是通过监听一定的系统事件然后在特定的时机执行指定的内容。只不过在实体定义文件这种数据驱动文件中事件响应执行的内容往往是有限的。在这里触发器只能用于触发另一个定义在实体行为包定义文件中的事件。
实体的触发器组件也有很多类型。我们这里想制作一个松鼠受伤后就会逃跑的机制,于是我们需要用到与受伤有关的触发器。在掠夺者的组件定义中,我们便可以发现类似的触发器。
```json
"minecraft:on_hurt": {
"event": "minecraft:ranged_mode",
"target": "self"
},
"minecraft:on_hurt_by_player": {
"event": "minecraft:ranged_mode",
"target": "self"
},
"minecraft:on_target_escape": {
"event": "minecraft:calm",
"target": "self"
}
```
这里,`minecraft:on_hurt`是实体受到伤害时触发,`minecraft:on_hurt_by_player`是实体受到玩家伤害时触发,而`minecraft:on_target_escape`是该实体的目标消失后(即不再将某个实体视为目标后)触发。
我们可以使用`minecraft:on_hurt``minecraft:on_target_escape`来设计一个逃跑机制。
## 为松鼠设计逃跑机制
我们知道要想使松鼠逃跑我们应该需要使用一种可以用来规避生物的AI意向。通过对原版机制的了解我们知道苦力怕会规避“猫科”生物。我们利用这一点到苦力怕的行为文件中寻找相关AI意向。不出所料我们找到了规避特定生物的AI意向`minecraft:behavior.avoid_mob_type`,这是他在苦力怕文件中的表达:
```json
"minecraft:behavior.avoid_mob_type": {
"priority": 3,
"entity_types": [
{
"filters": {
"any_of": [
{ "test" : "is_family", "subject" : "other", "value" : "ocelot"},
{ "test" : "is_family", "subject" : "other", "value" : "cat"}
]
},
"max_dist": 6,
"walk_speed_multiplier": 1,
"sprint_speed_multiplier": 1.2
}
]
}
```
我们便可以使用这个AI意向来作为松鼠规避对它造成伤害的生物的意向。为了改造成我们需要的这种功能我们需要先了解这个AI意向中使用的一种我的世界机制——**过滤器****Filter**)。
上面这段代码中的`filters`字段便是用于指定过滤器的字段。我们可以看到原版的苦力怕指定了两个过滤器,并且使用`any_of`逻辑意为只要两者出现其一满足便可以触发该AI意向。这里原版使用了`is_family`过滤器。该过滤器是一个字符串测试这里原版分别测试了对立实体是否为豹猫或猫一旦满足便执行规避的AI意向。
我们期望我们的实体规避对其造成伤害的实体。而我们知道我们已经有了`minecraft:behavior.hurt_by_target`AI意向任意对其造成伤害的实体都会被其标记为目标。因此我们只需要`has_target`过滤器,该过滤器是一个布尔测试。我们只需要测试对立实体是否为我们实体的目标即可。
我们改造后的组件如下:
```json
"minecraft:behavior.avoid_mob_type": {
"priority": 3,
"entity_types": [
{
"filters": {
"test": "is_target",
"subject": "other",
"value": true
},
"max_dist": 6,
"walk_speed_multiplier": 1,
"sprint_speed_multiplier": 1.2
}
]
}
```
这样只要对立实体是其目标我们的松鼠便会自动规避至与其距离6米远之外。
当然,这里我们意图通过触发器来做到这一点。虽然由于过滤器的存在,再使用触发器便没有必要了,但是我们依旧设计了一个使用触发器触发的松鼠。设计完毕的实体定义文件如下:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"tutorial_demo:color_red": {
"minecraft:variant": {
"value": 0
}
},
"tutorial_demo:color_gray": {
"minecraft:variant": {
"value": 1
}
},
"tutorial_demo:escaping": {
"minecraft:behavior.avoid_mob_type": {
"priority": 3,
"entity_types": [
{
"filters": {
"test": "is_target",
"subject": "other",
"value": true
},
"max_dist": 6,
"walk_speed_multiplier": 1,
"sprint_speed_multiplier": 1.2
}
]
}
}
},
"components": {
// ...
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.delayed_attack": {
"priority": 4,
"reach_multiplier": 1.5,
"attack_duration": 0.75,
"hit_delay_pct": 0.25,
"track_target": true,
"sound_event": "attack.strong"
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"xz_dist": 2,
"y_dist": 1,
"speed_multiplier": 0.8
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
},
// ...
"minecraft:movement": {
"value": 0.3
},
"minecraft:health": {
"value": 30,
"max": 30
},
"minecraft:on_hurt": {
"event": "tutorial_demo:escape_started",
"target": "self"
},
"minecraft:on_hurt_by_player": {
"event": "tutorial_demo:escape_started",
"target": "self"
},
"minecraft:on_target_escape": {
"event": "tutorial_demo:escape_ended",
"target": "self"
}
},
"events": {
"minecraft:entity_spawned": {
// ...
},
"tutorial_demo:escape_started": {
"add": {
"component_groups": [
"tutorial_demo:escaping"
]
}
},
"tutorial_demo:escape_ended": {
"remove": {
"component_groups": [
"tutorial_demo:escaping"
]
}
}
}
}
}
```
这里我们添加了一个`tutorial_demo:escaping`组件组,用于`minecraft:behavior.avoid_mob_type`AI意向的动态增减。然后我们通过`tutorial_demo:escape_started``tutorial_demo:escape_ended`事件来进行组件组的增减。最后,我们通过`minecraft:on_hurt``minecraft:on_hurt_by_player``minecraft:on_target_escape`来实现两个事件的触发。这样一个动态增删功能这里是增删规避生物的AI意向的实现就完成了。
当然,如果我们的“脑补”能力比较强,我们可以发现这样的设计有个明显的缺点。那就是由于`minecraft:behavior.delayed_attack`的存在虽然其优先级为4低于优先级为3的`minecraft:behavior.avoid_mob_type`,但是`minecraft:behavior.avoid_mob_type`只会让实体规避至6米以外。当实体行走至6米以外时`minecraft:behavior.avoid_mob_type`便会暂时被停用,此时`minecraft:behavior.delayed_attack`便会由于高优先级的AI意向被停用从而被激活实体就会重新接近之前攻击它的目标。而接近到一定距离时又会因为太近而重新激活`minecraft:behavior.avoid_mob_type`,同时`minecraft:behavior.delayed_attack`再次被停用。实体便会处于一会规避一会接近的循环状态。我们也可以在电脑开发版中通过ImGui来直观地看到这一点。
![](./images/12.7_in-game_1.gif)
我们需要对此进行优化。
### 优化数据
我们意图通过改变`minecraft:behavior.delayed_attack`的存在性来解决这个问题。我们期望两种变体的松鼠的行为不一致,红色松鼠比较“勇”,只要有人打它就反击,而灰色松鼠比较“逊”,有人打它就逃跑。那么,我们如何实现这一机制呢?事实上,`minecraft:on_hurt`等触发器也是可以使用过滤器来进行条件触发的。我们可以以如下这种方式来写入一个过滤器:
```json
"minecraft:on_hurt": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_hurt_by_player": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_target_escape": {
"event": "tutorial_demo:escape_ended",
"target": "self"
}
```
其中`is_variant`过滤器是用于检测变体的值的过滤器。灰色松鼠的变体值为1因此我们只有检测到变体值为1时我们才触发添加对应AI意向的事件。而红色松鼠则是直接在生成时也通过组件组的形式添加延迟攻击组件这样灰色松鼠就不会附带有这个组件。我们将`minecraft:behavior.delayed_attack`组件移动到红色松鼠的变体组件组中,如下:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"tutorial_demo:color_red": {
"minecraft:behavior.delayed_attack": {
"priority": 4,
"reach_multiplier": 1.5,
"attack_duration": 0.75,
"hit_delay_pct": 0.25,
"track_target": true,
"sound_event": "attack.strong"
},
"minecraft:variant": {
"value": 0
}
},
"tutorial_demo:color_gray": {
"minecraft:variant": {
"value": 1
}
},
"tutorial_demo:escaping": {
"minecraft:behavior.avoid_mob_type": {
"priority": 3,
"entity_types": [
{
"filters": {
"test": "is_target",
"subject": "other",
"value": true
},
"max_dist": 6,
"walk_speed_multiplier": 1,
"sprint_speed_multiplier": 1.2
}
]
}
}
},
"components": {
// ...
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"xz_dist": 2,
"y_dist": 1,
"speed_multiplier": 0.8
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
},
// ...
"minecraft:on_hurt": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_hurt_by_player": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_target_escape": {
"event": "tutorial_demo:escape_ended",
"target": "self"
}
},
"events": {
"minecraft:entity_spawned": {
// ...
},
"tutorial_demo:escape_started": {
// ...
},
"tutorial_demo:escape_ended": {
// ...
}
}
}
}
```
这样我们便实现了红灰两种松鼠的行为特异化我们进入游戏来检测我们的内容。我们可以通过电脑开发版的ImGui直观地看到组件组的增删和AI意向的激活与停用。
![](./images/12.7_in-game_2.gif)
这样,我们便修复了我们的异常。下面,我们放出我们目前为止行为包定义文件中全部的内容,供各位开发者们参考:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:squirrel",
"is_experimental": false,
"is_spawnable": true,
"is_summonable": true
},
"component_groups": {
"tutorial_demo:color_red": {
"minecraft:behavior.delayed_attack": {
"priority": 4,
"reach_multiplier": 1.5,
"attack_duration": 0.75,
"hit_delay_pct": 0.25,
"track_target": true,
"sound_event": "attack.strong"
},
"minecraft:variant": {
"value": 0
}
},
"tutorial_demo:color_gray": {
"minecraft:variant": {
"value": 1
}
},
"tutorial_demo:escaping": {
"minecraft:behavior.avoid_mob_type": {
"priority": 3,
"entity_types": [
{
"filters": {
"test": "is_target",
"subject": "other",
"value": true
},
"max_dist": 6,
"walk_speed_multiplier": 1,
"sprint_speed_multiplier": 1.2
}
]
}
}
},
"components": {
"minecraft:hurt_on_condition": {
"damage_conditions": [{
"filters": {
"test": "in_lava",
"subject": "self",
"operator": "==",
"value": true
},
"cause": "lava",
"damage_per_tick": 4
}]
},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"minecraft:experience_reward": {
"on_death": "query.last_hit_by_player ? Math.Random(0,1) : 0"
},
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": 0
},
"minecraft:physics": {},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"avoid_water": true
},
"minecraft:movement.skip": {},
"minecraft:jump.static": {},
"minecraft:behavior.float": {
"priority": 0
},
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"xz_dist": 2,
"y_dist": 1,
"speed_multiplier": 0.8
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:behavior.look_at_player": {
"priority": 11
},
"minecraft:equipment": {
"table": "loot_tables/entities/squirrel_equipment.json"
},
"minecraft:type_family": {
"family":["squirrel", "mob"]
},
"minecraft:can_climb": {},
"minecraft:collision_box": {
"width": 0.7,
"height": 0.7
},
"minecraft:attack": {
"damage": 1.0
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:health": {
"value": 30,
"max": 30
},
"minecraft:on_hurt": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_hurt_by_player": {
"event": "tutorial_demo:escape_started",
"filters":{
"test": "is_variant",
"value": 1
},
"target": "self"
},
"minecraft:on_target_escape": {
"event": "tutorial_demo:escape_ended",
"target": "self"
}
},
"events": {
"minecraft:entity_spawned": {
"sequence": [
{
"randomize": [
{
"weight": 1,
"add": {
"component_groups": [
"tutorial_demo:color_red"
]
}
},
{
"weight": 1,
"add": {
"component_groups": [
"tutorial_demo:color_gray"
]
}
}
]
}
]
},
"tutorial_demo:escape_started": {
"add": {
"component_groups": [
"tutorial_demo:escaping"
]
}
},
"tutorial_demo:escape_ended": {
"remove": {
"component_groups": [
"tutorial_demo:escaping"
]
}
}
}
}
}
```

View File

@@ -0,0 +1,493 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 30分钟
---
# 挑战:制作一辆卡丁车
在本节中我们一起来制作一个卡丁车。这次的挑战主要是针对Molang的复习和更多复杂应用的学习所以卡丁车本身模型的精细便置为次要目标同时我们将精力全部放在Molang相关的表达式的编写过程上。
## 准备模型
![](./images/12.8_kart_model.png)
我们粗略地绘制一个卡丁车的模型,重点突出了卡丁车的四个轮胎骨骼,同时将所有的骨骼全部放在一个根组里。这样能够方便我们的后续操作。
![](./images/12.8_kart_texture_create.png)
![](./images/12.8_kart_texture_created.png)
我们通过新建纹理贴图的“填充”功能为其快速上色。之后,我们便可以关注卡丁车的动画了。这也是我们本节挑战的重中之重。
## 设计移动、拐弯和起伏动画
我们将关注点移动到车轮上。通过对现实的观察,我们可以得知,当一个卡丁车移动时,轮胎会旋转,而车身则相对不动。当一个卡丁车拐弯时,前轮的旋转角度将会相对于车身更加地大。当卡丁车在空中飞起或下降时,车身会整体后仰或前倾。我们根据实际卡丁车的这些效果来制作我们游戏中卡丁车的动画。
### 移动动画
对于移动动画来说,我们只需要关注四个车轮的旋转即可。我们只需要车轮随着前进的距离进行旋转。
![](./images/12.8_kart_animation.png)
通过查阅文档,我们可以得知`query.modified_distance_moved`查询函数可以用于返回一个实体从进入世界到当前时间移动的总距离。我们可以使用这个查询来进行车轮向前移动的动画。我们更改$yOz$面的转角即X轴向所代表的字段比如将其设置为`50 * query.modified_distance_moved`。我们将四个轮胎设置为同样的值,这样可以保证四个轮子转速一致。
### 拐弯动画
在拐弯时,我们需要改变前轮的转角。我们继续关注前轮的旋转通道。
![](./images/12.8_kart_animation_1.png)
![](./images/12.8_kart_animation_2.png)
我们通过查阅文档得知,当实体在左右移动时,实体会具备一个**偏航角****Yaw**)速度,这个速度可以通过`query.yaw_speed`查询函数得到。我们可以利用这一点来做到车轮的左右偏转。我们将Y轴向的值改为例如`math.clamp(query.yaw_speed * 35, -35, 35)`,这样可以在使其旋转的同时不要转得过于离谱。`math.clamp`可以用于**钳制****Clamp**)一个值到指定的区间中。
### 起伏动画
接下来我们制作起伏动画。我们对于一个卡丁车在下落时一般都具有这种直观:随着卡丁车在空中滑行时落得越来越快,车头会越来越靠下。因此,我们对卡丁车的`root`骨骼,也就是控制着整体的骨骼的旋转通道来进行操作,使其随着竖直速度的变化而旋转。
![](./images/12.8_kart_animation_3.png)
![](./images/12.8_kart_animation_4.png)
通过查阅文档我们可以得知,实体的竖直速度可以用`query.vertical_speed`查询函数来获得。因此我们可以在整体骨骼的X轴向上使用类似于`math.clamp(query.vertical_speed * -7, -35, 35)`的表达式来做到控制起伏。不过值得注意的是,`query.vertical_speed`的方向与世界坐标的Y轴方向一致。也就是说当卡丁车下落时竖直速度的方向是向下的。为了使我们的骨骼向前转我们需要对其乘一个负数。
这样,我们就完成了卡丁车基本动画的制作。接下来,我们并不着急导出动画。我们在这个基础上为其添加粒子效果。
## 添加尾气和胎痕粒子
我们准备好尾气粒子和胎痕粒子,假设他们的短名称分别为`gas``mark`。为了把他们挂接到实体上,我们需要为他们设置**定位器****Locator**)。定位器是模型中一个单点元素,代表模型中一个固定的位置。有了定位器,我们就可以将其他的一些小部件例如粒子挂接到模型的特定位置上。
![](./images/12.8_locator.png)
我们回到“编辑”模式,在右侧的“大纲”窗格上选中我们想加入定位器的骨骼,右键点击“**添加定位器**”。
![](./images/12.8_locator_add.png)
像其他元素一样,我们可以通过拖动预览窗中的坐标轴来改变定位器的位置。这里我们将其放在车轮的底部,代表胎痕粒子的起始位置。
![](./images/12.8_locator_added.png)
很快,我们为四个车轮和尾气管的位置添加了定位器。现在,我们便可以为其添加粒子动画了。回到“动画”模式,我们将着眼于下方的时间轴窗格。
![](./images/12.8_effect.png)
点击“动画效果”按钮,我们可以在时间轴中看到一个除了骨骼之外的额外的动画主体,那便是包含了粒子、声音和命令的效果主体。我们点击“粒子”通道右侧的“+”按钮,为其添加一个`0.0`的关键帧。
### 尾气粒子
我们点击关键帧,可以在左侧的“关键帧”窗格中看到除了“脚本”之外,多出了“效果”和“定位器”两个属性。
![](./images/12.8_locator_select.png)
其中,定位器可以通过下拉菜单选择的方式来选取。
![](./images/12.8_gas.png)
我们将为其粒子的短名称挂接到`pipe`定位器上,这样只差尾气粒子在实体定义文件中的短名称定义,就完成尾气粒子的添加了。
### 胎痕粒子
我们点击“关键帧”窗格右侧的“+”按钮,在同一个关键帧处添加多个效果。
![](./images/12.8_particle_added_all.png)
通过重复上述过程,即可将四个轮胎的胎痕粒子也挂接完毕。现在,我们可以到处几何、纹理和动画文件了,接下来,我们将这些资源挂接到实体客户端定义文件上。
## 挂接卡丁车实体资源
我们在我的世界开发工作台中创建一个AddOn组件然后通过配置创建一个`tutorial_demo:kart`实体。我们将我们在Blockbench项目中的几何、纹理和动画全部导出到我们的AddOn组件的文件夹中。
我们将实体客户端定义文件补全:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial_demo:kart",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/kart/kart"
},
"geometry": {
"default": "geometry.kart"
},
"animations": {
"move": "animation.kart.move"
},
"animation_controllers": [
{ "move": "controller.animation.kart.move" }
],
"render_controllers": [
"controller.render.kart"
],
"particle_effects": {
"mark": "tutorial_demo:mark",
"gas": "tutorial_demo:gas"
}
}
}
}
```
除此之外,我们需要单独制作一个动画控制器用于播放`move`动画:
```json
{
"format_version" : "1.10.0",
"animation_controllers" : {
"controller.animation.kart.move" : {
"initial_state" : "default",
"states" : {
"default" : {
"animations" : [
"move"
]
}
}
}
}
}
```
还需制作一个基础的渲染控制器用于使实体正常渲染:
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.kart": {
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [
"Texture.default"
]
}
}
}
```
除此之外,我们也需要检查一下导出的资源文件,避免挂接失败。首先是几何文件:
```json
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.kart",
"texture_width": 128,
"texture_height": 128,
"visible_bounds_width": 3,
"visible_bounds_height": 2.5,
"visible_bounds_offset": [0, 0.75, 0]
},
"bones": [
{
"name": "root",
"pivot": [0, 1.5, -3.5],
"cubes": [
{"origin": [-7, 1, -4], "size": [14, 1, 1], "uv": [0, 24]},
{"origin": [-7, 1, 7], "size": [14, 1, 1], "uv": [0, 21]},
{"origin": [-7, 3.5, -7.5], "size": [14, 1, 19], "uv": [0, 0]},
{"origin": [-0.5, 4.5, -3.5], "size": [1, 6, 1], "uv": [0, 27]},
{"origin": [-3.5, 10.5, -5.5], "size": [7, 1, 5], "pivot": [0, 10.5, -3.5], "rotation": [-40, 0, 0], "uv": [26, 22]},
{"origin": [-0.5, 2, -4], "size": [1, 1.5, 1], "uv": [11, 0]},
{"origin": [-1, 2, 7], "size": [1, 1.5, 1], "uv": [6, 0]}
],
"locators": {
"pipe": [-2, 4, 11]
}
},
{
"name": "left_wheel",
"parent": "root",
"pivot": [7.5, 1.5, -3.5],
"cubes": [
{"origin": [7, 0, -5], "size": [1, 3, 3], "uv": [9, 11]}
],
"locators": {
"left_front": [7.5, 0, -3.5]
}
},
{
"name": "right_wheel",
"parent": "root",
"pivot": [-8, 1.5, -3.5],
"cubes": [
{"origin": [-8, 0, -5], "size": [1, 3, 3], "uv": [0, 8]}
],
"locators": {
"right_front": [-7.5, 0, -3.5]
}
},
{
"name": "left_back_wheel",
"parent": "root",
"pivot": [7.5, 1.5, 7.5],
"cubes": [
{"origin": [7, 0, 6], "size": [1, 3, 3], "uv": [6, 4]}
],
"locators": {
"left_back": [7.5, 0, 7.5]
}
},
{
"name": "right_back_wheel",
"parent": "root",
"pivot": [-7.5, 1.5, 7.5],
"cubes": [
{"origin": [-8, 0, 6], "size": [1, 3, 3], "uv": [0, 0]}
],
"locators": {
"right_back": [-7.5, 0, 7.5]
}
}
]
}
]
}
```
然后是动画文件:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.kart.move": {
"loop": true,
"bones": {
"left_wheel": {
"rotation": ["50 * query.modified_distance_moved", "math.clamp(query.yaw_speed * 35, -35, 35)", 0]
},
"right_wheel": {
"rotation": ["50 * query.modified_distance_moved", "math.clamp(query.yaw_speed * 35, -35, 35)", 0]
},
"root": {
"rotation": ["math.clamp(query.vertical_speed * -7, -35, 35)", 0, 0]
},
"left_back_wheel": {
"rotation": ["50 * query.modified_distance_moved", 0, 0]
},
"right_back_wheel": {
"rotation": ["50 * query.modified_distance_moved", 0, 0]
}
},
"particle_effects": {
"0.0": [
{
"effect": "gas",
"locator": "pipe"
},
{
"effect": "mark",
"locator": "left_front"
},
{
"effect": "mark",
"locator": "right_front"
},
{
"effect": "mark",
"locator": "left_back"
},
{
"effect": "mark",
"locator": "right_back"
}
]
}
}
}
}
```
最后是两个粒子:
```json
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "tutorial_demo:mark",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particle/particles"
}
},
"curves": {
"variable.psize": {
"type": "catmull_rom",
"input": "variable.particle_age",
"horizontal_range": "variable.particle_lifetime",
"nodes": [1, 1, 0.83, 0, 0]
}
},
"components": {
"minecraft:emitter_initialization": {
"creation_expression": "variable.radius = 2.5;"
},
"minecraft:emitter_rate_instant": {
"num_particles": 1
},
"minecraft:emitter_lifetime_looping": {
"active_time": 2
},
"minecraft:emitter_shape_point": {},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 1.5
},
"minecraft:particle_appearance_billboard": {
"size": ["0.12 * variable.psize", "0.12 * variable.psize"],
"facing_camera_mode": "emitter_transform_xz",
"uv": {
"texture_width": 128,
"texture_height": 128,
"uv": [56, 0],
"uv_size": [8, 8]
}
},
"minecraft:particle_appearance_tinting": {
"color": [0.01176, 0.01176, 0.01176, 0.9098]
}
}
}
}
```
```json
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "tutorial_demo:gas",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particle/particles"
}
},
"curves": {
"variable.psize": {
"type": "catmull_rom",
"input": "variable.particle_age",
"horizontal_range": "variable.particle_lifetime",
"nodes": [1, 1, 0.83, 0, 0]
}
},
"components": {
"minecraft:emitter_initialization": {
"creation_expression": "variable.radius = 2.5;"
},
"minecraft:emitter_rate_instant": {
"num_particles": 1
},
"minecraft:emitter_lifetime_looping": {
"active_time": 2
},
"minecraft:emitter_shape_point": {},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 1.5
},
"minecraft:particle_appearance_billboard": {
"size": ["0.12 * variable.psize", "0.12 * variable.psize"],
"facing_camera_mode": "lookat_xyz",
"uv": {
"texture_width": 128,
"texture_height": 128,
"uv": [56, 0],
"uv_size": [8, 8]
}
},
"minecraft:particle_appearance_tinting": {
"color": [0.01176, 0.01176, 0.01176, 0.9098]
}
}
}
}
```
这样,就可以保证我们的卡丁车的渲染不会出现任何问题了。接下来,我们为卡丁车添加实体行为组件。
## 制作卡丁车实体行为
我们接下来为卡丁车添加行为。比较重要的行为便是可以使其受到物理引擎作用的`minecraft:physics`,使其可被骑乘的`minecraft:rideable`。添加行为如下:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial_demo:kart",
"is_experimental": false,
"is_spawnable": false,
"is_summonable": true
},
"component_groups": {
},
"components": {
"minecraft:persistent": {},
"minecraft:physics": {},
"minecraft:rideable": {
"seat_count": 1,
"crouching_skip_interact": true,
"interact_text": "action.interact.ride.kart",
"family_types": [
"player"
],
"seats": [
{
"position": [
0,
0.2,
-0.3
],
"min_rider_count": 1,
"max_rider_count": 1
}
]
},
"minecraft:pushable": {
"is_pushable": false,
"is_pushable_by_piston": true
},
"minecraft:movement.basic": {},
"minecraft:jump.static": {},
"minecraft:input_ground_controlled": {},
"minecraft:collision_box": {
"width": 1,
"height": 1
},
"minecraft:type_family": {
"family": [
"kart",
"inanimate"
]
},
"minecraft:health": {
"value": 50
},
"minecraft:knockback_resistance": {
"value": 1,
"max": 1
},
"minecraft:movement": {
"value": 0.5
}
},
"events": {
}
}
}
```
![](./images/12.8_in-game_1.gif)
![](./images/12.8_in-game_2.gif)
我们可以看到,卡丁车的各个部分的行为和动画皆按照我们预想的那样进行,这说明我们的成功添加了一个卡丁车实体!