feat:上传历史教程

This commit is contained in:
Othniel su
2024-11-07 18:36:06 +08:00
parent 2a701d54e7
commit da2cc1d4fb
4615 changed files with 66207 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章的学习中,你将学到如何使用**我的世界开发工作台****MC Studio**)创建一个自定义合成**配方****Recipe**)的玩法组件。
- 在第一节(*新建第一个空白基岩版组件作品*)中,你将接触到何为我的世界开发工作台,并学习开发工作台的界面。通过对开发工作台的简单操作,你将成功创建你的第一个空白基岩版组件作品。这意味着你已经成功迈出了我的世界玩法创作的第一步!
- 在第二节(*使用配方配置自定义新的合成配方*)中,通过开发工作台中的简单可视化界面,你将学习如何通过**配置****Configuration**)功能新建一个自定义合成配方。通过一系列易于上手的操作,合成配方的自定义将不在话下。
- 在最后一节(*保存并运行玩法*)中,你将学习到如何运行和自测一个玩法组件。这将有助于你在今后的日子里更加有力地编写和调试自己的模组!
本章关键词:我的世界开发工作台 玩法组件 自定义配方 命名空间 配置 编辑器 自测

View File

@@ -0,0 +1,34 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 新建第一个空白基岩版组件作品
**我的世界开发工作台****MC Studio**)是一个集成了开发者启动器、地图编辑器、关卡编辑器、逻辑编辑器、特效编辑器、云端测试平台等功能的一体化开发工具。它极易上手的操作和简单易懂的可视化界面能够为我们的开发工作提供极大的便利。
接下来,我们通过我的世界开发工作台,为我的世界基岩版制作第一个玩法组件作品!
## 下载我的世界开发工作台
我的世界开发工作台可以在我的世界开发者官网([https://mc.163.com/dev/](https://mc.163.com/dev/))下载。进入官网,点击“下载我的世界开发工作台”即可开始下载。下载完成后点击安装包开始安装并等待安装完成。
![我的世界开发者官网](./images/1.1_official_dev_website.png)
安装完成后桌面即会出现“**我的世界开发者启动器**”的快捷方式。双击打开,在登陆界面中输入开发者账号密码进行登录,便可以查看到我的世界开发工作台的主界面了。
![我的世界开发工作台新闻页面](./images/1.1_mc_studio_main_screen.png)
## 新建组件
在上图中我们可以看到,窗口左侧有一个竖排导航栏,这里是我的世界开发工作台的诸多功能的选项卡。我们点击“+新建”按钮,跳转到新建组件页面。
![我的世界开发工作台新建页面](./images/1.1_mc_studio_new_screen.png)
此处的**组件****Component**)指代的便是可以作为一个作品而独立存在的模组和地图文件的集合。我们只需要点击“**空白地图**”或“**空白AddOn**”便可以新建一个基岩版空白组件。当然,等你熟悉了组件的结构之后,你也可以通过下方的各种**模板**来快速生成一些具备一定初始功能的组件。此处我们只希望建立一个空白的不包含地图的组件,即空白的**附加包****Add-on**组件所以我们点击上方的“空白AddOn”按钮。此时我们看到了一个弹出窗口
![新建空白附加包](./images/1.1_new_component_configurating.png)
根据你的需要进行配置之后,点击“启动编辑”,即可打开我的世界开发工作台的**编辑器****Editor**)。此时,我们便已经成功新建了一个空白的附加包组件啦!之后,你便可以在这个空白的组件上大展拳脚,充分发挥你的想象力和技术力,开始你的我的世界开发之旅!

View File

@@ -0,0 +1,109 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 15分钟
---
# 使用配方配置自定义新的合成配方
接下来我们使用关卡编辑器来添加一个自定义配方。不过在在此之前,我们首先需要熟悉一下编辑器的界面和编辑器的新旧之别。
## 新旧编辑器
长久以来,我的世界开发工作台都使用的编辑器都是现在被称作旧版编辑器的一款软件。在最新的版本中,我们引入了新版编辑器,该编辑器是旧版编辑器的一个重构,引入了旧版编辑器的大部分功能,同时优化了很多功能,使得模组的开发更加便捷与高效。
旧编编辑器的初始化页面:
![旧编编辑器的初始化页面](./images/1.2_level_editor_init_screen.png)
新编编辑器的初始化页面:
![新编编辑器的初始化页面](./images/1.2_new_level_editor_init_screen.png)
### 将组件作品升级为新版编辑器作品
在上一节的末尾,我们的操作将默认打开旧版编辑器。而为了使我们的作品开发更加有效,在接下来的教程中我们将使用新版编辑器进行讲解。所以我们需要把我们的作品升级为新版编辑器作品。我们首先切换到“**最近**”标签页,如图所示,这里有我们刚刚创建的组件作品。
![我的世界开发工作台最近页面](./images/1.2_mc_studio_recent_screen.png)
点击“**编辑**”按钮,点击“**升级作品**”,并在弹出的对话框中点击“**确定**”。之后开发工作台会自动创建一个同名的新版作品,并打开该作品。之后,你便可以使用新版编辑器进行作品编辑了!
![作品编辑对话框](./images/1.2_recent_screen_component_edit.png)
![升级警告对话框](./images/1.2_upgrade_warning.png)
在返回到“最近”标签页时,你可以看到新版作品的周围带有绿色边框,并标注有“新版”字样。
![新版作品](./images/1.2_mc_studio_recent_screen_with_new_editor.png)
## 命名空间
接下来我们希望添加一个自定义配方。但是在添加配方之前,我们有一个不得不需要理解的概念,那就是**命名空间****Namespace**)。每个模组都具有且必须具有至少一个命名空间。命名空间就像一个身份证号一样,他保证了模组与模组之间就算有重名的项目出现,也可以互不干扰,互相可以分辨得开。
你也可以把命名空间比作文件夹。不同的文件夹中就算出现同名的文件,他们依旧可以共存。但是如果没有了文件夹,那么相同命名的文件之间就会出现冲突,出现要么只能保留一个,要么就要有一个文件妥协而改名的尴尬处境。因此,给自己的模组一个合适的命名空间是非常必要的。
### 更改命名空间
我的世界开发工作台的编辑器提供了一个快速更改命名空间的功能。打开新版编辑器,在编辑器顶部的导航栏点击:作品 -> 命名空间,即可打开更改命名空间的对话框。
![作品 -> 命名空间](./images/1.2_navigation_namespace.png)
命名空间实质上是一个标记你所创作的内容所有权的标识符,所以在给你的命名空间起名字时,我们建议使用英语单词配合下划线的方式进行命名。在本教程中,为了行文统一,我们使用`tutorial_demo`作为命名空间。如果你正在跟随本教程进行实践操作,不必拘泥,请尽情地使用你自己想用的命名空间。只需记住一点,命名空间要尽可能的独特和唯一,只有这样才能把你的作品和其他人的作品更好地区分开来。
![输入命名空间](./images/1.2_modify_namespace.png)
## 配置
在新版编辑器中,我们引入了**配置****Configuration**)功能。它默认在关卡编辑器窗口的左下角。
![配置子窗口](./images/1.2_configuration_subwindow.png)
但此时配置是空的,因为我们还没有创建过任何配置。因此我们需要在“**资源管理**”窗口内通过“**新建**”功能来创建新的配置。
![资源管理 - 新建](./images/1.2_resource_management_new.png)
一个配置是一系列有关某个功能的文件的集合。通过对配置的修改,可以实现相关文件的自动修改和自动匹配。这使得复杂的文件变得可视化和有序化。我们在“新建文件向导”对话框中选中“**配置**”选项卡。然后选择一个你想要创建的配置,即可通过向导完成一个配置的创建。
![配置选项卡](./images/1.2_new_file_wizard_config.png)
### 创建新的合成配方
我们回归到本节的正题,通过配置功能创建一个自定义合成**配方****Recipe**)。我们只需要在上图窗口中选择“配方”配置,即可进入新建自定义配方的向导。我们可以看到,这里有两个功能,第一个是选择**数据模板****Data Template**)。数据模板是编辑器内置的已经有一些初始数据的配置,如果选择数据模板,即意味着你可以在一个已经设置了一些属性的配置的基础上继续进行操作。我们选择“**空**”,即创建一个完全空白的配方配置。第二个功能是给配方**命名****Naming**)。如同刚才所述的命名空间代表着模组的唯一标识,这里的名字代表着该配方的唯一标识。给它起一个好名字有助于之后再次看到它时能够迅速回忆起其内容,也有助于避免配方之间的冲突。谨记,命名只能使用英文、数字和下划线,且对大小写不敏感,因此`Aa``aa`本质上是相同的名字,所以建议所有的字母都采用小写。这里我们使用`recipe_demo`来代表这个演示用的配方。你可以根据自己的喜好与习惯对其赋予任意的命名。
![新建自定义配方](./images/1.2_recipe_config_wizard.png)
我们可以看到,配置会为我们自动创建一个叫做`<命名空间>_<配方名>.json`的文件,这是该配置所对应的数据文件。命名空间的存在使其有效避免了与其他模组的同名配方的冲突。
在创建配方配置后,我们便可以在“配置”窗口和“属性”窗口下看到我们刚创建的配方了。如果你成功地看到了如下界面,那么恭喜你,你已经成功创建了一个空白的自定义配方!现在我们只需要把该配方稍加完善即可得到我们想要的结果!
![完成配方配置的创建](./images/1.2_post-creation_of_recipe.png)
### 给配方添加属性
目光转移到屏幕右侧,我们看到了配方的属性栏。在此我们可以更改我们刚才新创建的配方的属性。
![配方属性栏](./images/1.2_recipe_property.png)
**配方类型**代表着配方的适用情况。
- **有序合成**是类工作台配方的一种。对于这种类型的配方,玩家必须摆出和配方的形状一模一样的物品组合时,才会合成出对应物品。在原版中只适用于工作台。
- **无序合成**也是类工作台配方的一种,但是只要合成网格中对应的物品及其数目满足要求即可合成出对应的结果,无需形状如何。在原版中,除了工作台,这种配方还适用于制图台和切石机等方块。
- **熔炉配方**代表着类熔炉配方,熔炉、高炉、营火、烟熏炉等都适用于此类配方。
**配方构造**中的网格代表配方的输入物品和形状,点击网格中的槽位,即可弹出一个可视化物品选择器。通过浏览和搜索,你可以选择任意的原版方块和在我的世界开发工作台中自定义的方块作为槽位物品。这对开发者非常有帮助。
![资源选择器](./images/1.2_resource_pickup_dialog.png)
**配方结果**代表着配方的输出物品。你依旧可以通过点击其右侧文件夹形状的图标来进入物品选择器,从而选取对应的配方结果。**结果数量**即对配方进行一次合成产出的物品数目。
### 示例:可合成的命名牌
我们将配方属性调整至如下所示:
![可合成的命名牌](./images/1.2_craftable_nametag.png)
即可在游戏中获得一个可合成的命名牌。
![可合成的命名牌配方](./images/1.2_craftable_nametag_recipe.png)
恭喜你!你已经熟练掌握了自定义合成配方的制作!但是,这还不代表着万无一失。你还没有保存和在游戏内进行测试。下一节我们将一起学习如何保存并自测玩法组件。

