本项目名为SK Model Workspace(模型工作空间),旨在通过简单的方式,创建可交互,可复用的模型。同时具有丰富的接口和较强的可拓展性由于作者一直在咕咕咕,导致该项目有很多坑没有填,如果你正在 香草图书馆 浏览本页面,可以 点这里 访问该文章的原始页面,文章将在原始页面继续保持更新,之后会添加更多有用的功能
之后我也会制作一些基于该数据包的原版家具,(然而我并不会建模,所以做的不怎么好)
- 运作方式:原版游戏,数据包
- 支持版本:1.21.8
本文将详细介绍该数据包的功能,并且提供一些案例教程方便读者理解
有什么问题或建议可以直接在b站或QQ上联系我哦
数据包下载
依赖关系
- (数据包) SK Model Workspace
- (前置数据包) SK API
概述
什么是“可交互模型”呢
玩家对模型进行一定操作,模型对操作进行反馈,具备这种特征的模型可以称作“可交互模型”,比方说有一个椅子,玩家左键点击即可将其破坏,右键点击可以坐到上面。其中“左键点击”和“右键点击”即为操作,“破坏”和“坐”即为反馈,此时,这个椅子就是一个“可交互模型”
展示实体与交互实体在1.19.4版本被加入,为原版开发者们提供了诸多便利,也为找到“可交互模型”的简单实现方式带来了可能性,这一领域目前已有许多优秀的作品:
该如何实现呢
在SK Model Workspace中,每一个可交互模型都由一个Marker,一个或多个展示实体与交互实体组成,其中交互实体用于接收玩家的操作,然后Marker将作为执行者执行事先设定好的事件,最终展示实体给予一定反馈
交互实体接收到玩家的操作以后,需要告诉Marker让其作为执行者,但是如何让交互实体找到Marker呢,一种方法是让交互实体作为Marker的乘客,交互实体可以使用execute on vehicle找到Marker,但是这样做很显然有一个问题:假如有不止一个展示实体,让它们都作为Marker的乘客,那么这些展示实体就无法分别设定自己的坐标。显然这是不合适的
另外一种方法是,在模型被创建的之时,将展示实体和Marker的UUID存入storage中,展示实体只需要查找表即可找到Marker
现在Marker成为了执行者,它可以操作所有的交互实体,这又该如何实现呢?其实也不难,我们将一个模型中所有的展示实体与交互实体称为该模型的元素(element),为每一个元素设定一个不重复的元素ID,然后在Marker中存储所有元素的元素ID和UUID,Marker可以通过给定元素ID来查找该元素的UUID,从而对该元素执行操作
此外,SK Model Workspace还支持给模型配置方块,模型本身是没有碰撞体积的。可以配置屏障方块来给模型添加碰撞体积。也可以配置光源方块,让模型发光。 同时为了不影响世界中已经存在的方块,在模型被创建时,会检查目标位置的方块是否为空气,如果是,配置好的方块才会被放置
模型类
模型类格式
在SK Model Workspace中,模型是以类的形式定义的
data modify storage skmws reg.class.<类名> set value <模型数据>
参数说明
<模型ID> 模型的ID,这是唯一的
<模型数据> 一个包含该模型所有数据的复合标签,格式如下
abstract: (可选)指明该模型类是否为抽象类
extends: (可选)一个类名,该模型类的父类
elements: 元素列表
(一个元素),详见:元素格式
marker_merge: (可选)合并数据至该模型的标记实体,详见:Wiki:Marker
blocks: (可选)方块列表
(一个方块)
position: 方块的相对位置
block: 方块ID
events: (可选)该模型的私有事件列表
anim: (可选)该模型的私有动画
(动画ID) (可选)一个自定义动画,详见:动画格式
properties: (可选)该模型的动态配置
permission: (可选)模型的操作权限,详见:权限控制
playsound_on_place: (可选)该模型被实例化时播放的声音
(一个自定义项目)
align_position: (可选)对齐坐标,若无该项则不进行坐标对齐,列表中有三个数,对应XYZ三轴,实际坐标为不大于当前坐标且能被该值整除的最大数字,填入-1则表示不对该轴坐标进行对齐,示例:[1,-1,1]表示XZ轴对齐方块网格,Y轴不进行对齐
align_rotation: (可选)约束偏航角,若无该项则不进行偏航角约束,示例:输入90代表实际偏航角被约束至东南西北四个方向之一,输入45代表实际偏航角被约束至八个基本方向
lock_rotation: (可选)锁定旋转角,让旋转角恒为指定值,如果该项与align_rotation同时存在,则优先使用该项
元素格式
我们将一个模型中的展示实体与交互实体统称为这个模型的元素,每一个元素都有一个在本模型类中唯一的元素ID
id: 元素ID,元素列表中所有元素的ID不能重复
type: 实体类型,可选值为item_display,text_display,block_display,interaction
当 type:"interaction" 时criteria:
merge: (可选)合并数据至该元素,详见:Wiki:交互实体
position: (可选)该元素的局部坐标
当 type:"item_display" 或 type:"text_display" 或 type:"block_display" 时merge: (可选)合并数据,详见:Wiki:展示实体
position: (可选)该元素的局部坐标
rotation: (可选)该元素的相对旋转角
模型类的动态配置
模型的动态配置数据存储在properties中,什么是动态配置呢?模型在使用过程中可能存在存取一些变量的需求,这些变量就是动态配置,动态配置可以在模型被实例化后更改并立即生效,例如操作权限
permission,也是可以在模型被实例化后更改的,本数据包的一些内置功能也依靠动态配置实现
模型类的继承
模型类跟java中的类比较相似,同样支持多层继承,继承可以提升代码的复用性和扩展性
这里举一个例子:我们要制作「门」,门的种类有很多「橡木门」,「白桦木门」,「金合欢木门」... 你可能会想到给每种门分别创建模型类,不过呢,这样做的话会产生相当多的冗余代码,因为任何一扇门都支持「开门」,「关门」等操作,而这项操作在每种类型的门的模型类中都写了一遍,虽然这样做也没什么问题无非多写了一点代码,我复制粘贴不就行了吗(笑),但是如果某一天要升级「开门」,「关门」等操作的相关代码,需要修改每一个类中的相关代码,这实在是太麻烦了
于是聪明的你想到可以先创建一个「门」类,在「门」类中写上「开门」,「关门」相关代码,然后让「橡木门」类,「白桦木门」类,「金合欢木门」类 ... 都继承自「门」类,这样每一个继承自「门」类的类都拥有了「开门」,「关门」操作,接下来要做的只是在每一个子类中单独定义例如材质,模型,音效等子类特有内容即可
SK Model Workspace按照以下方式处理类的继承,对处理好的数据进行临时存储,方便下次取用
- 检查当前模型类的
extends字段,如果存在的话,则对模型类继承进行以下处理,如果父类也继承自某个类,将对父类进行递归处理
- 合并(merge)子类与父类的除
elements以外所有数据
- 在
elements中,子类中与父类中
id相同的元素进行合并,其余ID仅存在于父类或子类中的元素的数据则全部保留
此外,父类中的数据可能是残缺的,必须经过子类的补充才可以被实例化,为了避免这种父类被误实例化而造成未知错误,可以添加abstract: 1b字段将该类声明为抽象类,抽象类不可被实例化
实例化模型类
什么是“实例化”呢,实例化确实是一个很抽象的概念,不过在这里你可以简单理解为把创建好的模型类摆出来,可以想象一下,你拿着一个方块对着地面点击右键,方块就被放在了地面上,这可以被认为是一种实例化
SK Model Workspace提供多种实例化模型类的方式
- 通过类名
function skmws:construct {class:"<类名>"}
(execute) as 将作为该模型的拥有者
(execute) positioned 模型的创建位置
(execute) rotated 模型创建时的旋转角
- 通过传入完整的模型类数据
function skmws:construct_with
(execute) as 将作为该模型的拥有者
(execute) positioned 模型的创建位置
(execute) rotated 模型创建时的旋转角
storage skmws temp.input 输入一个完整的模型类数据
- 通过Marker(供内部使用,不建议直接调用)
生成一个带有如下数据的Marker,Marker的位置和旋转角将被应用于模型创建
Tags:["skmws.construct"]:
data:
input: 一个类名,或者一个完整的模型类数据
然后执行这个命令
该命令会处理距离执行者最近的带有skmws.construct标签的Marker
function skmws:_private/_player_detect
(execute) as 将作为该模型的拥有者
条件列表
概述
条件列表位于模型类中的interaction元素的criteria标签中,这个标签里的
leftclick和
rightclick就是条件列表,比方说当玩家左键点击了这个交互实体,则会执行
leftclick标签中的条件列表
为了解释清楚条件列表是干什么用的,举个例子:假设有一只羊,你可以手持小麦给羊喂食,或者手持染料给羊染色,手持剪刀给羊剪毛。像这种在不同情况下执行不同操作的行为可以通过条件列表实现
在SK Model Workspace中,条件列表将按照以下方式处理
- 依次检查条件列表中每一个项目
- 直到找到一个项目,该项目中的条件检查均通过,则执行该项目中的事件列表,并不再继续检查后续的项目
条件列表格式
事件列表
概述
模型被交互后执行的操作均由事件列表实现,当一个事件列表被执行时,会依次执行该列表中所有的事件,事件列表可以定义在以下几个地方
- 条件列表的
event 标签中
- 模型类的 events.<事件列表ID> 标签中,此时定义的事件列表为私有事件列表,仅可被本类及其子类访问
- 存储 storage skmws reg.events.<事件列表ID> 标签中,此时定义的事件列表为全局事件列表,可以被所有模型类访问,本包的一些内置功能是使用全局事件列表实现的,详见:模块
事件列表格式
事件列表的执行者为该模型的Marker
可以使用 @a[tag=skmws.s] 来指定正在执行交互操作的玩家
(一个事件)
type: 事件类型
该事件的额外参数(见下文)
# 破坏
当 type:"remove" 时,移除该模型,同时触发on_remove私有事件列表
当 type:"destroy" 时,破坏该模型,同时触发on_remove私有事件列表
破坏模型产生的效果定义在模型类的动态配置中,格式如下
properties:
destroy: 存储模型被破坏时产生的效果
sound: 模型被破坏时播放声音
particle: 破坏粒子,详见:Wiki:方块粒子选项
item: (可选)破坏时的掉落物,该项不存在时则不产生掉落物,该项存在时则产生掉落物,并将该项的内容合并至物品,详见:Wiki:物品格式
# 冷却
当 type:"cooldown" 时,设置交互冷却时间,在该时间段内模型不接受任何操作
time: 冷却时间
# 坐
当 type:"sit" 时,让执行交互操作的玩家坐在该模型上
id: 元素ID,指定让玩家坐到哪个元素上
# 调用事件列表
当 type:"call" 时,调用另一事件列表
with:
event: 事件列表ID
global: (可选)指明调用的事件列表是否为公共事件列表
# 动画
当 type:"anim" 时,播放动画
with:
anim: 动画ID
global: (可选)指明调用动画是否为公共动画
loop: (可选)指明该动画是否会被循环播放
time: (可选)循环播放动画时,动画播放一次所用的时间
当 type:"stopanim" 时,停止正在播放的循环动画
# 执行命令
当 type:"cmd" 时,执行指定命令
with:
cmd: 要执行的命令
key: (可选)一个NBT路径,从模型的动态配置中取用值作为要执行的命令的参数
当 type:"execute" 时,让指定元素执行指定命令
id: 元素ID,作为执行者
with:
cmd: 要执行的命令
key: (可选)一个NBT路径,从模型的动态配置中取用值作为要执行的命令的参数
# 修改元素数据
当 type:"merge" 时,合并数据至指定元素
id: 元素ID
data: 合并数据
当 type:"modify" 时,修改指定元素的指定数据
id: 元素ID
key: 一个NBT路径
value: 值
# 元素增删
当 type:"element_append" 时,添加元素
id: 元素ID
data: (一个元素)
当 type:"element_remove" 时,移除元素
id: 元素ID
# 方块增删
当 type:"block_append" 时,添加方块
position: 放置位置
block: 方块ID
当 type:"block_remove" 时,移除方块
position: 要被移除的方块的位置
# 移动模型
当 type:"move" 时,移动整个模型,包括所有的方块和元素
需要指定position,或者同时指定
facing和
px
with:
position: (可选)一个三元列表,表示相对位移
facing: (可选)朝向,可选值为"N","E","S","W"
px: (可选)沿该朝向移动的像素点数
# 修改模型动态配置
当 type:"properties" 时,编辑该模型的动态配置
key: 一个NBT路径
value: 值
# 播放声音
当 type:"playsound" 时,播放声音
key: 一个NBT路径,从模型的动态配置中取用值作为要播放的声音
事件的函数形式
此外这些事件还有其对应的函数,调用这些函数所达到的效果与在事件列表中执行事件相同
可以在 type:"cmd" 事件执行的命令中使用这些函数,不能在其他的上下文中使用
格式如下:
function skmws:event/<事件类型> {<除type以外的参数>}
动画
概述
展示实体的transformation字段可以进行插值,我们可以利用这个特性制作简单的动画
动画可以定义在以下几个地方
- 模型类的 anim.<动画ID> 标签中,此时定义的动画为私有动画,仅可被本类及其子类访问
- 存储 storage skmws reg.anim.<动画ID> 标签中,此时定义的动画为全局动画,可以被所有模型类访问
动画格式
(一个项目)
merge: 要合并至元素的数据
(元素ID) 要合并至该元素的数据
delay: 距离上个项目的时间,若该项目为第一个项目则可省略该项
关于本节内容的教程详见 教程:动画
权限控制
概述
每一个实例化后的模型都有自己的操作权限,权限信息定义在模型类的动态配置permission中
可选值和解释如下表:
权限描述 | |
---|---|
0 | 所有人可以访问 |
1 | 仅模型拥有者及其好友可以访问 |
2 | 仅模型拥有者可以访问 |
3 | 所有人不可访问 |
- 拥有者即为模型的创建者,谁实例化了这个模型,谁就是这个模型的拥有者
- 带有skmws.debug标签的玩家可以无视权限直接对模型进行操作
好友系统
这是权限控制系统的一个分支,各个玩家的好友数据存储在storage skmws friends中,目前只能通过命令操作
(一个项目)
UUID: 玩家的UUID字符串
friends: 该玩家的所有好友
(一个项目)
UUID: 玩家的UUID字符串
...
...
模块
概述
本包将一些常用的功能封装成了全局事件列表,我们将这些全局事件列表称为模块,模块可以在任何模型类中调用,简化了重复的操作。使用这些模块时,需要在模型类的动态配置中添加一些字段,具体内容见下文
全局事件:模型状态切换
有的时候创建的模型会有很多状态,例如门可以分为“打开”和“关闭”两种状态,蛋糕可以按食用程度分为8种不同的状态。为了简化对模型状态的处理,可以调用toggle_state全局事件列表让模型切换到下一个状态,然后自动调用该模型的私有事件列表on_state_<当前的状态> ,以完成对模型更细致的处理
全局事件列表ID:toggle_state
properties:
toggle_state: 存储模型状态切换相关配置
current_state: (可选)当前状态,默认为0
number_of_state: (可选)状态的数量,默认为2
关于本节内容的教程详见 实战:更丝滑的门
全局事件:推动模型
该模块被调用时可以让模型沿玩家面向的方向移动一格,如果模型的目标位置处有方块阻挡则不移动
全局事件列表ID:push
关于本节内容的教程详见 教程:推动模型
全局事件:染色与清洗
众多周知mojang给很多物品和方块都加入了16种可供选择的颜色,本数据包也提供了类似的功能
染色:全局事件列表ID:dye
清洗:全局事件列表ID:wash
properties:
dyeable: 存储染色相关配置
id: 将要被染色的元素的ID
key: 一个NBT路径,模型被染色时将该值修改为指定值
values: 值列表
default: 默认颜色时,上方key中指向的的值将会被修改为该值
(一个自定义颜色) 当模型被染成指定颜色时,上方key中指向的的值将会被修改为该值
playsound_on_dyeing:(可选) 模型被染色时播放的声音,若不指定则播放默认声音
playsound_on_washing:(可选) 模型被清洗时播放的声音,若不指定则播放默认声音
该模块的配置数据存储在storage skmws config.dyeable中,可以通过修改颜色配置以适配新的颜色
colors: 颜色配置
(一个项目)
item: 检查手持物品,详见:Wiki:物品谓词
key: 该颜色的键名
color: 一个三元列表,该颜色的RGB颜色值,用于显示粒子效果
playsound_on_dyeing: 模型被染色时默认播放的声音
playsound_on_washing: 模型被清洗时默认播放的声音
该配置的默认值如下:
默认值写在了function/cfg/config.mcfunction中
{
colors:[
{item:"red_dye",key:"red",color:[0.7, 0.19, 0.17]},
{item:"blue_dye",key:"blue",color:[0.15, 0.19, 0.57]},
{item:"cyan_dye",key:"cyan",color:[0.16, 0.46, 0.59]},
{item:"gray_dye",key:"gray",color:[0.26, 0.26, 0.26]},
{item:"lime_dye",key:"lime",color:[0.25, 0.8, 0.2]},
{item:"pink_dye",key:"pink",color:[0.85, 0.51, 0.6]},
{item:"black_dye",key:"black",color:[0.12, 0.11, 0.11]},
{item:"brown_dye",key:"brown",color:[0.32, 0.19, 0.1]},
{item:"green_dye",key:"green",color:[0.23, 0.32, 0.1]},
{item:"white_dye",key:"white",color:[0.94, 0.94, 0.94]},
{item:"orange_dye",key:"orange",color:[0.92, 0.53, 0.27]},
{item:"purple_dye",key:"purple",color:[0.48, 0.18, 0.75]},
{item:"yellow_dye",key:"yello",color:[0.87, 0.81, 0.16]},
{item:"magenta_dye",key:"magenta",color:[0.76, 0.33, 0.8]},
{item:"light_blue_dye",key:"light_blue",color:[0.4, 0.54, 0.83]},
{item:"light_gray_dye",key:"light_gray",color:[0.67, 0.67, 0.67]}
],
playsound_on_dyeing:"minecraft:item.brush.brushing.gravel",
playsound_on_washing:"minecraft:item.bucket.empty"
}
当全局事件列表dye被执行时,首先会检查玩家手持物品,如果在storage skmws config.dyeable.colors中定义了这个物品,则获取对应的颜色键名key和粒子颜色
color,然后使用该键名去模型的动态配置properties.dyeable.values中查询一个值,使用该值覆盖掉properties.dyeable.key指向的数据的原始值,最后生成指定颜色的粒子。如果上述步骤有任意一项执行失败,则模型颜色不会改变
当全局事件列表wash被执行时,使用模型的动态配置中properties.dyeable.values.default的值覆盖掉properties.dyeable.key指向的数据的原始值
关于本节内容的教程详见 教程:染色
案例教程
例程:简单装饰模型
先从最简单的开始,简单装饰模型主要的功能是装饰,所以不需要太复杂的效果,我们需要实现的内容有这些

