Files
2025-08-25 18:36:29 +08:00

1191 lines
50 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 进阶
time: 80分钟
---
# 开始添加家具方块
除了农作物外,我们还需要继续制作家具,也是地图中重要的玩法之一;同样使用新自定义方块的方法,不过需要额外给不同的家具添加不同的功能。
![3](./images/3.png)
## 学会设置方块的转向
当我们制作自定义方块的时候,如果方块模型的四个方向都是一样的,那方块在什么方向放置就不重要了;但如果是家具方块,如沙发,当玩家放下的时候,沙发的靠背朝前那就不太合适了,所以我们需要设置方块的转向,使每次放置都能保证方块的正面是适合的;
我们需要在方块中添加rotation的属性[0,1,2,3]对应四个方向:东南西北。然后添加四个组合,利用**"minecraft:rotation"**组件对应这四个方向的旋转角度并且在放置方块的时候获取玩家朝向的角度然后根据角度的不同修改方块的rotation属性以rotation属性为条件添加组合代码如下
```json
{
"format_version": "1.16.100",
"minecraft:block": {
"description": {
"identifier": "farm:water_tap",
"properties": {
"farm:rotation":[0,1,2,3] //旋转属性
}
},
"permutations": [
{
"condition": "query.block_property('farm:rotation') == 0", //当下方获取到的玩家角度是2时2-2=0North
"components": {
"minecraft:rotation": [ //设置方块旋转角度
0,
180,
0
]
}
},
{
"condition": "query.block_property('farm:rotation') == 1", //当下方获取到的玩家角度是2时3-2=1South
"components": {
"minecraft:rotation": [ //设置方块旋转角度
0,
0,
0
]
}
},
{
"condition": "query.block_property('farm:rotation') == 2", //当下方获取到的玩家角度是2时4-2=2West
"components": {
"minecraft:rotation": [ //设置方块旋转角度
0,
270,
0
]
}
},
{
"condition": "query.block_property('farm:rotation') == 3", //当下方获取到的玩家角度是2时5-2=3East
"components": {
"minecraft:rotation": [ //设置方块旋转角度
0,
90,
0
]
}
}
],
"components": {
"minecraft:loot": "loot_tables/blocks/water_tap.json",
"minecraft:explosion_resistance": 1,
"minecraft:pick_collision": {
"origin": [-8, 0, -8],
"size": [16, 10, 16]
},
"minecraft:geometry": "geometry.water_tap",
"minecraft:block_light_absorption": 0,
"minecraft:material_instances": {
"*": {
"texture": "farm:water_tap",
"render_method": "alpha_test",
"ambient_occlusion": false
}
},
"minecraft:on_player_placing": { //玩家放置方块的时候触发事件
"event": "farm:set_rotation"
},
"minecraft:entity_collision": {
"origin": [-8, 0, -8],
"size": [16, 10, 16]
}
},
"events": {
"farm:set_rotation": {
"set_block_property": { //设置方块属性
"farm:rotation": "query.cardinal_facing_2d-2"
//使用表达式获取玩家的方向(North=2.0, South=3.0, West=4.0, East=5.0, Undefined=6.0)
//因为上方的方块属性是从0开始的所以获取到的数字需要-2才能对应上方的方块属性
}
}
}
}
}
```
<img src="./images/24.gif" alt="24" style="zoom:115%;" />
## 制作可以拼在一起的桌子
这里需要用到中国版自定义模型方块的写法微软的1.16+自定义方块写法暂时还无法制作此功能;[认识中国版自定义方块](../../10-addon教程/第09章自定义方块/课程01.认识自定义方块.md)
给桌子方块添加基本的组件后,还需要用到**"netease:connection"**用来定义这个方块的拼接属性。
```json
{
"format_version": "1.10.0",
"minecraft:block": {
"description": {
"identifier": "farm:connect_table"
},
"components": {
"minecraft:block_light_absorption":{
"value": 0
},
"netease:render_layer": {
"value": "alpha"
},
"netease:solid": {
"value": false
},
"netease:connection": {
"blocks": ["farm:connect_table"]
}
}
}
}
```
然后打开桌子的模型文件,我们需要进行修改以实现桌子拼接时,部分模型块会消失,如桌子四周的挡板:
```json
{
"cubes": [···],
"enable": "!query.is_connect(2)", //用表达式判断方块连接面的方向(连接面为北时启用这个部分的模型,因为前面加了!,所以为不启用这部分的模型)
// 0-down面1-up面2-north面3-south面4-west面5-east面
"name": "n", //这个部分是桌子的背面挡板
"parent": "table",
"pivot": [···],
"rotation": [···]
},
```
使用上面的方法依次将东南西北四个方向的挡板都添加,点击编辑器的**开发测试**进入游戏,就可以从下方看到当桌子连接起来的时候,侧面的挡板消失了;
![25](./images/25.png)
![26](./images/26.png)
相比桌子的挡板,桌角并不是在某一侧,而是两个方向的夹角,所以只判断某一个方向的连接是不行的,需要两个方向都有桌子进行链接的时候才消失:
```json
{
"cubes": [···],
"enable": "!query.is_connect(5) && !query.is_connect(3)", //桌子的东面和南面都有桌子时这个部分的模型不启用
// 0-down面1-up面2-north面3-south面4-west面5-east面
"name": "east_south", //东面和南面夹角的桌角
"parent": "table",
"pivot": [···],
"rotation": [···]
},
```
把所有的桌脚都设置好后,进入游戏是这样的
![27](./images/27.png)
## 制作带有开关的装饰方块
开关可以添加方块属性来进行判断,给台灯和电视都添加一个属性,值为[0,1]对应开和关然后利用MODSDK来切换方块属性
```json
{
"format_version": "1.16.100",
"minecraft:block": {
"description": {
"identifier": "farm:acacia_table_lamp", //台灯
"properties": {
"farm:open_light" : [0, 1] //添加属性用作开灯的判断
}
},
"permutations": [
{
"condition": "query.block_property('farm:open_light') == 1", //当属性open_light为1时
"components": {
"minecraft:block_light_emission": 1 //方块发光
}
}
],
"components": {···},
"events": {···}
}
}
```
```python
import time
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.interact_cooldown = {} #创建一个时间戳变量,用于控制玩家交互方块的冷却时间,以免造成极短时间内多次交互的情况
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
# 获取玩家ID
player_id = event['playerId']
# 获取事件里交互的方块类型
block_name = event['blockName']
# 创建获取方块信息的接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
# 获取交互方块的坐标
x = event['x']
y = event['y']
z = event['z']
# 所有台灯的列表
table_lamp_list = ["farm:acacia_table_lamp", "farm:bigoak_table_lamp", "farm:birch_table_lamp",
"farm:crimson_table_lamp", "farm:jungle_table_lamp", "farm:oak_table_lamp",
"farm:spruce_table_lamp", "farm:warped_table_lamp"]
#如果交互的方块在台灯列表呢
if block_name in table_lamp_list:
#获取交互的方块信息
state = blockstatecomp.GetBlockStates((x, y, z), 0)
#如果玩家id不在时间戳变量内
if player_id not in self.interact_cooldown:
#给时间戳变量添加一个玩家id和时间戳的键值
self.interact_cooldown[player_id] = time.time()
#如果方块的属性open_light等于0台灯还没有打开
if state['farm:open_light'] == 0:
#修改台灯的open_light属性为1
state['farm:open_light'] = 1
#设置修改后的台灯属性
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#反之方块属性open_light等于1台灯如果已经时开启的了
else:
#修改台灯的open_light属性为0
state['farm:open_light'] = 0
#设置修改后的台灯属性
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#如果当前的时间减去存储在时间戳变量内的时间大于0.1
elif time.time() - self.interact_cooldown[player_id] > 0.1:
#逻辑与上方的if一致
if state['farm:open_light'] == 0:
state['farm:open_light'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
else:
state['farm:open_light'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#更新时间戳变量中的时间
self.interact_cooldown[player_id] = time.time()
```
<img src="./images/30.gif" alt="30" style="zoom:115%;" />
电视和台灯同理,只需要简单修改一些判断条件即可:
```json
{
"format_version": "1.16.100",
"minecraft:block": {
"description": {
"identifier": "farm:logo_tv", //电视
"properties": {
"farm:open_tv": [0,1] //添加属性用作开电视的判断
}
},
"permutations": [
{
"condition": "query.block_property('farm:open_tv') == 1", //当属性open_tv为1时
"components": {
"minecraft:material_instances": { //修改方块的贴图
"*": {
"texture": "farm:logo_tv_open",
"render_method": "alpha_test",
"ambient_occlusion": false
}
}
}
}
],
"components": {···},
"events": {···}
}
}
```
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
if block_name == "farm:logo_tv":
state = blockstatecomp.GetBlockStates((x, y, z), 0)
if player_id not in self.interact_cooldown:
self.interact_cooldown[player_id] = time.time()
print state
if state['farm:open_tv'] == 0:
state['farm:open_tv'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
else:
state['farm:open_tv'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
elif time.time() - self.interact_cooldown[player_id] > 0.1:
if state['farm:open_tv'] == 0:
state['farm:open_tv'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
else:
state['farm:open_tv'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
self.interact_cooldown[player_id] = time.time()
```
## 制作自定义床方块
自定义床方块比较复杂因为床需要和史蒂夫一样长也就是两格方块左右但是自定义方块的碰撞箱最大就是16x16所以需要两个方块拼接成一个床我们需要使用modsdk来控制床的放置和破坏
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name) # 监听玩家放置方块前后和破坏方块的事件
self.ListenForEvent(namespace, system_name,'EntityPlaceBlockAfterServerEvent', self, self.place_block)
self.ListenForEvent(namespace, system_name,'ServerPlayerTryDestroyBlockEvent', self, self.player_destroy_block
self.ListenForEvent(namespace, system_name,'ServerEntityTryPlaceBlockEvent', self, self.try_place_block)
# 玩家放置方块成功后触发
def place_block(self, args):
# 现代床的下半部分
modern_bed_tail = {
'name': 'farm:modern_bed_tail',
'aux': 0
}
# 通过事件获取到放置的方块名、方块坐标、玩家id、放置方块的世界id、
x = args["x"]
y = args["y"]
z = args["z"]
blockname = args["fullName"]
playerid = args["entityId"]
worldid = args["dimensionId"]
# 创建方块相关的接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
blockcomp = serverApi.GetEngineCompFactory().CreateBlockInfo(serverApi.GetLevelId())
# 如果放置的方块是farm:modern_bed_head(现代床的上半部分)
if blockname == "farm:modern_bed_head":
# 获取这个方块的属性
block_state = blockstatecomp.GetBlockStates((x, y, z), worldid)
# 通过坐标获取到这个方块东西南北的方块信息
east_block_state = blockstatecomp.GetBlockStates((x + 1, y, z), worldid)
west_block_state = blockstatecomp.GetBlockStates((x - 1, y, z), worldid)
south_block_state = blockstatecomp.GetBlockStates((x, y, z + 1), worldid)
north_block_state = blockstatecomp.GetBlockStates((x, y, z - 1), worldid)
# 通过if判断上半部分的放置方向
if block_state["farm:rotation"] == 0:
# 在上半部分的方块方向的反方向放置下半部分方块
blockcomp.SetBlockNew((x, y, z + 1), modern_bed_tail, 0, worldid)
block_state['farm:rotation'] = 0
blockstatecomp.SetBlockStates((x, y, z + 1), block_state, worldid)
elif block_state["farm:rotation"] == 1:
blockcomp.SetBlockNew((x, y, z - 1), modern_bed_tail, 0, worldid)
block_state['farm:rotation'] = 1
blockstatecomp.SetBlockStates((x, y, z - 1), block_state, worldid)
elif block_state["farm:rotation"] == 2:
blockcomp.SetBlockNew((x + 1, y, z), modern_bed_tail, 0, worldid)
block_state['farm:rotation'] = 2
blockstatecomp.SetBlockStates((x + 1, y, z), block_state, worldid)
elif block_state["farm:rotation"] == 3:
blockcomp.SetBlockNew((x - 1, y, z), modern_bed_tail, 0, worldid)
block_state['farm:rotation'] = 3
blockstatecomp.SetBlockStates((x - 1, y, z), block_state, worldid)
# 玩家破坏方块时触发
def player_destroy_block(self, args):
# 方块信息字典(空气)
air = {
'name': 'minecraft:air',
'aux': 0
}
# 方块列表(现代床的上半部和下半部分)
block_list = ['farm:modern_bed_head', 'farm:modern_bed_tail']
# 通过事件获取破坏的方块名称、坐标、玩家id和世界id
blockname = args['fullName']
x = args['x']
y = args['y']
z = args['z']
blockname = args['fullName']
playerid = args['playerId']
worldid = args['dimensionId']
# 创建方块相关的接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
blockcomp = serverApi.GetEngineCompFactory().CreateBlockInfo(serverApi.GetLevelId())
# 如果方块名是farm:modern_bed_head现代床的上半部分
if blockname == "farm:modern_bed_head":
# 获取这个方块的属性
block_state = blockstatecomp.GetBlockStates((x, y, z), worldid)
# 用if判断方块的旋转属性并同时破坏反方向一格的方块反方向就是下半部分
if block_state['farm:rotation'] == 0:
blockcomp.SetBlockNew((x, y, z + 1), air, 0, worldid)
elif block_state['farm:rotation'] == 1:
blockcomp.SetBlockNew((x, y, z - 1), air, 0, worldid)
elif block_state['farm:rotation'] == 2:
blockcomp.SetBlockNew((x + 1, y, z), air, 0, worldid)
elif block_state['farm:rotation'] == 3:
blockcomp.SetBlockNew((x - 1, y, z), air, 0, worldid)
# 如果方块名是farm:modern_bed_tail现代床的下半部分
elif blockname == "farm:modern_bed_tail":
# 获取这个方块的属性
block_state = blockstatecomp.GetBlockStates((x, y, z), worldid)
# 用if判断方块的旋转属性并同时破坏反方向一格的方块反方向就是上半部分
if block_state['farm:rotation'] == 0:
blockcomp.SetBlockNew((x, y, z - 1), air, 0, worldid)
elif block_state['farm:rotation'] == 1:
blockcomp.SetBlockNew((x, y, z + 1), air, 0, worldid)
elif block_state['farm:rotation'] == 2:
blockcomp.SetBlockNew((x - 1, y, z), air, 0, worldid)
elif block_state['farm:rotation'] == 3:
blockcomp.SetBlockNew((x + 1, y, z), air, 0, worldid)
# 玩家尝试放置方块时触发
def try_place_block(self,args):
# 通过事件获取方块的坐标、名称、玩家id、世界id
x = args["x"]
y = args["y"]
z = args["z"]
blockname = args["fullName"]
playerid = args["entityId"]
worldid = args["dimensionId"]
# 创建方块相关接口
blockinfocomp = serverApi.GetEngineCompFactory().CreateBlockInfo(serverApi.GetLevelId())
# 获取getplayerrot的返回值
playerrot = self.getplayerrot(playerid)
# 获取消息相关的接口
msgcomp = serverApi.GetEngineCompFactory().CreateMsg(playerid)
# 如果放置的方块时farm:modern_bed_head现代床的上半部分
if blockname == "farm:modern_bed_head":
# 如果玩家的视角是2
if playerrot == 2:
# 获取视角方向的方块信息字典
south_block = blockinfocomp.GetBlockNew((x,y,z+1), worldid)
# 如果方块的名字不是空气(说明有方块阻挡)
if south_block['name'] != "minecraft:air":
# 取消放置方块
args['cancel'] = True
# 给尝试放置的玩家提醒
msgcomp.NotifyOneMessage(playerid, "附近有方块遮挡,无法放置方块", "§c")
# 下面的if分支同上只是获取的玩家视角角度不同
elif playerrot == 3:
west_block = blockinfocomp.GetBlockNew((x - 1,y,z), worldid)
if west_block['name'] != "minecraft:air":
args['cancel'] = True
msgcomp.NotifyOneMessage(playerid, "附近有方块遮挡,无法放置方块", "§c")
elif playerrot == 0:
north_block = blockinfocomp.GetBlockNew((x,y,z-1), worldid)
if north_block['name'] != "minecraft:air":
args['cancel'] = True
msgcomp.NotifyOneMessage(playerid, "附近有方块遮挡,无法放置方块", "§c")
elif playerrot == 1:
east_block = blockinfocomp.GetBlockNew((x+1,y,z), worldid)
if east_block['name'] != "minecraft:air":
args['cancel'] = True
msgcomp.NotifyOneMessage(playerid, "附近有方块遮挡,无法放置方块", "§c")
# 获取玩家视角角度的函数
def getplayerrot(self,playerid):
# 获取玩家角度
rot = serverApi.GetEngineCompFactory().CreateRot(playerid).GetRot()
# 根据不同角度返回不同的数字用于判断
if 135.0 < rot[1] <= 180.0:
return 2
elif 90.0 < rot[1] <= 135.0:
return 1
elif 45.0 < rot[1] <= 90.0:
return 1
elif 0.0 < rot[1] <= 45.0:
return 0
elif -45.0 < rot[1] <= 0.0:
return 0
elif -90.0 < rot[1] <= -45.0:
return 3
elif -135.0 < rot[1] <= -90.0:
return 3
elif -180.0 < rot[1] <= -135.0:
return 2
else:
return 0
```
床方块的放置和破坏逻辑写完后,我们还需要对其进行交互功能相关的逻辑,使玩家可以设置出生点并睡在床上跳过一段时间:
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
# 监听方块交互时间
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
# 时间戳变量
self.interact_cooldown = {}
def using_item(self, event):
# 创建 计时器接口
timercomp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
# 创建玩家行为的接口
playercomp = serverApi.GetEngineCompFactory().CreatePlayer(player_id)
# 创建获取方块信息的接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
# 创建使用指令的接口
commandcomp = serverApi.GetEngineCompFactory().CreateCommand(serverApi.GetLevelId())
# 如果交互的方块是现代床
if block_name == "farm:modern_bed_head":
# 获取方块属性
state = blockstatecomp.GetBlockStates((x, y, z), 0)
# 存储一些需要的参数
camera_event = {'state': state, 'x': x, 'y': y, 'z': z, 'playerid': player_id}
# 设置玩家重生点
commandcomp.SetCommand("spawnpoint @s ~ ~ ~")
# 如果方块的旋转属性为2判断方块的放置方向并且根据方向设置玩家的视角
if state['farm:rotation'] == 2:
# 使用指令播放玩家睡觉的动画
commandcomp.SetCommand("playanimation @s east_sleep")
# 使用指令将玩家传送到床上
commandcomp.SetCommand("tp @s {} {} {}".format(x,y+0.5,z))
# 设置玩家无法移动
playercomp.SetPlayerMovable(False)
# 传输事件到客户端,锁定玩家视角到固定位置
self.NotifyToClient(player_id, "camera_lock", camera_event)
# 添加一个4秒的计时器
timercomp.AddTimer(4.0,self.standup,player_id,)
#同上if只是判断不同的旋转角度播放不同方向的玩家动画和视角
elif state['farm:rotation'] == 0:
commandcomp.SetCommand("tp @s {} {} {}".format(x,y+0.5,z))
commandcomp.SetCommand("playanimation @s west_sleep humanoid_base_pose 0")
playercomp.SetPlayerMovable(False)
self.NotifyToClient(player_id, "camera_lock", camera_event)
timercomp.AddTimer(4.0,self.standup,player_id,)
elif state['farm:rotation'] == 1:
commandcomp.SetCommand("tp @s {} {} {}".format(x,y+0.5,z))
commandcomp.SetCommand("playanimation @s north_sleep humanoid_base_pose 0")
playercomp.SetPlayerMovable(False)
self.NotifyToClient(player_id, "camera_lock", camera_event)
timercomp.AddTimer(4.0,self.standup,player_id,)
elif state['farm:rotation'] == 3:
commandcomp.SetCommand("tp @s {} {} {}".format(x,y+0.5,z))
commandcomp.SetCommand("playanimation @s south_sleep humanoid_base_pose 0")
playercomp.SetPlayerMovable(False)
self.NotifyToClient(player_id, "camera_lock", camera_event)
timercomp.AddTimer(4.0,self.standup,player_id,)
# 计时器触发的函数
def standup(self,player_id):
# 获取相关接口
timecomp = serverApi.GetEngineCompFactory().CreateTime(serverApi.GetLevelId())
playercomp = serverApi.GetEngineCompFactory().CreatePlayer(player_id)
# 设置玩家可以移动
playercomp.SetPlayerMovable(True)
# 从游戏开始经过的总帧数
passedTime = timecomp.GetTime()
# 当前游戏天内的帧数
timeOfDay = passedTime % 24000
# 从游戏开始经过的游戏天数
day = passedTime / 24000
# 使用时间戳,避免多次执行的情况
if player_id not in self.interact_cooldown:
self.interact_cooldown[player_id] = time.time()
# 根据当前时间+12000
timecomp.SetTime(passedTime + 12000)
print day
elif time.time() - self.interact_cooldown[player_id] > 1:
timecomp.SetTime(passedTime + 12000)
self.interact_cooldown[player_id] = time.time()
print day
```
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
# 监听由服务端传输过来的事件
self.ListenForEvent("FarmMod", "ServerBlockListenerServer", "camera_lock",self, self.Camera_Lock)
def Camera_Lock(self,event):
# 获取传输过来的参数:方块的坐标和属性
x = event['x']
y = event['y']
z = event['z']
state = event['state']
# 判断方块的旋转属性
if state['farm:rotation'] == 0:
# 锁定玩家视角到特定的位置
cameracomp.LockCamera((x,y+1,z), (-40, 0))
# 添加一个4s的计时器
timercomp.AddTimer(4.0, self.Camera_UnLock, event, )
elif state['farm:rotation'] == 2:
cameracomp.LockCamera((x,y+1,z), (-40, -90))
timercomp.AddTimer(4.0, self.Camera_UnLock, event, )
elif state['farm:rotation'] == 3:
cameracomp.LockCamera((x,y+1,z), (-40, -270))
timercomp.AddTimer(4.0, self.Camera_UnLock, event, )
elif state['farm:rotation'] == 1:
cameracomp.LockCamera((x,y+1,z), (-40, -180))
timercomp.AddTimer(4.0, self.Camera_UnLock, event, )
# 计时器触发的函数
def Camera_UnLock(self,event):
# 解锁玩家的视角
cameracomp.UnLockCamera()
```
现在,我们已经有了一个较完整的现代床,在编辑器中点击**开发测试**进入到游戏中试一下:
<img src="./images/31.gif" alt="31" style="zoom:115%;" />
## 制作可以回收道具的垃圾桶
这个功能只需要使用MODSDK即可做到不需要对自定方块的组件进行复杂修改代码如下
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.ListenForEvent(namespace, system_name,
'ServerItemUseOnEvent', self, self.using_block) #监听ServerItemUseOnEvent事件
def using_block(self,args):
trash_can_list = ['farm:acacia_trash_can', 'farm:bigoak_trash_can', 'farm:birch_trash_can',
'farm:crimson_trash_can', 'farm:jungle_trash_can', 'farm:oak_trash_can',
'farm:spruce_trash_can', 'farm:warped_trash_can'] #所有的垃圾桶方块
playerid = args['entityId'] #事件中获取到的玩家id
blockname = args['blockName'] #事件中获取到的玩家交互方块的identifier
itemname = args['itemDict'] #事件中获取到的物品信息字典
item_comp = serverApi.GetEngineCompFactory().CreateItem(playerid) #创建CreateItem接口
if blockname in trash_can_list and itemname: #如果交互的方块在trash_can_list的列表中并且玩家手里有物品
args['ret'] = True #取消手中物品的使用(这个可以防止玩家手里的物品或方块被放置到地上从而无法清除)
carried_item = item_comp.GetPlayerItem(ItemPosType.CARRIED, 0, True) #获取玩家手中的物品信息
carried_item['count'] = 0 #将获取到的物品的数量设置为0
item_comp.SetPlayerAllItems({(ItemPosType.CARRIED, 0): carried_item}) #重新设置玩家手里的物品
```
在ServerSystem中只需要监听**ServerItemUseOnEvent**事件并且添加一些简单的逻辑即可不过这个事件建议在客户端同时监听并取消物品的使用所以在ClientSystem中也需要一些代码
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
self.ListenForEvent(namespace, system_name,
'ClientItemUseOnEvent', self, self.using_item) #监听事件
def using_item(self,args):
trash_can_list = ['farm:acacia_trash_can', 'farm:bigoak_trash_can', 'farm:birch_trash_can',
'farm:crimson_trash_can', 'farm:jungle_trash_can', 'farm:oak_trash_can',
'farm:spruce_trash_can', 'farm:warped_trash_can'] #所有的垃圾桶方块
blockname = args['blockName'] #事件中获取到的玩家交互方块的identifier
itemname = args['itemDict'] #事件中获取到的物品信息字典
if blockname in trash_can_list and itemname: #如果交互的方块在trash_can_list的列表中并且玩家手里有物品
args['ret'] = True #取消手中物品的使用
```
<img src="./images/29.gif" alt="29" style="zoom:115%;" />
## 制作可以榨取果汁的榨汁机
榨汁机工作需要用到我们收获的农作物:柠檬、菠菜、玉米,分别对应:柠檬汁、菠菜汁和玉米汁
给榨汁机添加多个属性分别代表 “判断榨汁机运行”、“榨玉米汁“、”榨柠檬汁“、”榨菠菜汁“然后添加对应的组合通过事件来进行调控不过仅用自定义方块的组件内容无法制作全部的功能我们还需要使用MODSDK
```json
{
"format_version": "1.16.100",
"minecraft:block": {
"description": {
"identifier": "farm:juicer",
"properties": {
"farm:run":[0,1], //判断榨汁机有没有在运行中
"farm:juicer_has_corn":[0,1,2], //榨玉米汁的过程
"farm:juicer_has_lemon":[0,1,2], //榨柠檬汁的过程
"farm:juicer_has_spinach":[0,1,2] //榨菠菜汁的过程
}
},
"permutations": [
{
"condition": "query.block_property('farm:juicer_has_corn') == 1", //满足条件,添加下方组件(此时榨汁机在榨汁中:玉米)
"components": {
"minecraft:material_instances": { //修改榨汁机的贴图
"*": {
"texture": "farm:juicer_corn",
"render_method": "alpha_test",
"ambient_occlusion": false
}
},
"minecraft:ticking": { //添加计时器3秒后运行下方事件不重复运行
"range": [3.0, 3.0],
"looping": false,
"on_tick": {
"event": "farm:corn_juice_start",
"target": "self"
}
}
}
},
{
"condition": "query.block_property('farm:juicer_has_corn') == 2", //满足条件,添加下方组件(此时榨汁机榨完了:玉米)
"components": {
"minecraft:material_instances": { //修改榨汁机贴图
"*": {
"texture": "farm:juicer_corn_bottle",
"render_method": "alpha_test",
"ambient_occlusion": false
}
}
}
},
{
"condition": "query.block_property('farm:juicer_has_spinach') == 1", //满足条件,添加下方组件(此时榨汁机在榨汁中:菠菜)
"components": {
"minecraft:material_instances": { //修改榨汁机贴图
"*": {
"texture": "farm:juicer_spinach",
"render_method": "alpha_test",
"ambient_occlusion": false
}
},
"minecraft:ticking": { //添加计时器3秒后运行下方事件不重复运行
"range": [3.0, 3.0],
"looping": false,
"on_tick": {
"event": "farm:spinach_juice_start",
"target": "self"
}
}
}
},
{
"condition": "query.block_property('farm:juicer_has_spinach') == 2", //满足条件,添加下方组件(此时榨汁机榨完了:菠菜)
"components": {
"minecraft:material_instances": { //修改榨汁机贴图
"*": {
"texture": "farm:juicer_spinach_bottle",
"render_method": "alpha_test",
"ambient_occlusion": false
}
}
}
},
{
"condition": "query.block_property('farm:juicer_has_lemon') == 1", //满足条件,添加下方组件(此时榨汁机在榨汁中:柠檬)
"components": {
"minecraft:material_instances": { //修改榨汁机贴图
"*": {
"texture": "farm:juicer_lemon",
"render_method": "alpha_test",
"ambient_occlusion": false
}
},
"minecraft:ticking": { //添加计时器3秒后运行下方事件不重复运行
"range": [3.0, 3.0],
"looping": false,
"on_tick": {
"event": "farm:lemon_juice_start",
"target": "self"
}
}
}
},
{
"condition": "query.block_property('farm:juicer_has_lemon') == 2", //满足条件,添加下方组件(此时榨汁机榨完了:柠檬)
"components": {
"minecraft:material_instances": { //修改榨汁机贴图
"*": {
"texture": "farm:juicer_lemon_bottle",
"render_method": "alpha_test",
"ambient_occlusion": false
}
}
}
}
],
"components": {
"minecraft:loot": "loot_tables/blocks/juicer.json",
"minecraft:explosion_resistance": 1,
"minecraft:pick_collision": {
"origin": [-8, 0, -8],
"size": [16, 16, 16]
},
"minecraft:material_instances": { //榨汁机普通情况下的贴图
"*": {
"texture": "farm:juicer",
"render_method": "alpha_test",
"ambient_occlusion": false
}
},
"minecraft:geometry": "geometry.juicer", //榨汁机的模型
"minecraft:block_light_absorption": 0,
"minecraft:entity_collision": {
"origin": [-8, 0, -8],
"size": [16, 16, 16]
}
},
"events": {
"farm:corn_juice_start":{
"set_block_property": { //设置属性juicer_has_cron为2(从运行榨汁机到榨完汁)
"farm:juicer_has_corn": 2
}
},
"farm:spinach_juice_start":{ //设置属性juicer_has_spinach为2(从运行榨汁机到榨完汁)
"set_block_property": {
"farm:juicer_has_spinach": 2
}
},
"farm:lemon_juice_start":{ //设置属性juicer_has_lemon为2(从运行榨汁机到榨完汁)
"set_block_property": {
"farm:juicer_has_lemon": 2
}
}
}
}
}
```
上面的自定义方块内容可以实现榨汁的过程3秒并且根据状态切换榨汁机的贴图开始榨汁并且拿走果汁的功能就需要MODSDK来实现
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.ListenForEvent(namespace, system_name,
'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
# 获取玩家ID
player_id = event['playerId']
# 创建玩家的物品接口
item_comp = serverApi.GetEngineCompFactory().CreateItem(player_id)
# 获取玩家手持物品信息
carried_item = item_comp.GetPlayerItem(ItemPosType.CARRIED, 0, True)
# 获取事件里交互的方块类型
block_name = event['blockName']
# 创建获取方块信息的接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
# 创建生成掉落物的接口
drop_comp = serverApi.GetEngineCompFactory().CreateItem(serverApi.GetLevelId())
# 获取交互方块的坐标
x = event['x']
y = event['y']
z = event['z']
# 创建一个存储果汁物品信息的变量
juice = [
{
'newItemName': 'farm:corn_juice',
'count': 1,
'newAuxValue': 0,
},
{
'newItemName': 'farm:lemon_juice',
'count': 1,
'newAuxValue': 0,
},
{
'newItemName': 'farm:spinach_juice',
'count': 1,
'newAuxValue': 0,
}
]
#如果交互的方块是榨汁机
if block_name == "farm:juicer":
#获取坐标位置方块的属性
state = blockstatecomp.GetBlockStates((x, y, z), 0)
#如果手里有东西
if carried_item:
#如果方块的属性 has_corn和run都为0 并且手里的是玉米
if state['farm:juicer_has_corn'] == 0 and state['farm:run'] == 0 and carried_item['newItemName'] == "farm:corn_item":
#将has_corn和run改为1并且将手里的玉米-1
state['farm:juicer_has_corn'] = 1
state['farm:run'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
carried_item['count'] -= 1
item_comp.SetPlayerAllItems({(ItemPosType.CARRIED, 0): carried_item})
#如果方块的属性 has_lemon和run都为0 并且手里的是柠檬
elif state['farm:juicer_has_lemon'] == 0 and state['farm:run'] == 0 and carried_item['newItemName'] == "farm:lemon_item":
#将has_lemon和run改为1并且将手里的柠檬-1
state['farm:juicer_has_lemon'] = 1
state['farm:run'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
carried_item['count'] -= 1
item_comp.SetPlayerAllItems({(ItemPosType.CARRIED, 0): carried_item})
#如果方块的属性 has_spinach和run都为0 并且手里的是菠菜
elif state['farm:juicer_has_spinach'] == 0 and state['farm:run'] == 0 and carried_item['newItemName'] == "farm:spinach_item":
#将has_spinach和run改为1并且将手里的菠菜-1
state['farm:juicer_has_spinach'] = 1
state['farm:run'] = 1
blockstatecomp.SetBlockStates((x, y, z), state, 0)
carried_item['count'] -= 1
item_comp.SetPlayerAllItems({(ItemPosType.CARRIED, 0): carried_item})
#当手里有东西时并且榨玉米汁工作完
elif state['farm:juicer_has_corn'] == 2:
#生成掉落物并且将has_corn和run恢复为0
drop_comp.SpawnItemToLevel(juice[0], 0, (x, y+1, z))
state['farm:juicer_has_corn'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#当手里有东西时并且榨柠檬汁工作完
elif state['farm:juicer_has_lemon'] == 2:
#生成掉落物并且将has_lemon和run恢复为0
drop_comp.SpawnItemToLevel(juice[1], 0, (x, y+1, z))
state['farm:juicer_has_lemon'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#当手里有东西时并且榨菠菜汁工作完
elif state['farm:juicer_has_spinach'] == 2:
#生成掉落物并且将has_spinach和run恢复为0
drop_comp.SpawnItemToLevel(juice[2], 0, (x, y+1, z))
state['farm:juicer_has_spinach'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#如果手里没东西
else:
#如果榨玉米汁工作完
if state['farm:juicer_has_corn'] == 2:
drop_comp.SpawnItemToLevel(juice[0], 0, (x, y+1, z))
state['farm:juicer_has_corn'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
# 如果榨柠檬汁工作完
elif state['farm:juicer_has_lemon'] == 2:
#生成掉落物并且将has_lemon和run恢复为0
drop_comp.SpawnItemToLevel(juice[1], 0, (x, y+1, z))
state['farm:juicer_has_lemon'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
#如果榨菠菜汁工作完
elif state['farm:juicer_has_spinach'] == 2:
#生成掉落物并且将has_spinach和run恢复为0
drop_comp.SpawnItemToLevel(juice[2], 0, (x, y+1, z))
state['farm:juicer_has_spinach'] = 0
state['farm:run'] = 0
blockstatecomp.SetBlockStates((x, y, z), state, 0)
else:
pass
```
MODSDK和自定义方块组件的相互配合完成这个榨汁机的全部功能点击编辑器的**开发测试**进入到游戏中测试一下:
<img src="./images/28.gif" alt="28" style="zoom:115%;" />
## 制作可以坐的方块
可以坐下的椅子和前面的现代床实现的方法差不多,并且没有那么的复杂,我们只需要传送玩家到椅子上并且播放坐下的动画即可,不过我们可以添加更多有意思的功能,比如在玩家坐下的时候可以每三秒回复玩家的半格血量:
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
# 判断坐下的变量
self.sit = 0
# 时间戳变量
self.interact_cooldown = {}
# 监听方块交互的事件
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
# 创建 计时器接口
timercomp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
# 创建使用指令的接口
commandcomp = serverApi.GetEngineCompFactory().CreateCommand(serverApi.GetLevelId())
# 创建使用指令的接口
commandcomp = serverApi.GetEngineCompFactory().CreateCommand(serverApi.GetLevelId())
# 事件获取的方块名称和坐标
block_name = event['blockName']
x = event['x']
y = event['y']
z = event['z']
# 所以椅子名称的列表
chair_list = ["farm:acacia_chair","farm:bigoak_chair","farm:birch_chair","farm:crimson_chair",
"farm:jungle_chair","farm:oak_chair","farm:spruce_chair","farm:warped_chair"]
# 如果交互的方块名称给列表内
if block_name in chair_list:
# 通过时间戳避免短时间内多次交互的情况
if player_id not in self.interact_cooldown:
self.interact_cooldown[player_id] = time.time()
# 如果sit为0说明玩家未坐下
if self.sit == 0:
# 使用指令播放玩家坐下的动画
commandcomp.SetCommand("playanimation @s sit_chair")
# 传送玩家到椅子上
commandcomp.SetCommand("tp @s {} {} {}".format(x,y-0.1,z))
# 设置玩家无法移动
playercomp.SetPlayerMovable(False)
# 创建消息接口并提醒玩家再次点击可以站起
msgcomp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msgcomp.NotifyOneMessage(player_id, "再次点击椅子以站起", "§f")
# 创建一个3秒的循环计时器
self.healtimer = timercomp.AddRepeatedTimer(3.0,self.player_heal,player_id)
# 修改坐下的变量为1
self.sit = 1
# 如果变量为1此时玩家在坐着
elif self.sit == 1:
# 设置玩家可以移动
playercomp.SetPlayerMovable(True)
# 取消循环的计时器
timercomp.CancelTimer(self.healtimer)
# 播放玩家普通
commandcomp.SetCommand("playanimation @s humanoid_base_pose")
# 修改坐下的变量为0
self.sit = 0
# 时间戳判断与上方if逻辑一致
elif time.time() - self.interact_cooldown[player_id] > 0.5:
if self.sit == 0:
commandcomp.SetCommand("playanimation @s sit_chair")
commandcomp.SetCommand("tp @s {} {} {}".format(x,y-0.1,z))
playercomp.SetPlayerMovable(False)
msgcomp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msgcomp.NotifyOneMessage(player_id, "再次点击椅子以站起", "§f")
self.healtimer = timercomp.AddRepeatedTimer(3.0, self.player_heal, player_id)
self.sit = 1
elif self.sit == 1:
playercomp.SetPlayerMovable(True)
timercomp.CancelTimer(self.healtimer)
commandcomp.SetCommand("playanimation @s humanoid_base_pose")
self.sit = 0
self.interact_cooldown[player_id] = time.time()
# 计时器循环触发的函数
def player_heal(self,playerid):
# 获取玩家属性的接口
attcomp = serverApi.GetEngineCompFactory().CreateAttr(playerid)
# 获取玩家当前的血量
playerheal = attcomp.GetAttrValue(serverApi.GetMinecraftEnum().AttrType.HEALTH)
# 设置玩家当前的血量并+1
attcomp.SetAttrValue(serverApi.GetMinecraftEnum().AttrType.HEALTH, playerheal+1)
```
椅子的逻辑相对简单,我们点击编辑器的**开发测试**,进入到游戏中测试一下:
<img src="./images/32.gif" alt="32" style="zoom:115%;" />
## 制作其它装饰方块
接下来我们继续填充装饰性的方块家具:马克杯、床头柜、水龙头、熊玩偶(纯装饰),马桶、沙发、浴缸(重复功能)。纯装饰的家具方块只需要添加属性使其可以跟随玩家视角旋转即可,一些重复功能的家具只需要利用上方的逻辑进行简单的修改即可。
马桶模仿椅子的逻辑,在判断方块的部分修改为马桶方块即可:
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
# 如果交互的方块是farm:close_stool马桶
if block_name == "farm:close_stool":
if player_id not in self.interact_cooldown:
self.interact_cooldown[player_id] = time.time()
if self.sit == 0:
commandcomp.SetCommand("tp @s {} {} {}".format(x, y - 0.1, z))
commandcomp.SetCommand("playanimation @s sit_chair")
playercomp.SetPlayerMovable(False)
msgcomp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msgcomp.NotifyOneMessage(player_id, "再次点击马桶以站起", "§f")
self.sit = 1
elif self.sit == 1:
playercomp.SetPlayerMovable(True)
commandcomp.SetCommand("playanimation @s humanoid_base_pose")
self.sit = 0
elif time.time() - self.interact_cooldown[player_id] > 0.5:
if self.sit == 0:
commandcomp.SetCommand("tp @s {} {} {}".format(x, y - 0.1, z))
commandcomp.SetCommand("playanimation @s sit_chair")
playercomp.SetPlayerMovable(False)
msgcomp = serverApi.GetEngineCompFactory().CreateMsg(player_id)
msgcomp.NotifyOneMessage(player_id, "再次点击马桶以站起", "§f")
self.sit = 1
elif self.sit == 1:
playercomp.SetPlayerMovable(True)
commandcomp.SetCommand("playanimation @s humanoid_base_pose")
self.sit = 0
self.interact_cooldown[player_id] = time.time()
```
沙发坐下的逻辑也与椅子、马桶相似,不过需要设置沙发在同行摆放的时候使其可以拼接,参照桌子的拼接方法,在沙发的两侧添加拼接:
```json
{
"cubes": [···],
"enable": "!query.is_connect(4)",
"name": "left",
"parent": "sofa",
"pivot": [···],
"rotation": [···]
},
{
"cubes": [···],
"enable": "!query.is_connect(5)",
"name": "right",
"parent": "sofa",
"pivot": [···],
"rotation": [···]
}
```
浴缸的功能也是大同小异放置和破坏同时对上半、下半部分生效交互可以泡澡每隔3秒增加玩家经验值。需要进行修改的也就只有方块的判断以及循环计时器的触发函数
```python
class Main(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self.ListenForEvent(namespace, system_name,'ServerBlockUseEvent', self, self.using_item)
def using_item(self, event):
# 如果交互的方块是浴缸上半部分或下半部分
# 判断内的逻辑和现代床、椅子、沙发类似(传送玩家到方块位置、设置玩家无法移动、播放动画、添加循环触发的计时器)
if block_name == "farm:bathtub_head" or block_name == "farm:bathtub_tail":
# 循坏触发的计时器每三秒触发一次player_exp函数
# ···
self.healtimer = timercomp.AddRepeatedTimer(3.0,self.player_exp,player_id,)
def player_exp(self,playerid):
# 创建玩家经验接口
expcomp = serverApi.GetEngineCompFactory().CreateExp(playerid)
# 给玩家添加10点经验
expcomp.AddPlayerExperience(10)
```
到此,所以的家具都已经制作完成,用它们来装饰地图吧:
![33](./images/33.png)