View File

@@ -0,0 +1,63 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 15分钟
---
# 保存并运行玩法
及时保存玩法是开发过程中非常重要的一环。只有养成及时保存的习惯才能避免自己的心血成果因各种不确定因素而丢失的情形发生。而在保存之后,我们也需要通过实机测试来确保自己的玩法真正有效地加入了游戏。
## 保存玩法
我们在编辑器的右上角,可以看到有几个全局按钮。我们只需点击“**保存**”按钮,便可以启动组件的保存程序。
![编辑器右上角](./images/1.3_editor_top-right_save.png)
如果见到如下提示弹窗,说明你已经保存成功了。你可以安心地继续你的创作,或者稍微暂停离开椅子休息一下了。
![保存成功](./images/1.3_save_successfully.png)
## 运行和自测
只在编辑器中进行操作,终归不是真实的游戏体验。在你的作品分发到千千万万玩家手中之前,确保玩法不会出现纰漏最好的办法就是打开游戏进行实测。我的世界开发工作台便提供了这一功能。
### 电脑开发版自测
在我的世界开发工作台中,最简单的运行和自测方式便是使用**我的世界基岩版电脑开发版**(即**Mod PC开发包**)进行自测。你有两种进入电脑开发版的方式。
#### 编辑器内直接进入
不要退出编辑器,目光定位到右上角。我们可以看到一个“**运行**”按钮。点击运行按钮,你的玩法组件将自动保存,同时系统开启我的世界基岩版电脑开发版,自动进入一个加载着你的玩法组件的存档进行测试。
![运行按钮](./images/1.3_editor_top-right_run.png)
#### 从主界面进入
在我的世界开发工作台中找到“**最近**”或“**作品库**”标签页,找到你的作品。将鼠标移至“**开发测试**”按钮。
![开发测试按钮](./images/1.3_mc_studio_lib_screen_dev_test.png)
点击开发测试按钮将会弹出一个开发测试对话框。进行一定配置后点击“**开始**”按钮,即可达到和从编辑器内进入相同的效果——成功进入我的世界基岩版电脑开发版进行测试。
![开发测试对话框](./images/1.3_dev_test_dialog.png)
### 手机开发版自测
**我的世界基岩版手机开发版**自测需要我们先将组件发布至云端。之后我们便可以从手机开发版下载进行测试。我们将鼠标移动至地图或组件作品上,点击“**更多**”按钮。
![更多按钮](./images/1.3_mc_studio_lib_screen_more.png)
之后点击“**发布**”按钮。根据发布资源的流程依次填写好相关内容,点击保存,再点击提交审核。当作品处于“审核中”的状态时,便可以在手机开发版中访问到该作品。
![发布按钮](./images/1.3_release.png)
#### 下载和进入手机开发版启动器
在我的世界开发工作台上找到“**管理**”标签页。找到顶部“**开发者内容管理工具**”一栏中的“**测试版启动器下载**”。点击该按钮在弹出的对话框中会出现两个二维码分别是iOS版本和Android版本的手机开发版启动器下载链接。使用对应的手机扫码下载、安装即可得到手机开发版启动器应用程序。
![管理标签页](./images/1.3_mc_studio_management_screen_test_launcher_download.png)
![下载开发版的对话框](./images/1.3_test_launcher_download.png)
在我的世界基岩版手机开发版中打开你发布的组件,即可进入地图测试啦!

View File

@@ -0,0 +1,17 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章中,你将认识我的世界中的各种基本概念。这将有助于你在后续开发过程中能够更好地理解我的世界这款游戏,从而写出优异的模组。
- 在第一节(*区块、世界和存档*)中,你将认识我的世界中一个**世界****World**)的基本组成,了解一个世界是由一个个的**区块****Chunk**)组成的,而区块又是一个个**方块****Block**)组成的。
- 在第二节(*实体、生物和弹射物*)中,你讲学习到这个世界的**生物****Mob**)事实上都属于一种更广泛的类别——**实体****Entity**),以及了解到**弹射物****Projectile**)其实也是一种实体。
- 在第三节(*方块、物品和物品实体*)中,你将认识到方块和**物品****Item**)的关系,了解到物品和**物品实体****Item Entity**)的不同。
- 在第四节(*模型、纹理和动画*)中,你讲学习到两种不同的**模型****Model**),分别是原版模型的**几何****Geometry**)和中国版特有的骨骼模型。你还会认识到什么是**纹理****Texture**)和**动画****Animation**)。
- 在最后一节(*游戏界面和特效粒子*)中,你将熟悉什么是**UI**、**粒子****Particle**)和**特效****Effect**),了解我的世界的特效粒子是如何运行的。
本章关键词:世界 存档 区块 子区块 方块 碰撞箱 亮度 材料 形状 液体 物品 方块物品 堆叠 耐久 武器 工具 盔甲 物品实体 实体 生物 玩家 弹射物 模型 纹理 动画 序列帧 UI 粒子 特效

View File