- 能够像方块一样放置和破坏,并且破坏时可以掉落物品
- 放置时矫正方向和偏航角
- 一格碰撞箱
模型类定义如下
{
// 该模型类的元素
elements:[
// 物品展示实体,用于展示模型
{
type:"item_display",
id:"display",
merge:{
// 使用custom_model_data组件来显示自定义模型
item:{
id:"acacia_boat",
components:{
custom_model_data:{strings:["15230006"]}
}
}
},
// 该元素的相对旋转角,这里y轴旋转180度是因为在制作模型的时候角度弄错了导致多转了180度,然后懒得改了,这里再转180度就可以转回原位
rotation:[180,0]
},
// 交互实体,用于处理右键点击事件
{
type:"interaction",
id:"interact",
// 交互实体的宽高都比1稍大一些,这是为了防止玩家点到屏障
merge:{
width:1.01,
height:1.01
},
criteria:{
leftclick:[ // 一个条件列表,在左键模型时执行
{ //一个检查项目,但是没有定义任何条件,所以该项目无论何时都是通过的
event:[{type:"destroy"}] //一个事件列表,功能是摧毁模型
}
]
}
}
],
// 矫正模型位置,让模型对齐方块网格
align_position:[1,1,1],
// 矫正模型朝向,让模型对齐到8个基本方向
align_rotation:45,
// 模型的动态配置
properties:{
// 摧毁模型相关配置
destroy:{
// 摧毁时的声音和粒子
playsound:"block.oak_wood.break",
particle:"oak_planks",
// 摧毁时的掉落物
item:{
// 掉落物的物品堆叠组件
components:{
// 自定义物品模型
"minecraft:item_model":"acacia_boat",
"custom_model_data":{strings:["15230006"]},
// 自定义物品名字
"minecraft:item_name":"公告牌"
}
}
}
}
// 方块
blocks:[
// 在模型位置处放置一个屏障,用于充当一格的碰撞箱
{block:"barrier",position:[0,0,0]}
]
}
教程:动画
使用SK Model Workspace的动画系统,可以完成一些简单的模型动画
现在我们需要制作一个铁傀儡开凿机,并循环播放开凿动画

