像写诗一样制作可交互模型

本项目名为SK Model Workspace(模型工作空间),旨在通过简单的方式,创建可交互,可复用的模型。同时具有丰富的接口和较强的可拓展性
由于作者一直在咕咕咕,导致该项目有很多坑没有填,如果你正在香草图书馆浏览本页面,可以点这里访问该文章的原始页面,文章将在原始页面继续保持更新,之后会添加更多有用的功能
之后我也会制作一些基于该数据包的原版家具,(然而我并不会建模,所以做的不怎么好)

  • 运作方式:原版游戏,数据包
  • 支持版本:1.21.8

本文将详细介绍该数据包的功能,并且提供一些案例教程方便读者理解

数据包下载

依赖关系

  • (数据包) 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,从而对该元素执行操作

sequenceDiagram
  玩家->>交互实体: 玩家点击交互实体
  交互实体->>Marker: 查找表,找到Marker的UUID
  Marker-->>展示实体1: 通过元素ID查找UUID
  Marker-->>展示实体2: 通过元素ID查找UUID

此外,SK Model Workspace还支持给模型配置方块,模型本身是没有碰撞体积的。可以配置屏障方块来给模型添加碰撞体积。也可以配置光源方块,让模型发光。 同时为了不影响世界中已经存在的方块,在模型被创建时,会检查目标位置的方块是否为空气,如果是,配置好的方块才会被放置

定义模型

秉持可复用原则,需要先定义模型,再创建模型



定义模型
1
data modify storage skmws reg.model.<模型ID> set value <模型数据>
参数说明
<模型ID> 模型的ID,这是唯一的
<模型数据> 一个包含该模型所有数据的复合标签,格式如下
  • (根标签)
    • elements 元素列表
      • (一个元素)
        • type 实体类型,可选值为item_displaytext_displayblock_displayinteraction

          type:"interaction"
        • id 元素ID,元素列表中所有元素的ID不能重复
        • events 事件列表
          • leftclick (可选)一个事件列表,左键点击时执行的事件
          • rightclick (可选)一个事件列表,右键点击时执行的事件
        • merge (可选)合并数据
        • position (可选)相对位置

          type:"item_display"type:"text_display"type:"block_display"
        • id 元素ID,元素列表中所有元素的ID不得重复
        • merge (可选)合并数据
        • position (可选)相对位置
        • rotation (可选)相对旋转角

    • marker (可选)标记实体配置
      • merge 合并数据
    • blocks (可选)方块列表
      • (一个方块)
        • position 方块的相对位置
        • block 方块ID
    • events 事件列表
    • align_position (可选)对齐坐标,若无该项则不进行坐标对齐,列表中有三个数,对应XYZ三轴,实际坐标为不大于当前坐标且能被该值整除的最大数字,填入-1则表示不对该轴坐标进行对齐,示例:[1,-1,1]表示XZ轴对齐方块网格,Y轴不进行对齐
    • align_rotation (可选)约束偏航角,若无该项则不进行偏航角约束,示例:输入90代表实际偏航角被约束至东南西北四个方向之一,输入45代表实际偏航角被约束至八个基本方向
    • lock_rotation (可选)锁定旋转角,让旋转角恒为指定值,如果该项与align_rotation同时存在,则优先使用该项

事件列表

事件列表的执行者为该模型的Marker
可以使用@a [tag=skmws.s]来指定正在执行交互操作的玩家

  • (一个事件列表)
    • (一个事件)
      • type 事件类型

        type:"remove"时,移除该模型,同时触发on_remove事件列表(无需提供额外参数)

        type:"destroy"时,破坏该模型,同时触发on_remove事件列表
      • sound 破坏音效
      • particle 破坏粒子

        type:"cooldown"时,设置交互冷却时间,在该时间段内模型不接受任何操作
      • time 冷却时间

        type:"sit"时,让执行交互操作的玩家坐在该模型上
      • id 元素ID,指定让玩家坐到哪个元素上

        type:"call"时,执行另一事件列表,完毕后接着执行当前事件列表
      • event 事件列表ID

        type:"cmd"时,执行指定命令
      • cmd 要执行的命令

        type:"execute"时,让指定元素执行指定命令
      • id 元素ID,作为执行者
      • cmd 执行的命令

        type:"merge"时,合并数据至指定元素
      • id 元素ID
      • data 合并数据

        type:"element_append"时,添加元素
      • id 元素ID
      • data (一个元素)

        type:"element_remove"时,移除元素
      • id 元素ID

        type:"block_append"时,添加方块
      • position 放置位置
      • block 方块ID

        type:"block_remove"时,移除方块
      • position 要被移除的方块的位置



此外这些事件还有其对应的函数,{大括号里是函数的参数呢}
可以在type:"cmd"事件执行的命令中使用这些函数,不能在其他的上下文中使用