@@ -0,0 +1,29 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 区块、世界和存档
在本节中,我们将一起了解我的世界中一个世界的组成方式。
## 世界和存档
在我的世界中,**世界****World**)是玩家最主要的游玩场所。一个世界便是玩家进入游戏后所观察到一切事物的总和。丰富多彩的群系,林林总总的生物和各式各样的地形与组成地形的方块,都是这个世界的一部分。对于每一个世界,我们都有一个**存档****Level**)来储存它。存档是一个世界的文件记录,它通常是以一个文件夹的形式而存在的。在游戏选择世界的界面上,我们能看到我们创建过、游玩过的的各式各样的存档。这其中每一个条目都是代表着一个存档,同时也是一个可以供我们选择进入的世界
![游戏的世界选择界面](./images/2.1_world_screen.png)
在我们进入世界后,我们最先接触到的便是**方块****Block**了。由于世界约等于是无限大的所以方块也可以认为是无限多的。一个方块便是在游戏中以1m³为单位的一个块状物体。有的方块可以充斥这个1m³的空间我们称作完整方块有的则更小一些形状也多样一些这些都是不完整方块开发者还可以通过自定义方块定义出超过该空间大小的方块。方块组成了这个世界的基本形状配合以生物群系和各种生物使得这个世界更加多彩。
![方块举例](./images/2.1_block_example.png)
## 区块
世界在横向上非常广大,为了使世界与世界之间不出现重复,游戏使用**种子****Seed**)来配合生成世界。每一个种子都将对世界生成器造成随机的影响,使得每一个世界的每一个相同的位置都会出现不同的结果。再加之以玩家后续在世界中还会进行各种建筑、开采和改造,我们便需要一种存储方式来将这种不同存储在存档中。**区块****Chunk**)应运而生。
区块是一个尺寸为16×256×16个方块的集合。由于在我的世界的坐标中x坐标和z坐标代表长宽y坐标代表高度。所以我们可以看到区块其实是一个竖长条的长方体。而一个存档便是由一个个区块并排横向排列的一个二维地图。
![一个区块](./images/2.1_chunk.png)
区块又由一个个的**子区块****Subchunk**组成每个子区块是一个16×16×16个方块的立方体。所以一个区块事实上是由16个子区块自下而上排列而成的。我的世界中很多游戏机制都是以区块或者子区块为单位的更多的信息可以参考[Minecraft Wiki上的区块页面](https://zh.minecraft.wiki/w/%E5%8C%BA%E5%9D%97)。

View File

@@ -0,0 +1,31 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 实体、生物和弹射物
本节中,我们将一起学习实体,了解实体的组成和分类,为以后自定义实体打下概念基础。
## 生物和实体
在一个存档中,除了方块之外,和玩家接触最密切的便是**生物****Mob**了。在世界中猪、牛、羊等有好生物僵尸、骷髅、末影人等敌对生物还有末影龙、凋灵等Boss都是生物的一员。然而生物并不是这个世界的非方块造物的全部。比如我们经常使用盔甲架摆放物品、做出造型有时还拿它加入红石电路制作一些高级的机械。而我们刚刚提到的盔甲架便不是生物。它属于一个更上层的概念那便是**实体****Entity**)。
![盔甲架](./images/2.2_armor_stand.png)
实体也称为**活动对象****Actor**),是一种拥有各种形状、各种行为的可交互的物体。与方块不同,实体拥有生命值,可以遭到攻击。当然,有时候有些实体也可以攻击他人,和玩家一样破坏一些方块。**玩家****Player**)本质上也是实体的一种。
![实体和方块](./images/2.2_entity_and_block.png)
一般来说,实体分为生物和**非生物实体****Non-mob Entity**但是这两种实体其实并没有非常严格的界定。一般人们认为拥有AI的的实体都被称为生物。在我们自定义实体时我们也可以给予该实体一些**AI意向****AI Goal**),使其富有生命活力。
![村民的AI意向](./images/2.2_villager_aigoals.png)
## 弹射物
**弹射物****Projectile**)是一种典型的非生物实体,他是一类可以被玩家抛出或发射器发射出去的实体,往往在空中运行一定的轨迹后击中某个方块或实体,然后触发一定的效果。箭、雪球、鸡蛋、末影珍珠、末影之眼等都是弹射物的一种。
![抛出的末影珍珠](./images/2.2_thrown_ender_pearl.png)
弹射物一般拥有和其他实体不同的运动计算方式,而且一般和其对应的物品共享同一个纹理贴图,是一种非常特殊的实体。

View File

@@ -0,0 +1,37 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 方块、物品和物品实体
在本节中,我们将一起进一步学习方块,学习物品和物品实体的区别。
## 方块
**方块****Block**,又称作**Tile**)是世界的基本组成单位,是玩家和这个世界交互的重要途径。方块可以被玩家拿在物品栏里,也可以被放置在世界中。当方块被放置在世界中时,大部分方块将在世界中占据一个块状的位置,阻止玩家和其他生物从此处通过,这被称作方块的**碰撞箱****Collision Box**)。并非所有的方块都有碰撞箱,如草、告示牌和火把都没有碰撞箱,玩家可以直接从其中穿过。
有些方块具有发光的能力,会使周围的亮度大为提升,如火把和荧石。而有些方块则会阻挡光的前进,减少或消除光照的等级,如会完全阻挡光线的石头和会部分阻挡光线的冰。不同光照属性的方块使世界中的**亮度****Lighting**)系统丰富多彩。
![火把和荧石的发光](./images/2.3_torch_and_glowstone.png)
方块有着不同的**材料****Material**)和**形状****Shape**)。方块的材料决定了其固液性质、开采性质和在地图上的颜色表现,而方块的形状则决定了其碰撞箱的造型、视觉的形状和渲染效果。
水和岩浆等方块属于**液体****Liquid**),他们也是方块的一种。只不过,他们有着一种特殊的传播方式。水和岩浆的流动本质上属于在其周围特定的位置放置一个新的方块,只不过新方块的碰撞体积会表现得要么更小一些(向四周流动),要么是整个方块(向下流动)。
## 物品
**物品****Item**,又称作**Icon**)是出现在玩家或其他实体的手上或物品栏中的物体。物品往往具有一些和方块或实体的交互性。例如,拿着名为剪刀的物品对着绵羊按下使用键,就可以将绵羊身上的毛剪落。有一些物品的图标是一个方块,放置后也可以在世界上生成一个方块,这种物品便是**方块物品****Block Item**),方块被破坏后被玩家收集到物品栏中的便是方块物品。广义上讲,方块物品也是物品的一种,只不过他们都是和方块一一对应的,因此大部分人也不区分方块物品和方块的关系,将其混淆使用,统一称呼为方块。此时,剩下的非方块物品便被称呼为狭义的物品。
很多物品可被**堆叠****Stack**)。堆叠后的物品会在物品的右下角显示一个数字,代表其目前的堆叠数目。有些物品可以被使用,使用后往往会减少其堆叠数目。但是,有一类特殊的物品的使用并不会减少堆叠数目,相反,他们会损失一种被称作**耐久****Durability**)的属性,这种特殊的物品往往是一种**武器****Weapon**)、**工具****Tool**)或**盔甲****Armor**。当然一般来说这种特殊物品的最大堆叠数目是1不过他们却可以使用相当多的次数而不消失。
![使用了一半的盔甲](./images/2.3_half-used_armor.png)
## 物品实体
**物品实体****Item Entity**)其实便是我们常说的**掉落物****Drop Item**)。破坏方块、击杀生物都有可能会产生掉落物,即物品实体。顾名思义,物品实体本质上是一种实体,但是其被渲染成了一个或一组物品的样子,并且可以自动与玩家交互——被靠近的玩家拾获。所以物品实体其实并不是物品,只不过他们存储了物品的数据,拥有物品的外观,并且可以在被玩家拾获变成玩家物品栏内的物品。
![一个物品实体](./images/2.3_drop_item.png)
为了使大家对概念更加地明晰,我们可以考虑如下场景:世界中存在一个草方块,它是一个方块。我破坏了草方块,他掉落了一块泥土,这个泥土其实是一个物品实体。更精确地说,它是一个方块物品的物品实体。我拾起了泥土,它变成了我物品栏中真真正正的物品。不过,这个物品是一个方块物品。在我对着地面放置这个物品后,它再次变成了方块,变成了世界中某处的一个泥土方块。

View File