观察一下开凿机,发现整个结构一共有5个方块,所以在模型类的elements中需要分别为这五个方块添加元素,元素ID分别为head,body_1,body_2,body_3,body_4,同时还需要一个交互实体,用于处理右键破坏操作
elements:[
{
type:"item_display",
id:"head",
merge:{item:{id:"carved_pumpkin"}},
position:[0,2,0]
},
{
type:"item_display",
id:"body_1",
merge:{item:{id:"iron_block"}},
position:[1,1,0]
},
{
type:"item_display",
id:"body_2",
merge:{item:{id:"iron_block"}},
position:[0,1,0]
},
{
type:"item_display",
id:"body_3",
merge:{item:{id:"iron_block"}},
position:[-1,1,0]
},
{
type:"item_display",
id:"body_4",
merge:{item:{id:"iron_block"}},
position:[0,0,0]
},
{
type:"interaction",
id:"interact",
merge:{
width:1.5,
height:3
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
]
}
},
]
在properties中配置一下破坏时的音效粒子和掉落物,这里的掉落物没有添加组件,所以会显示成默认材质(鸡刷怪蛋)
properties:{
destroy:{
particle:"iron_block",
playsound:"minecraft:block.iron.break",
item:{}
}
}
然后我们需要制作铁傀儡开凿动画,并循环播放,这该怎么做呢?首先需要将动画定义为私有动画,私有动画存储在模型类的anim中
anim:{
// 动画ID,这里定义了一个名为"main"的动画
main:[
{
merge:{
body_1:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_2:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_3:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_4:{transformation:{translation:[0,0,0]},interpolation_duration:3}
}
},
{
merge:{
body_1:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_2:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_3:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_4:{transformation:{translation:[0,0,1]},interpolation_duration:3}
},
delay:14
}
]
}
最后我们只需要让这个动画自动循环播放就可以了,可以通过on_load私有事件列表自动执行播放动画操作
events:{
on_load:[{type:"anim",with:{anim:"main",loop:1b,time:28}}]
}
完整代码 [展开..]
{
elements:[
{
type:"item_display",
id:"head",
merge:{item:{id:"carved_pumpkin"}},
position:[0,2,0]
},
{
type:"item_display",
id:"body_1",
merge:{item:{id:"iron_block"}},
position:[1,1,0]
},
{
type:"item_display",
id:"body_2",
merge:{item:{id:"iron_block"}},
position:[0,1,0]
},
{
type:"item_display",
id:"body_3",
merge:{item:{id:"iron_block"}},
position:[-1,1,0]
},
{
type:"item_display",
id:"body_4",
merge:{item:{id:"iron_block"}},
position:[0,0,0]
},
{
type:"interaction",
id:"interact",
merge:{
width:1.5,
height:3
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
]
}
},
],
properties:{
destroy:{
particle:"iron_block",
playsound:"minecraft:block.iron.break",
item:{}
}
},
anim:{
main:[
{
merge:{
body_1:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_2:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_3:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_4:{transformation:{translation:[0,0,0]},interpolation_duration:3}
}
},
{
merge:{
body_1:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_2:{transformation:{translation:[0,0,1]},interpolation_duration:3},
body_3:{transformation:{translation:[0,0,0]},interpolation_duration:3},
body_4:{transformation:{translation:[0,0,1]},interpolation_duration:3}
},
delay:14
}
]
},
events:{
on_load:[{type:"anim",with:{anim:"main",loop:1b,time:28}}]
},
align_position:[1,1,1],
align_rotation:90
}
教程:推动模型
来看一个例子,现在需要制作一个可以被玩家推动的TNT

