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

View File

@@ -0,0 +1,343 @@
---
front: https://nie.res.netease.com/r/pic/20220408/0c3f2c46-fb70-4102-820b-3ed513469dc5.png
hard: 进阶
time: 60分钟
selection: true
---
# 使用自定义数据记录数据
《海滨小岛》有三个决定游戏进度的数据钱数、家具数量和游戏天数我们需要针对这三种数据进行监听和存储所以我们需要使用特定的接口来存储、获取这三种数据并利用UI将数据可视化使玩家可以实时看到数据的变化。
在新手引导结束后,统一存储数据:
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
#新手引导结束,开始游戏触发的函数
def start_game_data(self,playerid):
# 存储硬币初始数量
player_data_coin = leveldatacomp.SetExtraData("player_coin",0)
# 存储家具初始数量
player_data_furniture = leveldatacomp.SetExtraData("player_furniture",0)
# 存储初始游戏天数
player_data_day = leveldatacomp.SetExtraData("player_day",0)
# 存储玩家id
player_data_id = leveldatacomp.SetExtraData("player_id",playerid)
```
## 编写追踪玩家交易行为的功能
我们需要根据玩家“得到钱”和“花费钱”这两个情况下实时变更数据所以在玩家和NPC交易时添加通信事件修改数据
NPC交易相关的教程内容在[第五章](../第05章设置NPC的基本状态和交易表/课程01.自定义NPC的基本行为.md)
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 监听事件PlayerAttackEntityEvent
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "PlayerAttackEntityEvent",
self,self.PlayerAttack)
# 监听由客户端传来的事件(玩家购买商品)
self.ListenForEvent("FarmMod", "ClientSystem", "buy_item",
self,self.PlayerBuyItem)
# 提前获取到的商人id
self.animal_shop_id = "-120259084268"
# 点击NPC时触发
def PlayerAttack(self,args):
# 玩家id
self.playername = args["playerId"]
# 点击的NPCid
entityid = args["victimId"]
# 如果点击的npc和存储的商人id一致
if entityid == self.animal_shop_id:
# 存储需要传输的参数玩家id、商人id、获取钱数
event = {"playerid":args["playerId"] , "entityid" : entityid,
"player_coin": leveldatacomp.GetExtraData("player_coin")}
# 发送事件到客户端
self.NotifyToClient(self.playername,"create_shop_ui", event)
# 玩家点击交易表购买按钮并成功购买后触发的函数
def PlayerBuyItem(self, args):
# 通过传过来的参数保存交易后剩下的钱数
leveldatacomp.SetExtraData("player_coin",args["coin"])
#传过来的玩家id
player_id = args['playerid']
#传过来的实际商品名称
item_name = args['buy_item']
# 发放物品
serverApi.GetEngineCompFactory().CreateItem(player_id).SpawnItemToPlayerInv(
{
'newItemName': item_name,
'count': 1
},
player_id
)
```
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
# 监听由ServerSystem发送的事件
self.ListenForEvent("FarmMod", "ServerSystem", "create_shop_ui",self, self.Create_Shop_UI)
# 提前获取到的商人id
self.animal_shop_id = "-120259084268"
def Create_Shop_UI(self,event):
if event["entityid"] == self.animal_shop_id:
# 创建交易表UI
self.ui = clientApi.PushScreen("Farm","new_shop")
# 通过UI实例修改变量商品
self.ui.item_button_text = self.animal_shop_item_button_text
# 将事件传送过来的玩家钱数设置到ui里
self.ui.coin = event["player_coin"]
```
```python
class FarmUIScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.coin = 0 #玩家钱数的变量
# 按钮绑定
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def buy_button_clicked(self,args):
if self.clicked_button_index == -1:
print "玩家还没选物品"
return
# 获取商品价格
price = self.item_button_text[self.clicked_button_index]['coin']
# 如果玩家钱数大于等于商品价格 → 相减 → 传送事件至服务端
if self.coin >= price:
self.coin -= price
#向服务端通信将玩家id剩余钱数以及购买的实际物品作为参数传送过去
self.clientsystem.NotifyToServer("buy_item",{"playerid":clientApi.GetLocalPlayerId(),"coin":self.coin,"buy_item"
:self.item_button_text[self.clicked_button_index]["itemname"]})
else:
print "你买不起"
```
绝大部分的功能逻辑都在第五章制作交易表的时候介绍过,只需要在此基础上添加一些获取和存储数据的接口即可;当然,只进不出是不行的,我们还需要制作能够获得钱的功能 **“回收商人会在傍晚回收箱子内的物品并获得钱”**
```json
{
"format_version": "1.13.0",
"minecraft:entity": {
"description": {
"identifier": "farm:acquirer", //回收商人
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false,
"runtime_identifier": "minecraft:villager"
},
"components": {
"minecraft:scheduler": { //在特定时间触发事件
"min_delay_secs": 0,
"max_delay_secs": 10,
"scheduled_events" : [
{
"filters": {
"all_of": [
{ "test": "hourly_clock_time", "operator": ">=", "value": 12000 },
{ "test": "hourly_clock_time", "operator": "<", "value": 13000 }
]
},
"event": "minecraft:work" //触发的事件
}
]
}
},
"component_groups": {
},
"events": {
"minecraft:work":{
}
}
}
}
```
给商人的行为文件添加特定时间触发事件的组件利用MODSDK监听该商人在触发事件的时候执行功能逻辑
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 监听事件EntityDefinitionsEventServerEvent
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),'EntityDefinitionsEventServerEvent',
self, self.Player_Event)
# 提前获取的回收商人id
self.acquirer_id = "-983547510779"
def Player_Event(self,args):
# 如果触发事件的生物id是回收商人
if args["entityId"] == self.acquirer_id:
# 使用寻路接口让回收商人走到箱子旁
movecomp = serverApi.GetEngineCompFactory().CreateMoveTo(self.acquirer_id)
movecomp.SetMoveSetting((72,66,82), 1.5, 2000, self.acquirer_callback)
# 寻路结束的回调函数
def acquirer_callback(self,entityid ,result):
# 如果回收商人到达地点
if result == 0:
# 所有可回收商品的名称和价格
goods_list = {"farm:spinach_item":8,"farm:whiteradish_item":4,"farm:peas_item":3,
"farm:lemon_item":6,"farm:eggplant_item":4,"farm:crown_dasiy_item":4,
"farm:corn_item":6,"farm:banana_item":6,"farm:bambooshoot_item":6,
"minecraft:log":2,"minecraft:cobblestone":2,"minecraft:egg":8,
"minecraft:wool":15,"minecraft:leather":15}
# 创建接口
chestitemcomp = serverApi.GetEngineCompFactory().CreateItem(serverApi.GetLevelId())
chestslotcomp = serverApi.GetEngineCompFactory().CreateChestBlock(serverApi.GetLevelId())
# 创建变量(箱子的槽数和最后结算的价格)
count = 0
add_price = 0
# 循环0-26对应小箱子的槽数
for item in range(0,27):
# 获取当前槽位的物品信息字典
itemdict = chestitemcomp.GetContainerItem((72,66,81), count, 0)
# 如果有东西
if itemdict:
# 如果该槽位的物品名在商品列表中
if itemdict["newItemName"] in goods_list:
# 物品数量*物品的价格 += 到变量中
add_price += itemdict["count"] * goods_list.get(itemdict["newItemName"])
# 设置该槽位的物品数量为0
chestslotcomp.SetChestBoxItemNum(None, (72,66,81), count, 0, 0)
# 箱子的槽数+=1
count += 1
# 获取当前玩家的钱数
now_coin = leveldatacomp.GetExtraData("player_coin")
# 把当前的钱数和上面结算的最终钱数相加再存储
leveldatacomp.SetExtraData("player_coin",now_coin + add_price)
# 使用寻路接口让回收商人再回到家中
movecomp = serverApi.GetEngineCompFactory().CreateMoveTo(self.acquirer_id)
movecomp.SetMoveSetting((132,71,96), 1.5, 2000, self.acquirer_callback_home)
def acquirer_callback_home(self,entityid,result):
print result
```
## 编写追踪玩家生存天数的功能
在玩家结束新手引导正式开始游戏时可以在函数中重置一下游戏的时间:
```python
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 新手引导结束,开始游戏触发的函数
def start_game_data(self,playerid):
# 创建时间的接口
timecomp = serverApi.GetEngineCompFactory().CreateTime(serverApi.GetLevelId())
# 设置世界时间为0
timecomp.SetTime(0)
```
获取当前世界的时间可以直接获取Molang或者使用相关接口
```python
# 通过query.day获取当前世界的天数
clientApi.GetEngineCompFactory().CreateQueryVariable(clientApi.GetLocalPlayerId()).GetMolangValue('query.day')
#或
# 创建时间的接口
comp = serverApi.GetEngineCompFactory().CreateTime(levelId)
# 获取当前的时间
passedTime = comp.GetTime()
# 获取当前的天数
day = passedTime / 24000
```
## 编写追踪玩家持有家具数量的功能
所有的家具都是由自定义方块制作的,所以我们只需要监听玩家放置和破坏方块这两个事件即可完成对家具数量的追踪:
```python
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 监听EntityPlaceBlockAfterServerEvent事件
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
'EntityPlaceBlockAfterServerEvent',
self, self.Place_Furniture)
# 监听ServerPlayerTryDestroyBlockEvent事件
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
'ServerPlayerTryDestroyBlockEvent',
self, self.Destroy_Furniture)
# 使用中国版自定义方块方法制作的家具列表
self.netease_block_list = ["farm:connect_table", "farm:sofa_black", "farm:sofa_blue", "farm:sofa_brown",
"farm:sofa_cyan", "farm:sofa_gray",
"farm:sofa_light_blue", "farm:sofa_light_cyan", "farm:sofa_light_green",
"farm:sofa_light_gray", "farm:sofa_orange", "farm:sofa_pink_red",
"farm:sofa_purple", "farm:sofa_red", "farm:sofa_white", "farm:sofa_yellow"]
# 放置方块时触发
def Place_Furniture(self, args):
# 通过事件获取的方块坐标、名称
x = args['x']
y = args['y']
z = args['z']
blockname = args['fullName']
# 创建方块状态接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
# 获取放置的方块状态
blockstate = blockstatecomp.GetBlockStates((x, y, z), 0)
# 如果farm:rotation不在blockstate并且方块名称不在列表中就返回所有的家具都有farmrotation属性所以可以用这个来判断是不是家具
# 不过通过中国版自定义方块的方法制作的家具并没有属性,所以需要额外给这些家具创建一个列表用于判断条件
# 当然也可以将所有的家具都放在一个列表里进行判断
if "farm:rotation" not in blockstate and blockname not in self.netease_block_list:
return
# 如果是家具的话就会继续执行,这时将家具的数据+=1
leveldatacomp.SetExtraData("player_furniture", leveldatacomp.GetExtraData("player_furniture") + 1)
# 破坏方块时触发
def Destroy_Furniture(self, args):
# 通过事件获取的方块坐标、名称
x = args['x']
y = args['y']
z = args['z']
blockname = args['fullName']
player_id = args['playerId']
# 创建方块状态接口
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
# 获取放置的方块状态
blockstate = blockstatecomp.GetBlockStates((x, y, z), 0)
# 下方的逻辑和放置家具时一致,不过是将数据从+=1改为-+1
if "farm:rotation" not in blockstate and blockname not in self.netease_block_list:
return
leveldatacomp.SetExtraData("player_furniture", leveldatacomp.GetExtraData("player_furniture") - 1)
event = {"player_data_furniture": leveldatacomp.GetExtraData("player_furniture")}
self.NotifyToClient(leveldatacomp.GetExtraData("player_id"), "re_dataui", event)
```