@@ -0,0 +1,53 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 15分钟
---
# 模型、纹理和动画
在本节中,我们将一起学习我的世界中的模型,了解模型的本质和分类。进一步了解纹理和动画对于模型的依赖性和重要性。
## 模型
在我的世界中,不论是方块、物品还是实体,他们本质上都是由一些具有体积的更基础的元素构成的,比如,绵羊是由头部的长方体、身体的长方体和四个代表着四肢的竖直长方体构成的。我们通常将这种具有一定体积的一个由多种基本元素构成的几何形状称为一个**模型****Model**)。
在我的世界中每种生物由具备一个模型。我们可以轻松地在游戏中通过观察看出每种生物模型的形状。每种方块也有一个应用在它身上的模型不过在游戏中很多方块可能会共用一个模型。比如全体的1×1×1的完整方块他们共用一个被称作`block`的模型,代表着标准正方体。
![不同的模型](./images/2.4_some_models.png)
开发者也可以在自己的模组中自定义生物和方块的模型。我的世界中国版支持两种模型格式,一种是微软开发支持的**原版模型**格式,使用该格式编写的模型作品被称为**几何****Geometry**),另一种是网易开发支持的**骨骼模型**格式,使用该格式得到的作品被称作**骨架****Skeleton**)。在我的世界开发工作台中,我们可以通过外部文件导入这两种模型。根据这两种模型的制作工具和格式的不同,又分别称作**BlockBench模型**`*.bbmodel`)和**FBX模型**(`*.fbx`)。
### 原版模型
**原版模型**本质上是一个按照国际版的[几何JSON模式Geometry JSON Schema](https://docs.microsoft.com/en-us/minecraft/creator/reference/content/schemasreference/schemas/minecraftschema_geometry_1.16.0)编写的JSON文件。开发者可以使用低多边形风格的编辑器Blockbench中可以通过可视化编辑的形式来创建原版模型几何然后通过我的世界开发工作台导入到模组中。在下一章中我们将重点介绍该编辑器的使用方法。
原版模型可以直接挂接在实体上,稍加修改后也可以挂接到方块上,为我们自定义模型提供了很大的便利。通过挂接在**附着物****Attachable*****挂件***上后还可以通过和物品配合实现3D物品模型的制作。但是原版模型也有相当大的缺点。最重要的一点就是原版模型只支持低多边形风格即我们俗称的方块像素风格。这导致我们最多实现至四边形网格而无法实现曲线和曲面。同时由于模型的局限性我们也无法在原版模型的基础上实现动态的纹理。因此通过网易开发组的努力我们实现了骨骼模型的支持。
### 骨骼模型
**骨骼模型**本质上也是一个JSON文件但是它是可以由FBX格式文件导入而生成的因此具备几乎所有原FBX模型的性质。因此骨骼模型支持各种高级的模型特性比如可以创造曲线和曲面。同时由于我的世界中国版的支持骨骼模型还可以加载模型贴图序列帧动画、绑定各种高级特效从而使模组玩法和外观更加丰富。
不管是原版模型还是骨骼模型,模型中的基本部件都被称作是一个**骨骼****Bone**)。每个骨骼有着自己特定的形状和位置,而骨骼与骨骼之间又相对独立,这有助于模型更好地完成预期的操作。
## 纹理
**纹理****Texture**,旧译**材质**是指覆盖在模型上的一层贴图文件也是我们在游戏中最能直观感受到的一个事物。不管是方块、物品还是实体他们都是以特定的纹理展示到玩家的镜头前的。比如在工作台方块上我们看到工作台顶部有一块桌布桌布上有一个3×3网格而侧面则摆放着锯子、齿轮等工具。这些外观贴图其实就是纹理而工作台本身就是在一个1×1×1的立方体模型的6个面上分别贴上了特定的纹理而得到的方块。实体亦然只不过由于实体的模型较为复杂我们看到的纹理贴图也稍微有些复杂。
![工作台的六个面](./images/2.4_workbench_texture.png)
![史蒂夫的纹理](./images/2.4_steve_texture.png)
## 动画
一个模型如果仅仅是一个模型,那么它便只能是静态的,好似一个假人。为了让模型更加逼真,实现各种运动或姿态的需要,**动画****Animation**)是必不可缺的一部分。一个动画就是一个和模型中的各个骨骼相绑定,然后通过**动画控制器****Animation Controller**)来使各个骨骼进行相对运动的过程。玩家的奔跑,生物的攻击,鱼儿的游泳,乃至潜影贝盖子旋转打开,这些都是动画。有了动画,我们的游戏内容才会更加生动形象。
潜影贝的动画行为,其中盖子旋转打开时,盖子所对应的骨骼进行了旋转变换和平移变换:
![潜影贝动画](./images/2.4_shulker_shooting.gif)
动画并不是实体独有的,只要游戏引擎支持,所有的模型都可以配以动画。比如,通过中国版的自定义方块实体功能,便可以为方块模型添加动画,以达到和实体模型动画相同的效果。
## 序列帧动画
在我的世界中一提到动画,人们往往想起的是“模型的动画”,也就是我们刚才介绍的动画。但是,还有一种“动画”存在于游戏中,它便是**序列帧动画****Frame Animation**)。序列帧动画是一种“纹理的动画”,又称**逐帧动画**,其原理是在“连续的关键帧”中分解动画动作,在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。所以,这种动画非常类似于我们小时候玩的翻页书,在微软的开发文档中又称作**翻书动画****Flipbook Animation**)。大家口中经常说的动态纹理或动态贴图,便是指的序列帧动画。

View File

@@ -0,0 +1,35 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 15分钟
---
# 游戏界面和特效粒子
在本节中,我们将一起认识游戏界面,了解什么是特效和粒子。
## 游戏界面
每当我们打开我的世界游戏时,我们都能看到一个界面展现在我们面前,上面有着按钮与文字。当我们通过按钮与界面交互时,我们可能会看到弹窗或新的界面。我们进入游戏的世界后,除了眼前的立体的方块世界之外,我们也能看到屏幕下方的物品栏,如果是手机游玩,还能看到屏幕上的方向按键、跳跃键和顶部的暂停键等按钮。这些所有的按钮、文字、窗口等元素都属于游戏界面的一部分,我们统称为**UI****用户界面****User Interface**,又译作**用户接口**)。顾名思义,用户界面就是用户与机器之间交互的接口。在计算机显示屏上,用户与机器交互的接口都是图形化的,因此**GUI****图形用户界面****Graphical User Interface**一词在游戏中与UI一词并无分别。
![游戏内界面](./images/2.5_in-game_UI.png)
我的世界的UI是一套工业化的JSON控制系统通过控件来添加屏幕元素同时通过绑定来事先逻辑功能。我的世界工作台提供了一套简便的可视化UI编辑器省去了繁杂的JSON数据编写的过程为我们提供了极大地方便我们将在下一章中介绍该功能。
## 粒子与特效
**粒子****Particle**是除了UI之外另一种非常重要的可视化效果同时也是一个玩家经常遇到但是又容易忽视的效果。但是细心的玩家依旧能够注意到在我的世界中处处都有着粒子的存在。粒子是一个颗粒状的始终面向玩家的平面面片贴图根据种类不同有着不同的存在时间和运动状态。破坏方块的时候出现的碎屑、玩家从高空掉在地上是激起的扬尘、火把和熔炉燃烧时产生的火焰、一脚踏进水里溅起的水花这些本质上都是粒子效果。基岩版内置的粒子效果在玩家客户端中并没有办法通过命令调出这种粒子称为**旧版粒子****Legacy Particle**),以上我们举的例子都是旧版粒子。为了让玩家和开发者在游戏中更好地使用粒子,国际版引入了一种“新版”的粒子功能,并直接在游戏中称之为**粒子**。
![粒子](./images/2.5_particles.png)
### 粒子
国际版的粒子可以通过一个JSON文件来定义。按照国际版的[粒子JSON的文件格式](https://docs.microsoft.com/en-us/minecraft/creator/reference/content/particlesreference/)便可以定义出一个粒子。这种粒子本质上是一个**粒子发射器****Particle Emitter**),是一种可以在世界中移动的能够根据预先设置好的规则源源不断地发射出单个粒子的虚拟物体。最终展现给玩家的效果只有其发射出的粒子,玩家本身是看不到粒子发射器的。因此,粒子发射器可以实现比单纯召唤出一个粒子更多的功能,这也给了开发者们进行创作时更多的可能。
### 特效
中国版的**特效****Effect**)是中国版开发组在旧版粒子系统的基础上独立于国际版的粒子而创做出的新的粒子系统。通过与骨骼模型的配合,开发者甚至可以快速地将其挂接到骨骼模型上,实现模型和粒子效果的紧密结合。
中国版的特效分为**粒子特效Particle Effect**和**序列帧特效Frame Effect**。粒子特效本质上也是一个粒子发射器,通过不断发射多个不同大小规模的单个粒子,来达到一种特定的效果。序列帧特效也是在游戏中展现一个平面面片贴图,但是其允许纹理贴图具备动态效果,也就是在贴图上播放序列帧动画,使其成为“动态的粒子效果”。因此,序列帧特效往往更加具备开发价值,也能使模组的模型或内容展现得更加丰富。
到此为止,我们在本章中对于我的世界概念的学习就先告一段落。如果有些内容并没有看懂,也并不用担心。因为,我们后面还会讲到这些内容的制作。届时,我们将能够通过各种操作和代码进一步加深这些概念的理解!

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