- 手持铁斧左键即可破坏
- 空手右键可以推动
- 手持打火石右键可以点燃
- 一格碰撞箱
通过观察不难发现这个模型需要一个展示实体和一个交互实体,元素列表如下
elements:[
{
type:"item_display",
id:"tnt",
merge:{
item:{id:"tnt"}
}
},
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:1.01
}
}
]
使用屏障方块模拟碰撞箱,写法如下
blocks:[
{position:[0,0,0],block:"barrier"}
],
现在我们需要对玩家的操作进行处理,可以通过条件列表→事件列表架构来处理
criteria:{
// 左键时执行的条件列表
leftclick:[
// 手持铁斧时破坏模型
{
mainhand_item:"minecraft:iron_axe",
event:[{type:"destroy"}]
}
],
// 右键时执行的条件列表
rightclick:[
// 手持打火石时,移除模型,并在相同位置生成一个点燃的TNT
{
mainhand_item:"minecraft:flint_and_steel",
event:[
{type:"remove"},
{type:"cmd",with:{cmd:"summon minecraft:tnt ~ ~ ~ {fuse:20}"}}
]
},
// 没有手持打火石时,右键即可推动模型
{
event:[{type:"call",with:{event:"push",global:1b}}]
}
]
}
最后只需要修改一下模型的动态配置
properties:{
pushable:{
// 推动时发出的声音
playsound:"minecraft:block.grass.hit"
},
destroy:{
// 破坏粒子和音效
particle:"tnt",
playsound:"minecraft:block.grass.break",
// 掉落物
item:{
components:{
"minecraft:item_model":"minecraft:tnt",
"minecraft:item_name":"TNT"
}
}
},
// 模型被创建时播放的声音
playsound_on_place:"minecraft:block.grass.place"
}
完整代码 [展开..]
{
elements:[
{
type:"item_display",
id:"tnt",
merge:{
item:{id:"tnt"},
teleport_duration:5
}
},
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:1.01
},
criteria:{
leftclick:[
{
mainhand_item:"minecraft:iron_axe",
event:[{type:"destroy"}]
}
],
rightclick:[
{
mainhand_item:"minecraft:flint_and_steel",
event:[
{type:"remove"},
{type:"cmd",with:{cmd:"summon minecraft:tnt ~ ~ ~ {fuse:20}"}}
]
},
{
event:[{type:"call",with:{event:"push",global:1b}}]
}
]
}
}
],
align_position:[1,1,1],
align_rotation:90,
blocks:[
{position:[0,0,0],block:"barrier"}
],
properties:{
pushable:{
playsound:"minecraft:block.grass.hit"
},
destroy:{
particle:"tnt",
playsound:"minecraft:block.grass.break",
item:{
components:{
"minecraft:item_model":"minecraft:tnt",
"minecraft:item_name":"TNT"
}
}
},
playsound_on_place:"minecraft:block.grass.place"
}
}
教程:染色
染色是一个很常用的功能,接下来我们通过一个简单的例子带你了解这个功能
现在要制作一个可以染色的羊毛块