skmws:event/remove
skmws:event/destroy {sound, particle}
skmws:event/cooldown {time}
skmws:event/sit {id}
skmws:event/call {event}
skmws:event/cmd {cmd}
skmws:event/execute {id, cmd}
skmws:event/merge {id, data}
skmws:event/element_append {id, data}
skmws:event/element_remove {id}
skmws:event/block_append {position, block}
skmws:event/block_remove {position}

创建模型

模型被定义后,需要“创建”才能将模型安放在世界中

创建模型
1
function skmws:construct
执行者 任意
执行位置 模型的创建位置
执行旋转角 模型创建时的旋转角
参数
  • (根标签)
    • id 模型ID

例程:装饰模型

装饰模型,就是字面意思,用于装饰的模型,需要实现的功能也很简单,如下

  • 展示自定义模型(使用物品展示实体)
  • 模型可交互(使用交互实体)
  • 左键点击时破坏模型(在leftclick事件列表中加入destroy事件)
  • 让模型对齐方块网格(添加align_position参数)
  • 让模型偏航角约束至八个基本方向(添加align_rotation参数)
  • 拥有一格高的碰撞箱(配置屏障方块)

定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
data modify storage skmws reg.model.test01 set value 
{
elements:[
{
type:"item_display",
id:"display",
merge:{
item:{
id:"acacia_boat",
components:{
custom_model_data:{strings:["15230006"]}
}
}
},
rotation:[180,0]
},
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:1.01
},
events:{
leftclick:[
{type:"destroy",playsound:"block.cherry_wood.break",particle:"cherry_planks"}
]
}
}
],
align_position:[1,1,1],
align_rotation:45,
blocks:[
{block:"barrier",position:[0,0,0]}
]
}

使用如下命令创建该模型

1
/function skmws:construct {id:"test01"}

技术细节

  • 交互实体的宽和高设定为1.01(比1稍大),为了防止玩家点到屏障
  • 自定义模型需要用到custom_model_data组件

效果展示

例程:更丝滑的门

原版MC的门没有开关门动画,现在来制作一个带有开关门动画的门

  • 使用方块展示实体显示门的模型,门分为上下两部分,所以需要用两个方块展示实体
  • 展示实体插值实现门的开关动画
  • 左键点击门即可破坏
  • 右键点击门可以打开或关闭门
  • 门关闭时,放置屏障阻止玩家通行;门打开时,移除屏障

定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
data modify storage skmws reg.model.door set value 
{
elements:[
{
type:"interaction",
id:"interact",
merge:{
width:1.01,
height:2
},
events:{
leftclick:[
{type:"destroy",playsound:"block.cherry_wood.break",particle:"cherry_planks"}
],
rightclick:[
{type:"cmd",cmd:"function skm:door/_update"}
]
}
},
{
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]
}
],
marker:{
merge:{
data:{door:0}
}
},
blocks:[
{position:[0,0,0],block:"barrier"},
{position:[0,1,0],block:"barrier"}
],
align_position:[1,1,1],
align_rotation:90
}

在刚才的定义中,Marker中存储了一项名为door的数据,这个表示门的开关状态,0表示关闭,1表示打开
然后还需要单独写一个函数来处理右键点击时开关门动作
这些功能并不由SK Model Workspace数据包提供,而是在实际开发中按照需求自行编写,这体现了该数据包的可拓展性

1
2
3
4
5
6
7
8
9
# skm:door/_update

execute store result score door skmws run data get entity @s data.door 1
scoreboard players add door skmws 1
execute if score door skmws matches 2 run scoreboard players set door skmws 0
execute store result entity @s data.door int 1 run scoreboard players get door skmws

execute if score door skmws matches 0 run function skm:door/_close
execute if score door skmws matches 1 run function skm:door/_open
1
2
3
4
5
6
7
8
9
# skm:door/_open

playsound block.cherry_wood_door.open block @a ~ ~ ~

function skmws:event/block_remove {position:[0,0,0]}
function skmws:event/block_remove {position:[0,1,0]}

function skmws:event/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}}
function skmws:event/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}}
1
2
3
4
5
6
7
8
9
# skm:door/_close

playsound block.cherry_wood_door.close block @a ~ ~ ~

function skmws:event/block_append {position:[0,0,0],block:"barrier"}
function skmws:event/block_append {position:[0,1,0],block:"barrier"}

function skmws:event/merge {id:"upper",data:{transformation:{left_rotation:{axis:[0,1,0],angle:0},translation:[0,0,0]},start_interpolation:0,interpolation_duration:5}}
function skmws:event/merge {id:"lower",data:{transformation:{left_rotation:{axis:[0,1,0],angle:0},translation:[0,0,0]},start_interpolation:0,interpolation_duration:5}}

效果展示