View File

@@ -0,0 +1,19 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章中,我们将一起初步学习**模组SDK****Mod SDK**的编写。一起了解模组SDK是如何使用系统、组件、事件相配合从而在脚本中完成各种逻辑操作的。
- 在第一节(*了解模组SDK框架的使用方法*我们将一起了解模组SDK框架的构成和用法一起学习模组SDK的**系统****System**)。
- 在第二节(*了解事件驱动*我们讲学习模组SDK的**事件****Event**),了解什么是事件驱动编程。
- 在第三节(*在快捷入口新建服务端系统文件*)中,我们将学习如何通过编辑器快速新建一个系统文件。
- 在第四节(*监听事件并创建组件逻辑*)中,我们将学习如何监听事件和创建**组件****Component**)并执行组件的逻辑。
- 在第五节(*打印信息并运调试运行*)中,我们将学习如何打印消息和调试脚本。
- 在第六节(*从编辑器里导出组件*)中,我们将一起导出我们的脚本作品。
- 在第七节(*挑战:自定义传送点*)中,我们将进行一个挑战,自定义一个传送点。
本章关键词:脚本 模组SDK 模组API 系统 事件 响应 监听 通知 广播 引擎组件 引擎组件工厂 滴答 更新 脚本刻 打印 日志 传送点

View File