- 副手持有刷子,主手持有染料时,右键羊毛方块,可以将羊毛染成对应颜色
- 副手持有刷子,主手持有水桶时,右键羊毛方块,可以冲洗羊毛方块使其恢复到白色
- 左键摧毁方块
不难发现这个模型包含两个元素,一个展示实体,一个交互实体
elements:[
// 交互实体
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:1.01
},
criteria:{
//...
}
},
// 展示实体
{
type:"item_display",
// 记住这个id,一会要考
id:"block",
merge:{
item:{
id:"white_wool"
},
teleport_duration:5
}
}
]
然后配置条件列表和事件列表
criteria:{
// 左键摧毁方块
leftclick:[
{
event:[{type:"destroy"}]
}
],
// 右键时
rightclick:[
// 副手持有刷子,主手持有染料时,右键羊毛方块,可以将羊毛染成对应颜色
{
offhand_item:"minecraft:brush",
// #skmws:dye 标签中包含了所有染料
mainhand_item:"#skmws:dyes",
event:[{type:"call",with:{event:"dye",global:1b}}]
},
// 副手持有刷子,主手持有水桶时,右键羊毛方块,可以冲洗羊毛方块
{
offhand_item:"minecraft:brush",
mainhand_item:"minecraft:water_bucket",
event:[{type:"call",with:{event:"wash",global:1b}}]
}
]
}
最后是模型类的动态配置
properties:{
destroy:{
particle:"white_wool",
playsound:"minecraft:block.wool.break"
},
playsound_on_place:"minecraft:block.wool.place",
// 染色与清洗相关的配置
dyeable:{
// 这里的元素id填写展示实体的元素id,表示要对该元素进行操作
id:"block",
// 这个NBT路径表示要修改指定元素的哪个值
key:"item.id",
// 值表,根据颜色决定要将key中路径的值替换为何值
values:{
default:"white_wool",
red:"red_wool",
blue:"blue_wool",
green:"green_wool"
//...
}
}
}
完整代码 [展开..]
{
elements:[
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:1.01
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
],
rightclick:[
{
offhand_item:"minecraft:brush",
mainhand_item:"#skmws:dyes",
event:[{type:"call",with:{event:"dye",global:1b}}]
},
{
offhand_item:"minecraft:brush",
mainhand_item:"minecraft:water_bucket",
event:[{type:"call",with:{event:"wash",global:1b}}]
}
]
}
},
{
type:"item_display",
id:"block",
merge:{
item:{
id:"white_wool"
},
teleport_duration:5
}
}
],
blocks:[
{position:[0,0,0],block:"barrier"}
],
properties:{
destroy:{
particle:"white_wool",
playsound:"minecraft:block.wool.break"
},
playsound_on_place:"minecraft:block.wool.place",
dyeable:{
id:"block",
key:"item.id",
values:{
default:"white_wool",
red:"red_wool",
blue:"blue_wool",
green:"green_wool"
}
}
},
align_position:[1,1,1],
align_rotation:90
}
实战:更丝滑的门
原版MC的门没有开关门动画,现在来制作一个带有开关门动画的门