View File

@@ -0,0 +1,282 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 进阶
time: 60分钟
---
# 使用编辑器制作计分板界面
打开界面编辑器创建一个计分板UI在main画布下创建合适大小的面板并新增背景
![1](./images/1.png)
接下来在背景下分别创建显示钱数、天数和家具数量的图片和文字并修改至合适的位置:
![2](./images/2.png)
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6152ba3eb647e504b523d398" height="600" width="800" allow="fullscreen" />
制作完UI界面以后打开UI文件将这三个label通过装饰器绑定到ScreenNode
```json
{
"coin_label" : {
"text" : "#text",
"bindings" : [
{
"binding_name":"#coin_text", //绑定名称
"binding_name_override":"#text", //绑定的回调,返回的参数将给到上方的#text
"binding_condition" : "always_when_visible" //绑定条件:总是可见
}
]
},
"day_label" : {
"text" : "#text",
"bindings" : [
{
"binding_name":"#day_text",
"binding_name_override":"#text",
"binding_condition" : "always_when_visible"
}
]
},
"furniture_label" : {
"text" : "#text",
"bindings" : [
{
"binding_name":"#furniture_text",
"binding_name_override":"#text",
"binding_condition" : "always_when_visible"
}
]
}
}
```
```python
class FarmUIScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
# 绑定字符串返回self.coincoin变化时通过创建ui实例修改参数
@ViewBinder.binding(ViewBinder.BF_BindString, "#coin_text")
def player_coin_text(self):
return str(self.coin)
# 绑定字符串使用Molang直接返回天数
@ViewBinder.binding(ViewBinder.BF_BindString, "#day_text")
def player_day_text(self):
return str(int(
clientApi.GetEngineCompFactory().CreateQueryVariable(clientApi.GetLocalPlayerId()).GetMolangValue(
'query.day')))
# 绑定字符串返回self.furniturefurniture变化时通过创建ui实例修改参数
@ViewBinder.binding(ViewBinder.BF_BindString, "#furniture_text")
def player_furniture_text(self):
return str(self.furniture)
```
我们在新手引导结束的函数中创建这个UI并且将数据给予UI
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 设置新手引导结束会触发的函数
def start_game_data(self, playerid):
# 使用ExtraData存放数据
leveldatacomp.SetExtraData("player_coin", 100)
leveldatacomp.SetExtraData("player_furniture", 0)
leveldatacomp.SetExtraData("player_day", 0)
# 将初始数据作为参数传到ClientSystem
event = {"playerid": playerid, "player_data_coin": leveldatacomp.GetExtraData("player_coin"),
"player_data_furniture": leveldatacomp.GetExtraData("player_furniture")}
self.NotifyToClient(self.playername, "create_data_ui", event)
```
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
# 监听由ServerSystem发送过来的事件
self.ListenForEvent("FarmMod", "ServerSystem", "create_data_ui",
self, self.CreateDataUI)
def CreateDataUI(self,event):
# 创建UI
clientApi.CreateUI("Farm","data_ui",{"isHud":1})
# 执行更新UI数据的函数
self.Re_DataUI(event)
def Re_DataUI(self,event):
# 获取UI实例
self.data_ui = clientApi.GetUI("Farm","data_ui")
# 如果传过来的参数有player_data_coin则更新UI中的coin
if "player_data_coin" in event:
self.data_ui.coin = event["player_data_coin"]
# 如果传过来的参数有player_data_furniture则更新UI中的furniture
if "player_data_furniture" in event:
self.data_ui.furniture = event["player_data_furniture"]
```
这样在完成新手引导的时候数据的计分板UI就可以正常创建了我们使用编辑器的**开发测试**功能尝试一下:
<img src="./images/3.gif" alt="3" style="zoom: 180%;" />
## 将追踪玩家的数据更新在UI上
在本章的第一节中我们已经完成了对玩家数据存取的功能但是这些数据只存在于地图中并不会直接显示在UI上所以我们需要在数据存取的同时给客户端发送事件修改UI实例的变量使数据可视化。
首先是玩家的钱数,我们只需要在有钱变动的地方(买商品和被回收商人收走商品)发送一个事件给客户端即可:
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 玩家通过交易表购买商品后 发货的函数(在第五章和本章的第一节都有出现过)
def PlayerBuyItem(self, args):
# 将买完商品后的钱 存进数据player_coin
leveldatacomp.SetExtraData("player_coin", args["coin"])
# 传过来的玩家id
player_id = args['playerid']
# 传过来的实际商品名称
item_name = args['buy_item']
# 将玩家id和买完商品后剩下的钱作为参数传给客户端的re_dataui更新ui的数据
event = {"playerid": player_id, "player_data_coin": args['coin'], }
self.NotifyToClient(player_id, "re_dataui", event)
# 发放物品
serverApi.GetEngineCompFactory().CreateItem(player_id).SpawnItemToPlayerInv(
{
'newItemName': item_name,
'count': 1
},
player_id
)
# 回收商人到达地点的回调函数,同时也是回收箱子内物品的函数(本章的第一节出现过)
def acquirer_callback(self, entityid, result):
print result
if result == 0:
# 循环箱子内的每个物品并算出其价格之和
chestitemcomp = serverApi.GetEngineCompFactory().CreateItem(serverApi.GetLevelId())
chestslotcomp = serverApi.GetEngineCompFactory().CreateChestBlock(serverApi.GetLevelId())
count = 0
add_price = 0
for item in range(0, 27):
itemdict = chestitemcomp.GetContainerItem((72, 66, 81), count, 0)
if itemdict:
if itemdict["newItemName"] in self.goods_list:
add_price += itemdict["count"] * self.goods_list.get(itemdict["newItemName"])
chestslotcomp.SetChestBoxItemNum(None, (72, 66, 81), count, 0, 0)
count += 1
# 获取玩家当前的钱数
now_coin = leveldatacomp.GetExtraData("player_coin")
# 将当前钱数和箱子内物品价格的钱数相加并存储数据
leveldatacomp.SetExtraData("player_coin", now_coin + add_price)
# 将玩家新的钱数作为参数传送给客户端的re_dataui更新ui的数据
event = {"player_data_coin": now_coin + add_price}
self.NotifyToClient(leveldatacomp.GetExtraData("player_id"), "re_dataui", event)
# 用寻路组件让回收商人回家
movecomp = serverApi.GetEngineCompFactory().CreateMoveTo(self.acquirer_id)
movecomp.SetMoveSetting((132, 71, 96), 1.5, 2000, self.acquirer_callback_home)
```
需要实时更新的数据还有家具的放置,同样在第一节我们已经做好了这部分功能,只需要在此基础上添加即可:
```python
leveldatacomp = serverApi.GetEngineCompFactory().CreateExtraData(serverApi.GetLevelId())
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
# 监听玩家放置方块和破坏方块
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
'EntityPlaceBlockAfterServerEvent',
self, self.Place_Furniture)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
'ServerPlayerTryDestroyBlockEvent',
self, self.Destroy_Furniture)
def Place_Furniture(self, args):
# 通过事件获取方块的坐标、名称
x = args['x']
y = args['y']
z = args['z']
blockname = args['fullName']
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
blockstate = blockstatecomp.GetBlockStates((x, y, z), 0)
# 如果不是家具则返回
if "farm:rotation" not in blockstate and blockname not in self.netease_block_list:
return
# 获取现在的家具数量并+=1
leveldatacomp.SetExtraData("player_furniture", leveldatacomp.GetExtraData("player_furniture") + 1)
# 将新的家具数量作为参数传送给客户端的re_dataui更新ui的数据
event = {"player_data_furniture": leveldatacomp.GetExtraData("player_furniture")}
self.NotifyToClient(leveldatacomp.GetExtraData("player_id"), "re_dataui", event)
def Destroy_Furniture(self, args):
# 通过事件获取方块的坐标、名称
x = args['x']
y = args['y']
z = args['z']
blockname = args['fullName']
player_id = args['playerId']
blockstatecomp = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
blockstate = blockstatecomp.GetBlockStates((x, y, z), 0)
# 如果不是家具则返回
if "farm:rotation" not in blockstate and blockname not in self.netease_block_list:
return
# 获取现在的家具数量并-=1
leveldatacomp.SetExtraData("player_furniture", leveldatacomp.GetExtraData("player_furniture") - 1)
# 将新的家具数量作为参数传送给客户端的re_dataui更新ui的数据
event = {"player_data_furniture": leveldatacomp.GetExtraData("player_furniture")}
self.NotifyToClient(leveldatacomp.GetExtraData("player_id"), "re_dataui", event)
```
无论是钱数还是家具数量的变化都将通过事件传送到客户端的re_dataui中
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
# 监听由ServerSystem发送的事件re_dataui
self.ListenForEvent("FarmMod", "ServerSystem", "re_dataui",
self, self.Re_DataUI)
def Re_DataUI(self,event):
# 获取ui实例
self.data_ui = clientApi.GetUI("Farm","data_ui")
# 如果传送过来的参数中有player_data_coin则更新ui的参数
if "player_data_coin" in event:
self.data_ui.coin = event["player_data_coin"]
# 如果传送过来的参数中有player_data_furniture则更新ui的参数
if "player_data_furniture" in event:
self.data_ui.furniture = event["player_data_furniture"]
```
接下来使用编辑器的**开发测试**功能进入到游戏中依次测试一下。
回收箱子内的物品:
<img src="./images/4.gif" alt="4" style="zoom:115%;" />
通过交易表购买商品:
<img src="./images/5.gif" alt="5" style="zoom:115%;" />
摆放和破坏家具:
<img src="./images/6.gif" alt="6" style="zoom:115%;" />