@@ -0,0 +1,110 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 20分钟
---
# 了解模组SDK框架的使用方法
我们前面一起学习了数据驱动JSON文件的写法、功能。通过学习我们可以看到数据驱动文件具有非常重要的作用它们可以定义游戏玩法通过基于游戏中的实体各种各样的组件为我们的游戏赋予各种玩法的基础属性。但是数据驱动也有明显的缺点其中最大的缺点就是无法定义复杂的逻辑。即使某些数据驱动定义可以定义一些事件这些事件也只能在某些特定的限制下执行相应的逻辑比如只能去执行特定的有限的功能调用预先设定好的属性而且不能将各个属性通过灵活的逻辑连接起来。最终只能得到一个线性的事件响应网。这种事件系统被我们认为是一种伪事件系统。
为了实现真正的事件系统,我们需要更灵活的编写方法和标准。所以,我们引入了**脚本****Script**系统。中国版的脚本系统使用Python语言来编写通过脚本使能的组件灵活地响应事件将回调函数绑定到游戏运行时的对应位点上赋予游戏更灵活的逻辑。中国版的这套脚本系统提供的Python脚本接口叫做**模组API****Mod API**而中国版为带有以模组API为标准的脚本的附加包提供的开发包称为**模组SDK****Mod SDK**)。
## 配置开发环境
在开始进行模组SDK开发之前我们需要配置Python环境。否则我们将无法正常进行编写和调试。注意中国版的模组SDK使用的是Python2环境。Python2和Python3之间具有较大的代码差异我们无需使用Python3的相关语法。
### 安装Python2
我们进入[Python官网](https://www.python.org/downloads/)下载最新的Python2版本。由于Python2已停止维护我们搜索最新版本2.7并在下面的往期版本中下载。
![](./images/11.1_download_python.png)
**安装时请务必选择安装环境变量**这样可以方便我们快速在命令行中输入Python的程序可执行文件名。
### 安装IDE
我们推荐PyCharm作为Python的集成开发环境IDE。我们在[JetBrains官网](https://www.jetbrains.com/pycharm/download/)上下载PyCharm并安装。如果你没有PyCharm许可证则请下载社区版Community。如果你欲购买许可证或具备学生身份可以申请许可证并下载专业版Professional
![](./images/11.1_install_pycharm.png)
### 安装补全库
为了使我们的代码具备自动补全和定义查询功能我们需要安装模组SDK的补全库。我们可以使用编辑器来安装补全库。
![](./images/11.1_install_lib.png)
打开编辑器,在菜单栏的“工具”下拉菜单中选择你要安装补全库的版本。你可以根据面向的版本和规划来选择安装稳定版补全库或测试版补全库。
![](./images/11.1_cmd_line.png)
开始安装后将提示一个命令行。如果之前安装过旧版本补全库,将先卸载旧版本再安装新版本。
![](./images/11.1_install_successfully.png)
安装成功后将弹出一个提示窗口,提示你安装完成。当然,你也可以像提示窗口中所说的那样,通过命令提示符手动安装补全库。
### 查看示例包
至此,我们做好了所有准备工作。我们打开文档配套的<a href="../../../mcguide/20-玩法开发/13-模组SDK编程/60-Demo示例.html" rel="noopenner"> 示例包 </a>找到TutorialMod演示模组。我们可以在附加包中找到`tutorialScripts`文件夹。这里是便是该模组的一个脚本Python模块文件夹。如果你对Python较为熟悉可以注意到`__init__.py`这代表着这是一个Python模块。事实上一个包中可以有多个脚本文件夹。但每个文件夹都必须是一个模块也就是具备一个`__init__.py`文件。
![](./images/11.1_open_demo.png)
## 什么是系统
我们打开`modMain.py`文件这个文件是Python脚本的入口文件。基岩脚本的引擎将寻找入口文件作为一个Python模块的初始执行文件。目前我们无法自定义入口文件的文件名因此我们需要保证入口文件的文件名为`modMain.py`。我们一起来查看这个入口文件。
```python
# -*- coding: utf-8 -*-
# 上面这行是让这个文件按utf-8进行编码这样就可以在注释中写中文了
# 这行是import到MOD的绑定类Mod用于绑定类和函数
from mod.common.mod import Mod
# 这行import到的是引擎服务端的API模块
import mod.server.extraServerApi as serverApi
# 这行import到的是引擎客户端的API模块
import mod.client.extraClientApi as clientApi
# 用Mod.Binding来绑定MOD的类引擎从而能够识别这个类是MOD的入口类
@Mod.Binding(name = "TutorialMod", version = "0.0.1")
class TutorialMod(object):
# 类的初始化函数
def __init__(self):
print "===== init tutorial mod ====="
# InitServer绑定的函数作为服务端脚本初始化的入口函数通常是用来注册服务端系统system和组件component
@Mod.InitServer()
def TutorialServerInit(self):
print "===== init tutorial server ====="
# 函数可以将System注册到服务端引擎中实例的创建和销毁交给引擎处理。第一个参数是MOD名称第二个是System名称第三个是自定义MOD System类的路径
# 取名名称尽量个性化不能与其他人的MOD冲突可以使用英文、拼音、下划线这三种。
serverApi.RegisterSystem("TutorialMod", "TutorialServerSystem", "tutorialScripts.tutorialServerSystem.TutorialServerSystem")
# DestroyServer绑定的函数作为服务端脚本退出的时候执行的析构函数通常用来反注册一些内容,可为空
@Mod.DestroyServer()
def TutorialServerDestroy(self):
print "===== destroy tutorial server ====="
# InitClient绑定的函数作为客户端脚本初始化的入口函数通常用来注册客户端系统system和组件component
@Mod.InitClient()
def TutorialClientInit(self):
print "===== init hugo fps client ====="
# 函数可以将System注册到客户端引擎中实例的创建和销毁交给引擎处理。第一个参数是MOD名称第二个是System名称第三个是自定义MOD System类的路径
# 取名名称尽量个性化不能与其他人的MOD冲突可以使用英文、拼音、下划线这三种。
clientApi.RegisterSystem("TutorialMod", "TutorialClientSystem", "tutorialScripts.tutorialClientSystem.TutorialClientSystem")
# DestroyClient绑定的函数作为客户端脚本退出的时候执行的析构函数通常用来反注册一些内容,可为空
@Mod.DestroyClient()
def TutorialClientDestroy(self):
print "===== destroy hugo fps client ====="
```
在入口文件中我们看到了我们的Python模组作为一个类而定义而类中除了`__init__`方法之外定义了四个基本方法。分别为`TutorialServerInit``TutorialServerDestroy``TutorialClientInit``TutorialClientDestroy`。这四个方法的作用便是用于注册和销毁**系统****System**)。
我们的模组是分为服务端和客户端分别执行的,这是因为我的世界游戏是分为服务端和客户端两部分互联互通运行的。试想,如果我们不分服务端和客户端,执行一个操作时会出现什么样的结果。在一个服务器中,一个玩家触发了一定的逻辑,逻辑的结果是在世界(0, 64, 0)处放置一个特定的方块。如果我们的服务端和客户端时分开执行的,那么所有的玩家的客户端都将开始执行这一逻辑。但是我们先前知道,玩家在同一时刻只可能在某一个维度的世界中。所以客户端是分辨不出这个放置方块的指示是应该位于哪一维度的。虽然我们只希望在主世界中放置方块,但是处于下界和末地的客户端也将“模拟地”在这两个维度的对应位置放置一个方块。这个方块事实上在服务端中的下界和末地中并未放置,因此将出现客户端和服务端不同步的严重问题。所以,我们的脚本也需要和游戏的基本逻辑一样,分别在服务端和客户端同时设置一个系统,分别执行不同的逻辑,同时通过一定的功能互相通讯,保持同步。
那么系统是什么呢在模组API中我们在每个端都可以设置一个或多个系统。系统将负责宏观工作例如定义和反定义事件、注册和反注册事件的监听、广播或通知一个事件进行各种更新操作。而通过系统进行事件的广播和监听执行逻辑的过程便称为**事件驱动**。
事实上,所有的系统都是一个继承自系统基类的类。这个系统的基类为`mod.common.system.baseSystem`

View File

@@ -0,0 +1,112 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 25分钟
---
# 了解事件驱动
接下来我们以TutorialMod演示模组的服务端系统的定义部分来了解什么是**事件驱动****Event-driven**)系统。我们一起来查看演示模组中的`tutorialServerSystem.py`文件。
```python
# -*- coding: utf-8 -*-
# 获取引擎服务端API的模块
import mod.server.extraServerApi as serverApi
# 获取引擎服务端System的基类System都要继承于ServerSystem来调用相关函数
ServerSystem = serverApi.GetServerSystemCls()
# 获取组件工厂,用来创建组件
compFactory = serverApi.GetEngineCompFactory()
# 在modMain中注册的Server System类
class TutorialServerSystem(ServerSystem):
# ServerSystem的初始化函数
def __init__(self, namespace, systemName):
# 首先调用父类的初始化函数
super(TutorialServerSystem, self).__init__(namespace, systemName)
print "===== TutorialServerSystem init ====="
# 初始时调用监听函数监听事件
self.ListenEvent()
# 监听函数,用于定义和监听函数。函数名称除了强调的其他都是自取的,这个函数也是。
def ListenEvent(self):
# 在自定义的ServerSystem中监听引擎的事件ServerChatEvent回调函数为OnServerChat
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.OnServerChat)
# 监听引擎的事件ServerBlockUseEvent, 回调函数为 OnServerBlockUseEvent
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnServerBlockUseEvent)
# 反监听函数,用于反监听事件,在代码中有创建注册就对应了销毁反注册是一个好的编程习惯,不要依赖引擎来做这些事。
def UnListenEvent(self):
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.OnServerChat)
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnServerBlockUseEvent)
# 监听ServerBlockUseEvent的回调函数
def OnServerBlockUseEvent(self, args):
# 这里的sdkteam_test:block1替换成你自己的自定义方块的命名空间与方块名
if args["blockName"] == "sdkteam_test:block1":
# 调用给予物品的接口与OnServerChat中相似
playerId = args["playerId"]
comp = compFactory.CreateItem(playerId)
# 这里填钻石剑的物品名
comp.SpawnItemToPlayerInv({"itemName": "minecraft:diamond_sword", "count": 1, 'auxValue': 0}, args["playerId"])
# 监听ServerChatEvent的回调函数
def OnServerChat(self, args):
print "==== OnServerChat ==== ", args
# 生成掉落物品
# 当我们输入的信息等于右边这个值时,创建相应的物品
# 创建Component用来完成特定的功能这里是为了创建Item物品
playerId = args["playerId"]
comp = compFactory.CreateItem(playerId)
if args["message"] == "钻石剑":
# 调用SpawnItemToPlayerInv接口生成物品到玩家背包参数参考《MODSDK文档》
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_sword", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石镐":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_pickaxe", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石头盔":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_helmet", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石胸甲":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_chestplate", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石护腿":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_leggings", "count":1, 'auxValue': 0}, playerId)
elif args["message"] == "钻石靴子":
comp.SpawnItemToPlayerInv({"itemName":"minecraft:diamond_boots", "count":1, 'auxValue': 0}, playerId)
else:
print "==== Sorry man ===="
# 函数名为Destroy才会被调用在这个System被引擎回收的时候会调这个函数来销毁一些内容
def Destroy(self):
print "===== TutorialServerSystem Destroy ====="
# 调用上面的反监听函数来销毁
self.UnListenEvent()
```
## 事件驱动
我们可以看到,为了定义我们的系统的类,我们从**额外API****Extra API**,服务端为`mod.server.extraServerApi`)下的`GetServerSystemCls`方法得到了继承自系统基类`mod.common.system.baseSystem`的服务端系统类`mod.server.system.serverSystem`的引用。我们的`TutorialServerSystem`类的定义将继承自该服务端系统类。
接着,在`TutorialServerSystem`类的`__init__`方法中,我们可以看到在调用完毕超类的初始化方法后紧接着便调用了自定义方法`ListenEvent`。这是用来注册各种事件监听的方法。
`ListenEvent`方法中,我们通过调用系统(`self`)的`ListenForEvent`方法来注册一个事件监听。前两个参数分别是该事件监听要监听的系统的命名空间和系统的名称。如果我们要监听引擎默认给出的模组API原生事件我们需要使用额外API中的`GetEngineNamespace``GetEngineSystemName`方法获取**引擎系统****Engine System**的命名空间和名称。引擎系统也是一个系统是基岩引擎中模组API原生的系统。如果要监听自己的或者其他模组的自定义系统广播的事件则需要正确填写对应的命名空间和名称使两个参数和`modMain.py`中注册系统时提供的命名空间和系统名相一致。
**事件****Event**)是由基岩引擎发出或用户自定义的代码发出的,在游戏发生某种特定的逻辑时触发的一种标识。对一个事件进行**监听****Listen**)指的是将一条回调函数传入一个事件的响应代码中,在该事件被触发时同时执行该回调函数。这条回调函数往往被称为事件的**响应****Response**)。而在事件触发时将这一消息发送给指定的客户端或服务端的行为,根据发送的对象和接收端的数目不同分别被称作**广播****Broadcast**)或**通知****Notify**)。
`ListenEvent`方法的第三个参数便是事件的标识符,告诉引擎我将“哪个”事件的监听设置到了该系统上。第四个参数是该监听绑定到的系统的实例。因为我们的事件监听响应(即回调函数)是在该系统的示例中定义的,所以我们填写该系统自身(`self`)。在第五个参数中写入该监听的响应函数。这个函数会在事件触发时异步触发。
这一系列操作便是一个简单的事件驱动逻辑。通过注册一个事件的监听将一个回调函数作为相应绑定到事件上。当事件被触发并广播或通知到该端时便异步执行该回调函数,从而运行相关的逻辑。
## 组件接口的使用
我们从额外API下的`GetEngineCompFactory`方法可以获得服务端**引擎组件工厂****Engine Component Factory**)的类`mod.server.component.engineCompFactoryServer`的引用。引擎组件工厂是用来创建**引擎组件****Engine Component**的类。引擎组件与常规的ECS系统中的组件不同他们不仅是用来存储数据的更是用来执行逻辑的。每个引擎组件下都具有各种各样的方法每种方法都可以用来执行一个不同的逻辑或完成一个不同的操作。比如引擎组件`game`(服务端为`mod.server.component.gameCompServer`的实例)下的`KillEntity`方法便可以杀死一个指定ID的实体。只需使用如下实例代码便可做到杀死一个实体
```python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateGame(levelId)
comp.KillEntity(entityId)
```
上面的`comp`变量便是`mod.server.component.gameCompServer`引擎组件的一个实例。
通过引擎组件可以做到各种逻辑配合事件驱动系统便可以”精准打击“到游戏中各种逻辑发生点并插入自己的逻辑。这样的运作模式便是模组API如何修改游戏的基本方法

View File