我们先来思考一下这个该如何实现
首先门的展示部分可以用两个方块展示实体实现,交互使用交互实体,碰撞箱使用两个屏障方块
{
elements:[
// 交互实体
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:2
}
},
// 方块展示实体,下半部分
{
type:"block_display",
id:"lower",
merge:{
block_state:{
Name:"cherry_door",
Properties:{facing:"south",half:"lower"}
}
},
position:[-0.5,0,-0.5]
},
// 方块展示实体,上半部分
{
type:"block_display",
id:"upper",
merge:{
block_state:{
Name:"cherry_door",
Properties:{facing:"south",half:"upper"}
}
},
position:[-0.5,1,-0.5]
}
],
// 方块,两个屏障
blocks:[
{position:[0,0,0],block:"barrier"},
{position:[0,1,0],block:"barrier"}
]
}
不过现在这个门还无法进行任何交互,为了方便后续测试,先实现左键破坏,并在模型的动态配置中加入破坏相关的配置
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
]
}
properties:{
destroy:{
// 破坏时的声音和粒子
playsound:"block.cherry_wood.break",
particle:"cherry_planks",
// 掉落物
item:{
components:{
"minecraft:item_model":"minecraft:cherry_door",
"minecraft:item_name":"补帧 · 樱花木门"
}
}
}
}
现在需要实现右键开关门的操作,我们不妨把这个操作抽象一下,开门和关门可以视为两种状态,右键点击时可以从一种状态切换到另一种状态,使用 全局事件: toggle_state 可以很方便的解决有关状态切换的问题
条件列表这样写:
criteria:{
rightclick:[
{
event:[{type:"call",with:{event:"toggle_state",global:1b}}]
}
]
}
然后需要加入一些动态配置
properties:{
toggle_state:{
// 当前状态,我们可以规定1为关门,0为开门,创建模型时默认关门,所以这里写1
current_state:1,
// 状态的总数量,门只有开门和关门两种状态,所以这里写2
number_of_state:2
}
编辑一下切换到该状态时需要执行的事件
- 关门时:放置屏障,播放关门音效,关门动画
- 开门时:移除屏障,播放开门音效,开门动画
events:{
// 开门时执行的事件
on_state_0:[
// 播放开门音效
{type:"playsound",key:"_door.playsound_open"},
// 移除屏障
{type:"block_remove",position:[0,0,0]},
{type:"block_remove",position:[0,1,0]},
// 动画(简单的动画可以直接使用merge实现)
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
],
// 关门时执行的事件
on_state_1:[
// 播放关门音效
{type:"playsound",key:"_door.playsound_close"},
// 放置屏障
{type:"block_append",position:[0,0,0],block:"barrier"},
{type:"block_append",position:[0,1,0],block:"barrier"},
// 动画
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
]
}
事件中使用的音效需要在动态配置中定义
properties:{
_door:{
playsound_open:"block.cherry_wood_door.open",
playsound_close:"block.cherry_wood_door.close"
}
}
至此你完成了这个模型类的定义
完整代码 [展开..]
{
abstract: 1b,
elements:[
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:2
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
],
rightclick:[
{
event:[{type:"call",with:{event:"toggle_state",global:1b}}]
}
]
}
},
{
type:"block_display",
id:"lower",
merge:{
block_state:{
Name:"cherry_door",
Properties:{
facing:"south",
half:"lower"
}
}
},
position:[-0.5,0,-0.5]
},
{
type:"block_display",
id:"upper",
merge:{
block_state:{
Name:"cherry_door",
Properties:{
facing:"south",
half:"upper"
}
}
},
position:[-0.5,1,-0.5]
}
],
blocks:[
{position:[0,0,0],block:"barrier"},
{position:[0,1,0],block:"barrier"}
],
events:{
on_state_0:[
{type:"playsound",key:"_door.playsound_open"},
{type:"block_remove",position:[0,0,0]},
{type:"block_remove",position:[0,1,0]},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
],
on_state_1:[
{type:"playsound",key:"_door.playsound_close"},
{type:"block_append",position:[0,0,0],block:"barrier"},
{type:"block_append",position:[0,1,0],block:"barrier"},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
]
},
properties:{
destroy:{
playsound:"block.cherry_wood.break",
particle:"cherry_planks",
item:{
components:{
"minecraft:item_model":"minecraft:cherry_door",
"minecraft:item_name":"补帧 · 樱花木门"
}
}
},
_door:{
playsound_open:"block.cherry_wood_door.open",
playsound_close:"block.cherry_wood_door.close"
},
playsound_on_place:"block.cherry_wood.place",
toggle_state:{
current_state:1,
number_of_state:2
}
},
align_position:[1,1,1],
align_rotation:90
}
不过先别急,刚才只是做作了樱花木门的模型类,现在需要进行拓展。
先把所有门共有的数据提取出来放在父类中(下面的非共有数据已被注释),并将父类定义为抽象类
// 类名: _door
{
// 定义为抽象类
abstract: 1b,
elements:[
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:2
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
],
rightclick:[
{
event:[{type:"call",with:{event:"toggle_state",global:1b}}]
}
]
}
},
{
type:"block_display",
id:"lower",
merge:{
block_state:{
// Name:"cherry_door",
Properties:{
facing:"south",
half:"lower"
}
}
},
position:[-0.5,0,-0.5]
},
{
type:"block_display",
id:"upper",
merge:{
block_state:{
// Name:"cherry_door",
Properties:{
facing:"south",
half:"upper"
}
}
},
position:[-0.5,1,-0.5]
}
],
blocks:[
{position:[0,0,0],block:"barrier"},
{position:[0,1,0],block:"barrier"}
],
events:{
on_state_0:[
{type:"playsound",key:"_door.playsound_open"},
{type:"block_remove",position:[0,0,0]},
{type:"block_remove",position:[0,1,0]},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
],
on_state_1:[
{type:"playsound",key:"_door.playsound_close"},
{type:"block_append",position:[0,0,0],block:"barrier"},
{type:"block_append",position:[0,1,0],block:"barrier"},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
]
},
properties:{
destroy:{
// playsound:"block.cherry_wood.break",
// particle:"cherry_planks",
// item:{
// components:{
// "minecraft:item_model":"minecraft:cherry_door",
// "minecraft:item_name":"补帧 · 樱花木门"
// }
// }
},
_door:{
// playsound_open:"block.cherry_wood_door.open",
// playsound_close:"block.cherry_wood_door.close"
},
// playsound_on_place:"block.cherry_wood.place",
toggle_state:{
current_state:1,
number_of_state:2
}
},
align_position:[1,1,1],
align_rotation:90
}
然后让子类继承自父类,并加入子类私有的数据,下面演示定义樱花木门类和竹木门类
// 类名:cherry_door
{
extends:"_door",
elements:[
{id:"upper",merge:{block_state:{Name:"cherry_door"}}},
{id:"lower",merge:{block_state:{Name:"cherry_door"}}}
],
properties:{
destroy:{
playsound:"block.cherry_wood.break",
particle:"cherry_planks",
item:{
components:{
"minecraft:item_model":"minecraft:cherry_door",
"minecraft:item_name":"补帧 · 樱花木门"
}
}
},
_door:{
playsound_open:"block.cherry_wood_door.open",
playsound_close:"block.cherry_wood_door.close"
},
playsound_on_place:"block.cherry_wood.place"
}
}
// 类名:bamboo_door
{
extends:"_door",
elements:[
{id:"upper",merge:{block_state:{Name:"bamboo_door"}}},
{id:"lower",merge:{block_state:{Name:"bamboo_door"}}}
],
properties:{
destroy:{
playsound:"block.bamboo_wood.break",
particle:"bamboo_planks",
item:{
components:{
"minecraft:item_model":"minecraft:bamboo_door",
"minecraft:item_name":"补帧 · 竹木门"
}
}
},
_door:{
playsound_open:"block.bamboo_wood_door.open",
playsound_close:"block.bamboo_wood_door.close"
},
playsound_on_place:"block.bamboo_wood.place"
}
}
可见,继承减少了冗余代码,极大地提升了代码复用性
门类(_door) 完整代码 [展开..]
{
abstract: 1b,
elements:[
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:2
},
criteria:{
leftclick:[
{
event:[{type:"destroy"}]
}
],
rightclick:[
{
event:[{type:"call",with:{event:"toggle_state",global:1b}}]
}
]
}
},
{
type:"block_display",
id:"lower",
merge:{
block_state:{
Properties:{
facing:"south",
half:"lower"
}
}
},
position:[-0.5,0,-0.5]
},
{
type:"block_display",
id:"upper",
merge:{
block_state:{
Properties:{
facing:"south",
half:"upper"
}
}
},
position:[-0.5,1,-0.5]
}
],
blocks:[
{position:[0,0,0],block:"barrier"},
{position:[0,1,0],block:"barrier"}
],
events:{
on_state_0:[
{type:"playsound",key:"_door.playsound_open"},
{type:"block_remove",position:[0,0,0]},
{type:"block_remove",position:[0,1,0]},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:-1.5708},
translation:[0.1875,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
],
on_state_1:[
{type:"playsound",key:"_door.playsound_close"},
{type:"block_append",position:[0,0,0],block:"barrier"},
{type:"block_append",position:[0,1,0],block:"barrier"},
{
type:"merge",
id:"upper",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
},
{
type:"merge",
id:"lower",
data:{
transformation:{
left_rotation:{axis:[0,1,0],angle:0},
translation:[0,0,0]
},
start_interpolation:0,
interpolation_duration:5
}
}
]
},
properties:{
toggle_state:{
current_state:1,
number_of_state:2
}
},
align_position:[1,1,1],
align_rotation:90
}
樱花木门类(cherry_door) 完整代码 [展开..]
{
extends:"_door",
elements:[
{id:"upper",merge:{block_state:{Name:"cherry_door"}}},
{id:"lower",merge:{block_state:{Name:"cherry_door"}}}
],
properties:{
destroy:{
playsound:"block.cherry_wood.break",
particle:"cherry_planks",
item:{
components:{
"minecraft:item_model":"minecraft:cherry_door",
"minecraft:item_name":"补帧 · 樱花木门"
}
}
},
_door:{
playsound_open:"block.cherry_wood_door.open",
playsound_close:"block.cherry_wood_door.close"
},
playsound_on_place:"block.cherry_wood.place"
}
}
竹木门类(bamboo_door) 完整代码 [展开..]
{
extends:"_door",
elements:[
{id:"upper",merge:{block_state:{Name:"bamboo_door"}}},
{id:"lower",merge:{block_state:{Name:"bamboo_door"}}}
],
properties:{
destroy:{
playsound:"block.bamboo_wood.break",
particle:"bamboo_planks",
item:{
components:{
"minecraft:item_model":"minecraft:bamboo_door",
"minecraft:item_name":"补帧 · 竹木门"
}
}
},
_door:{
playsound_open:"block.bamboo_wood_door.open",
playsound_close:"block.bamboo_wood_door.close"
},
playsound_on_place:"block.bamboo_wood.place"
}
}