@@ -0,0 +1,101 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 15分钟
---
# 在快捷入口新建服务端系统文件
本节中,我们一起学习如何在新版编辑器中创建脚本文件。
## 使用资源管理器快捷新建脚本系统
我们打开编辑器,点击“**资源管理**”窗格的“**新建**”按钮。
![](./images/11.3_create_main.png)
在“**代码**”选项卡中找到**ModMain**。这个选项将可以帮助我们快速创建一个模组API文件夹其中包括一个代表该文件夹是一个模块的`__init__.py`和一个入口文件`modMain.py`
![](./images/11.3_create_main_2.png)
我们在这里给文件命名,实际上就是在给模组命名。我们模组类的装饰器`@Mod.Binding(name = "ModName", version = "0.0.1")`中的`ModName`处便代表我们的模组名。这里的名字将自动填充至模组入口文件中模组类的装饰器中。同时,脚本文件夹名和模组的类名也将会使用我们这里的模组名,所以请谨慎填写一个不会和其他开发者的模组中的模组名重名的名称,否则将有可能造成加载冲突。
接下来我们就可以分别给我们的模组创建服务端文件和客户端文件了!我们以服务端文件为例。
![](./images/11.3_create_server.png)
还是在“**代码**”选项卡中,我们选择**ServerSystem**。这个选项可以用于创建服务端系统文件`xxxxServerSystem.py`
![](./images/11.3_create_server_2.png)
我们在第一个下拉菜单中选择我们刚才创建的脚本文件夹目录。如果一个附加包中存在多个脚本文件夹,我们就需要进行在下拉菜单中选择正确的我们想要创建的脚本目录。文件名便是将会自动使用到服务端系统的类名上的名称。
![](./images/11.3_created.png)
我们可以看到,服务端系统文件也创建了。同时,编辑器还创建了一个`Part`文件夹,这是新版编辑器自动创建的文件。如果我们的脚本不是用于零件脚本,那么这个文件夹可以忽略。
我们来查看自动创建的`modMain.py`
```python
# -*- coding: utf-8 -*-
from common.mod import Mod
@Mod.Binding(name="DemoTutorialMod", version="0.0.1")
class DemoTutorialMod(object):
def __init__(self):
pass
@Mod.InitServer()
def DemoTutorialModServerInit(self):
pass
@Mod.DestroyServer()
def DemoTutorialModServerDestroy(self):
pass
@Mod.InitClient()
def DemoTutorialModClientInit(self):
pass
@Mod.DestroyClient()
def DemoTutorialModClientDestroy(self):
pass
```
`DemoTutorialServerSystem.py`
```python
# -*- coding: utf-8 -*-
import server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class DemoTutorialServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
pass
```
我们可以看到,自动创建时并不会在模组类中注册我们的系统,所以我们的系统注册代码需要手动输入。同时,自动创建给我们的服务端系统类写入了两个函数,分别是脚本刻的**滴答****Tick**)函数和系统类的**更新****Update**函数这两个函数分别通过事件响应回调和重载超类方法的形式实现每秒运行30次的代码。这样运行一次的时间我们称为一个**脚本刻****Script Tick**)。

View File

@@ -0,0 +1,122 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 20分钟
---
# 监听事件并创建组件逻辑
现在我们已经创建好了一个服务端系统,我们来尝试向这个系统中添加逻辑。
在添加逻辑之前我们需要手动将系统在主模组文件中注册。我们仿照之前看到的示例将额外API的`RegisterSystem`函数写在`@Mod.InitServer()`装饰器下的方法中。
```python
@Mod.InitServer()
def DemoTutorialModServerInit(self):
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
```
## 监听实体受伤事件
我们回到服务端系统文件`DemoTutorialServerSystem.py`中。我们一起尝试做一个简单的事件监听,比如,监听实体(活动对象)收到伤害的事件。
通过查阅API文档我们得到了控制实体受到伤害的事件`ActorHurtServerEvent`,其字面意思为“活动对象受伤服务端事件”。我们通过该系统本身的`ListenForEvent`方法来注册这个事件监听。我们在`__init__`方法的末尾加入我们的监听注册函数,同时在该类中定义一个新的方法,比如名为`OnActorHurtServer`,将该方法作为回调函数绑定到事件上。
```python
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
```
这样,我们便完成了实体受伤的监听。当实体受到伤害时,`DemoTutorialServerSystem`实例的`OnActorHurtServer`方法将会运行。
## 在事件内触发击退逻辑
我们希望不仅仅是监听该事件,更要在事件发生时执行一些逻辑,比如更改受击实体的击退属性。
我们通过查阅API文档得知`action`组件(`mod.server.component.actionCompServer`)具备更改击退逻辑的方法`SetMobKnockback`。所以我们使用引擎组件工厂创建一个`action`引擎组件,然后调用它的设置击退的方法做到一些逻辑,比如我们想增加击退的威力。我们在`OnActorHurtServer`中写入如下内容。
```python
def OnActorHurtServer(self, args):
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
```
![](./images/11.4_set_knockback.png)
这样,我们便成功更改了击退的威力。我们将完整的修改过的代码展示在此处。首先是`modMain.py`
```python
# -*- coding: utf-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
import mod.client.extraClientApi as clientApi
@Mod.Binding(name="DemoTutorialMod", version="0.0.1")
class DemoTutorialMod(object):
def __init__(self):
@Mod.InitServer()
def DemoTutorialModServerInit(self):
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
@Mod.DestroyServer()
def DemoTutorialModServerDestroy(self):
pass
@Mod.InitClient()
def DemoTutorialModClientInit(self):
pass
@Mod.DestroyClient()
def DemoTutorialModClientDestroy(self):
pass
```
然后是`DemoTutorialServerSystem.py`
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class DemoTutorialServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
def OnActorHurtServer(self, args):
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ActorHurtServerEvent", self, self.OnActorHurtServer)
```
![](./images/11.4_in-game.gif)
进入游戏测试,便可以发现`SetMobKnockback`更改击退属性“诚不我欺”。

View File

@@ -0,0 +1,58 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 10分钟
---
# 打印信息并运调试运行
懂得如何将信息打印到控制台上是一个合格的开发者必须具备的本领。在本节中我们将学习如何打印信息并调试运行。
## 学会打印日志并查看控制台
每次打开电脑开发版,我的世界开发工作台便会创建一个“**脚本测试日志**”窗口,这边是我们的脚本调试控制台。
在模组中,我们可以在任何位置凭借自己的意愿将任何模组中的上下文信息打印到控制台上。我们有两种打印方式。
### `print`
我们可以使用原生的`print`函数来打印信息。比如,这边是一个`print`函数的打印示例,它将在初始化模组服务端注册之前打印开始初始化的消息。
```python
@Mod.InitServer()
def DemoTutorialModServerInit(self):
print "===== init tutorial server ====="
serverApi.RegisterSystem("DemoTutorialMod", "Server", "Script_DemoTutorialMod.DemoTutorialServerSystem.DemoTutorialServerSystem")
```
### `mod_log`
我们还可以通过`mod_log`模块来打印信息。下面的示例中通过导入并执行`logger.info`打印了一个信息。
```python
from mod_log import logger
logger.info("print log: %s", "OK")
```
### 两者的不同
我们以下面代码为示例可以看到两种打印方式在控制台表现的不同。
```python
def OnActorHurtServer(self, args):
print "==== 使用print打印的日志 ==== "
logger.info("使用logger打印的日志")
comp = serverApi.GetEngineCompFactory().CreateAction(args["entityId"])
comp.SetMobKnockback(0.1, 0.1, 10.0, 1.0, 1.0)
```
![](./images/11.5_print.png)
## 使用热更新快速修复问题
我们的Python代码在游戏中支持热更新调试。在电脑开发版的游戏运行中时我们修改Python代码将导致文件在开发版中的重载。自动热更新主要用于修改函数内实现比如我可以修改击退威力使其增加为20那么我在游戏中就可以在下一次攻击时延长击退的距离。但是热更新对全局变量、新增类或文件无效。因为这些都是在Python代码运行一开始便初始化的内容热更新文件不会使其再次初始化所以可能会导致无效。
![](./images/11.5_overload.png)
我们将击退为例从10更改为20可以在控制台中看到热更新的信息。灵活掌握代码的热更新将有效大幅度减少开启关闭游戏的次数使调试更加方便。

View File

@@ -0,0 +1,25 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 10分钟
---
# 从编辑器里导出组件
导出组件也是开发者应该学会的开发中必不可少的一环。我们有多种组件导出方法。
## 导出为资源包
打开编辑器,点击“资源管理”窗格中的“导出”可以将组件导出为资源包,文件格式为`.mep`
![](./images/11.6_export.png)
通过这种方式导出的资源包可以在另一个附加包组件中通过同一位置的“导入”功能合并导入。
## 整体导出
在我的世界开发工作台中找到作品。右键或点击“更多”按钮,在展开的菜单中点击“导出”或“导出(含编辑信息)”。
![](./images/11.6_export_2.png)
这种方式可以使整个包导出为一个`.zip`文件。通过这种方式导出的文件可以在我的世界开发工作台中通过“本地导入”功能重新导入为一个组件。

View File

@@ -0,0 +1,362 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 30分钟
---
# 挑战:自定义传送点
在本节中,我们一起来制作一个传送点模组。我们知道,在我的世界中,地形可以近似认为是无限广阔的,所以,我们在探索的过程中可能经常迷失方向,找不到回到据点的路。因此,我们可以自定义一个传送点模组。通过保存一个传送点来做到在迷路时快速回到传送点所指定的位置。
下面我们一起使用模组SDK来制作一个传送点模组。
## 创建项目
我们在我的世界开发工作台中创建一个“传送点模组”的组件然后使用编辑器“资源管理”窗格中“创建”按钮来快速创建一个Python模组模块将其命名为`TeleportPointMod`
![](./images/11.7_tp_point_server_create.png)
由于我们知道,只有服务端才能获取和控制每个玩家的位置,所以我们只需创建一个服务端系统,将其命名为`TeleportPointServerSystem`。对于每个玩家来说,客户端一般只负责屏幕的渲染和本地的一些计算,是无法直接改变玩家处于整个世界中的位置的,所以我们无需使服务端与客户端交互。因此我们无需创建客户端系统。
![](./images/11.7_tp_point_server_created.png)
创建结束后我们可以在“资源管理”窗格中看到如图的文件排布。我们将整个行为包文件夹在Python的IDE中打开以便之后的代码编写。此时的Python文件内容如下。
`modMain.py`
```python
# -*- coding: utf-8 -*-
from common.mod import Mod
@Mod.Binding(name="TeleportPointMod", version="0.0.1")
class TeleportPointMod(object):
def __init__(self):
pass
@Mod.InitServer()
def TeleportPointModServerInit(self):
pass
@Mod.DestroyServer()
def TeleportPointModServerDestroy(self):
pass
@Mod.InitClient()
def TeleportPointModClientInit(self):
pass
@Mod.DestroyClient()
def TeleportPointModClientDestroy(self):
pass
```
`TeleportPointServerSystem.py`
```python
# -*- coding: utf-8 -*-
import server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# ScriptTickServerEvent的回调函数会在引擎tick的时候调用1秒30帧被调用30次
def OnTickServer(self):
"""
Driven by event, One tick way
"""
pass
# 这个Update函数是基类的方法同样会在引擎tick的时候被调用1秒30帧被调用30次
def Update(self):
"""
Driven by system manager, Two tick way
"""
pass
def Destroy(self):
pass
```
## 注册服务端系统
我们知道,单纯创建了服务端系统文件是无法在主模组文件中同时将其注册的。我们手动将我们的服务端系统注册。我们在`TeleportPointModServerInit`函数中输入注册系统的方法:
```python
@Mod.InitServer()
def TeleportPointModServerInit(self):
serverApi.RegisterSystem('TeleportPointMod', 'TeleportPointServerSystem', 'Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem')
```
其中`Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem`对应着`Script_TeleportPointMod`文件夹中`TeleportPointServerSystem.py`文件的`TeleportPointServerSystem`类。这样,我们便成功注册了系统。
## 制作传送点模组的主体
传送点模组的主体部分应该都位于服务单系统中,我们只需要考虑实现的逻辑即可。
我们可以允许玩家使用多种方式来触发传送。最简单的一种便是使用聊天栏触发。我们不妨就采取这种方式来制作模组。我们可以设置一些关键词,用于传送点的设置、传送和移除,比如`tppoint set``tppoint apply``tppoint remove`。而每个玩家为其使用一个变量来保存其设置的传送点坐标的数据。为了方便保存我们可以使用一个字典来做到这一点。字典中的键为玩家的ID而值为玩家保存的传送点坐标。当玩家执行传送时我们主需要检索该玩家的ID获取字典保存的值然后将玩家的坐标设定为这个值即可。
我们依次来实现这个逻辑。
### 监听玩家的聊天栏消息
我们通过查阅文档可知,服务端的`ServerChatEvent`事件可以做到响应玩家发送聊天栏信息,于是我们为其设置事件监听。
```python
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
```
同时加入一个`on_chat`方法,用于充当回调函数。
```python
def on_chat(self, event):
pass
```
### 记录传说点
下面我们按照我们刚才的设想开始写入基本逻辑。我们读取事件数据中的`playerId``message`。它们分别是发送消息的玩家ID和所发送的消息。我们使用字符串的`startswith`方法来判定消息是否以`tppoint set`开头。如果消息以`tppoint set`开头那么就通过玩家ID创建一个`pos`引擎组件。`pos`引擎组件具备`GetFootPos`方法、`GetPos`方法和`SetPos`方法。我们可以使用`GetFootPos`方法来获取玩家脚步坐标并存入一个字典,我们不妨起名为`player_tp_cache``player_tp_cache`字典变量由于是需要在整个类中实现的,所以我们将其放在类的初始化方法里。
```python
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
# 用来暂时缓存玩家的传送点信息,在每次重新开启一次游戏后会被重置
self.player_tp_cache = {}
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
```
### 删除传送点
依据同样的原理,我们使用字典的`pop`方法来移除一个玩家的键值对。我们加入删除传送点的逻辑。
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
print '已移除玩家传送点'
else:
print '该玩家没有设置传送点'
```
### 传送至传送点
最后,我们使用`pos`引擎组件的`SetPos`方法来实现传送逻辑。
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
print '已移除玩家传送点'
else:
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
print '已应用玩家传送点'
else:
print '该玩家没有设置传送点'
```
这样,我们便完成了传送逻辑的编写。额外地,我们还可以通过`msg`组件来为玩家显示消息提示,展示设置、传送或移除成功的提示。我们可以修改如下:
```python
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已设置传送点")
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已移除传送点")
print '已移除玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已传送至传送点")
print '已应用玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
```
我们可以进入游戏查看传送和消息提示情况。
![](./images/11.7_in-game.png)
我们可以看到,传送和消息都和我们所设置的一样正常运作!
我们最后放出完整的代码供大家参考,`modMain.py`如下:
```python
# -*- coding: utf-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
@Mod.Binding(name="TeleportPointMod", version="0.0.1")
class TeleportPointMod(object):
def __init__(self):
pass
@Mod.InitServer()
def TeleportPointModServerInit(self):
serverApi.RegisterSystem('TeleportPointMod', 'TeleportPointServerSystem', 'Script_TeleportPointMod.TeleportPointServerSystem.TeleportPointServerSystem')
@Mod.DestroyServer()
def TeleportPointModServerDestroy(self):
pass
@Mod.InitClient()
def TeleportPointModClientInit(self):
pass
@Mod.DestroyClient()
def TeleportPointModClientDestroy(self):
pass
```
`TeleportPointServerSystem.py`如下:
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
from mod.server.system.serverSystem import ServerSystem
ServerSystem = serverApi.GetServerSystemCls()
class TeleportPointServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerChatEvent", self, self.on_chat)
# 用来暂时缓存玩家的传送点信息,在每次重新开启一次游戏后会被重置
self.player_tp_cache = {}
def on_chat(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取聊天消息
message = event['message']
if message.startswith('tppoint set'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
teleport_pos = pos_comp.GetFootPos()
# 保存玩家的当前坐标至字典缓存中
self.player_tp_cache[player_id] = teleport_pos
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已设置传送点")
print '已设置玩家传送点'
if message.startswith('tppoint remove'):
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
# 弹出字典中的玩家数据
self.player_tp_cache.pop(player_id)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已移除传送点")
print '已移除玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
if message.startswith('tppoint apply'):
pos_comp = serverApi.GetEngineCompFactory().CreatePos(player_id)
# 判断该玩家是否存有数据
if player_id in self.player_tp_cache:
teleport_pos = self.player_tp_cache[player_id]
# 传送玩家
pos_comp.SetPos(teleport_pos)
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "已传送至传送点")
print '已应用玩家传送点'
else:
msg_comp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msg_comp.NotifyOneMessage(player_id, "请先设置传送点")
print '该玩家没有设置传送点'
def Destroy(self):
pass
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

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)
我们可以看到,卡丁车的各个部分的行为和动画皆按照我们预想的那样进行,这说明我们的成功添加了一个卡丁车实体!

Some files were not shown because too many files have changed in this diff Show More