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,47 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 5分钟
---
# 示例下载地址
有关玩法地图教程所需的资源素材与Demo可点击 [下载链接](https://g79.gdl.netease.com/demopack.zip) 下载到本地。
## MapDemo
该文件夹内主要放置了玩法地图开发相关的示例,是已经完工的玩法地图。可供开发者们研究学习。
## 家具
该文件夹内主要放置了玩法地图需要的家具方块美术素材。
## 农作物
该文件夹内主要放置了玩法地图需要的农作物方块美术素材。
## 实体
该文件夹内主要放置了商人NPC与船的美术素材。
## 装备
该文件夹内主要放置了穿着装备与道具的美术素材。
## 空地图
该文件夹内主要放置了玩法地图需要用到的地形地图与建筑模板地图。

View File

@@ -0,0 +1,15 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 5分钟
selection: true
---
# 什么是玩法地图
玩法地图是由地图作者创建的自定义世界。通过一种到多种的元素组合达到有别于原版生存的游戏体验。如建筑地形、命令方块、粒子特效、自定义实体、自定义UI等。它们可以是让玩家沉浸在史诗故事中的精彩角色扮演冒险也可以是考验玩家解决问题能力的具有挑战性的解密地图。
制作地图最佳的技巧是同理心。您必须对玩家所见、所感与所想建立一个游戏模型。同理心让您可以设计通关难度曲线,并知道何时让玩家休息,何时添加场景让玩家感到惊喜。同理心是最能对玩家产生心灵“暴击”的一种思维。
使用我的世界创作地图的可能性是无限的。唯一限制地图作者的只有想象力。任何人都可以在我的世界创建自己的玩法地图,并通过我的世界中国版开发者平台,将您的手作分享给所有玩家!
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6152b7bbb8a81f8fa07dc88f" height="600" width="800" allow="fullscreen" />

View File

@@ -0,0 +1,25 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 5分钟
---
# 玩法地图应注意的几点
由于我的世界是一个可以任意改变环境,并存在创造功能的游戏。为了保证玩家体验,特别是在多人环境中的感受。建议您根据具体的玩法来合理开关地图的机制规则,如果有用到命令方块,也需要将命令方块放在隐蔽的位置内。同时,控制玩家活动的可控性、提供一个新手引导也是一个可以给地图加分的选项。
## 玩家活动的可控性
当玩家轻易打破障碍物或破坏地图结构时,容易使得设定上的玩法体验变得非常不稳定。使用我的世界自带的 **冒险模式(Adventure Mode)** 可以使玩家不能破坏和放置任何方块,以避免损坏玩法地图或恶意破坏他人游戏体验。
同时,绝大多数地图地形会呈现三种状态,原版的自然群系生成,超平坦,亦或是虚空。为了避免玩家离开活动区域,使用屏障方块将建筑地形包裹成真空状态可以避免玩家失去方向。
最后世界中还存在着一些带有破坏方块能力的实体或生物。如苦力怕、爆炸中的TNT、以及火球。使用以下指令可以轻松地在基岩版世界内禁止它们破坏地形。
```
/gamerule tntexplodes false # 禁止TNT爆炸
/gamerule mobgriefing false # 禁止实体破坏方块
```
## 游戏中的新手引导
在制作地图时作者往往处于一个上帝视角很多时候在游戏机制上的设计会觉得理所当然但如果没有提供一个反馈或者引导。就无法告知玩家下一步的行动该去做什么容易使玩家感到疑惑甚至定性理解为BUG将责任推给作者。设计一个友好的新手引导可以在初期很大程度上降低玩家的困难预期同时提前让玩家了解玩法内容抓住他们的吸引力并降低新手上手难度。

View File

@@ -0,0 +1,27 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 5分钟
---
# 玩法地图需要什么基本元素
一张玩法地图一定存在着一条清晰的主线,也就是一个**核心玩法**。**核心玩法**是一名地图作者在这个作品里,希望玩家能够从一个地点去做某一件事最终达到某个目的流程。例如您制作了一张解密地图,希望玩家能够从一个空间逃出生天。当脑海念头出现的第一个这样的念头让你足够兴奋到愿意去做一张彰显自己想法的地图时,它很可能就是这张地图未来的**核心玩法**。
但就像我的世界是由无数个方块组成,玩法地图也需要有多种元素结合才能释放出最大的吸引力。如果您制作一个没有任何地形搭配的超平坦地图,去搭配一些指令组合让玩家在原版生存里做不到的事情在这里实现,它也能够吸引到自己的玩家群体。但是匮乏一个简单的说明书或者引导提示,就不能吸引到更多的玩家。充分利用我的世界的开放元素,能够做出更加耐玩的地图。
## 场景配套元素
通过地图环境的氛围去感染一个玩家是一种非常有效的手段。例如制作一个日式校园角色扮演的地图玩法。如果在搭建了校舍的基础上加入樱花的街道街机游戏厅以及填满室内空间的内饰日式的气息就扑面而来。同样在玩法地图中场景往往不仅仅会承担装饰美观的效果它也可能会作为资源的收集点NPC的任务点等等成为一个功能性区域。
## 玩法配套元素
我的世界随着版本更新带来了越来越多供开发者试验的机制和制作工具在社区里涌现了许多著名的地图。比如空岛生存【The Skyblock】、战墙【The Walls】等。它们都提供了我的世界核心玩法的不同变体。Skyblock 会使玩家出生在天空中的一个小岛上只靠一棵树、水和熔岩生存还有大量复杂的挑战需要完成。与此同时The Walls 是一款受饥饿游戏启发的大逃杀游戏。这些玩法容易激发玩家的挑战欲望,并通过社交网络上传自己的挑战视频、实况视频,将地图的知名度进一步向外推广,达到更好的传播效果。
## 数值配套元素
建立了场景、玩法的配套元素后,往往需要为这些规则机制开始搭建一个由数值输出的体系反馈层。它帮助玩家能够找到自己在游戏中的定位,如一个战斗系统,玩家会明显得到一个能力的成长反馈,敌人与自己的强弱,克制与被克制等,这部分主要用数值配套玩法规则来实现。当然,一个合理的挑战曲线还能让玩家在克服困难和达成目标间找到一个平衡感。
## 通关方向
有一个明确清晰的通关方向可以引导玩家去按照世界规则进行游戏,将玩家的对地图的认知和游戏的通关目的同步,就不至于出现跟着设计者的思路反着干的情况。同样,游戏最终通关是由玩家打出来的,所以当您为玩家呈现结局时,必须要与玩家之前所做的活动进行联系,给予一个良好的反馈。如果内容质量足够吸引,是有可能让玩家在之后继续反复游玩的。

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -0,0 +1,105 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 10分钟
---
# 主题展望之《我的海滨农场》
我的海滨农场是以强化我的世界农场经营元素为目的的玩法地图。玩家会扮演塔塔村即将跨越成年礼的年轻人,为村庄在海滨小岛上建立一个农场度假村。**游戏目标**是在100天内赚到50W金币并至少装饰12种家具才能过关。
依托于这个通关背景,我们对玩法流程和美术素材进行扩展。
## 玩法流程
- **引导关卡**
- 与家人NPC对话引出游戏背景。
- 玩家需要通过多个收集资源小游戏了解到之后主要需要做什么。未完成任务无法离开小岛。
- 完成任务后登上码头的船离开小岛,提供一个移动转场,增加玩家代入感。
- **流程一总结**
- 玩家到小岛后会见到荒芜的农场,小家和畜牧场都需要材料或资金修复,小家旁的**回收箱**可以回收农产作物,获得发展资金。
- 玩家需要优先修复农场小家,修复材料可以从荒芜的农田上收集木头和石头。余下的资材可以放进**回收箱**内被商人回收,获取初始资金。
- **回收箱**内存放着基本的工具,不掉耐久,并且不会被商人回收。若玩家没有任何进度推展(比如没有拿起箱子里的装备),回收商人会进行提示。
- **流程二总结**
- 使用结算资金去种子店购买种子后,可以在田野里播种。
- 通过播种种子回收作物获得更多资金,扩大农业规模。
- **流程三总结**
- 农业规模进一步扩大可以使用5000金币和一天时间修复畜牧场。
- 玩家可以通过畜牧场饲养动物在创造额外的利润。
- **流程四总结**
- 在服饰店购买个性装扮,打扮最闪亮的自己。
- 在家具店购买家具装饰小屋,完成家具集全之目标。
- 通过畜牧和种植完成金币50W之目标。
## 美术素材
| 类目 | 名称 | 功能 |
| :------- | :-------------- | ------------------------------------------------------------ |
| NPC | 图书管理员 | 帮助玩家在图书馆里获得地图玩法指引书。 |
| NPC | 杂货店老板 | 玩家可以与农民村民交易种子和肥料道具 |
| NPC | 畜牧店老板 | 玩家可以与畜牧店老板交易获得怪物蛋,怪物蛋召唤的生物必定是婴儿类型 |
| NPC | 家具店老板 | 玩家可以与家具店老板交易获得家具方块,家具方块可以放置在家里 |
| NPC | 服装店老板 | 玩家可以与服装店老板交易获得服装,服装可以穿在自己身上 |
| NPC | 回收商人 | 傍晚时间商人会来到回收物资桶【原版箱子】内将物品回收,自动增加玩家金币 |
| NPC | 引导关卡NPC | 以玩家的家人为身份,介绍玩家会继承家里在一座海岛上的农场,一边带出基础的游戏流程 |
| 植物方块 | 白萝卜 | 分为种子阶段、发芽阶段成熟阶段随机刻计数3次进入发芽阶段随机刻计数6次进入成熟阶段 |
| 植物方块 | 茼蒿 | 分为种子阶段、发芽阶段成熟阶段随机刻计数3次进入发芽阶段随机刻计数6次进入成熟阶段 |
| 植物方块 | 竹笋 | 分为种子阶段、发芽阶段成熟阶段随机刻计数4次进入发芽阶段随机刻计数8次进入成熟阶段 |
| 植物方块 | 豌豆 | 分为种子阶段、发芽阶段成熟阶段随机刻计数4次进入发芽阶段随机刻计数6次进入成熟阶段 |
| 植物方块 | 菠菜 | 分为种子阶段、发芽阶段成熟阶段随机刻计数4次进入发芽阶段随机刻计数6次进入成熟阶段 |
| 植物方块 | 茄子 | 分为种子阶段、发芽阶段成熟阶段随机刻计数8次进入发芽阶段随机刻计数10次进入成熟阶段 |
| 植物方块 | 柠檬 | 分为种子阶段、发芽阶段成熟阶段随机刻计数8次进入发芽阶段随机刻计数10次进入成熟阶段 |
| 植物方块 | 香蕉 | 分为种子阶段、发芽阶段成熟阶段随机刻计数8次进入发芽阶段随机刻计数12次进入成熟阶段可重复采集两次 |
| 植物方块 | 玉米 | 分为种子阶段、发芽阶段成熟阶段随机刻计数10次进入发芽阶段随机刻计数12次进入成熟阶段可重复采集两次 |
| 家具方块 | 现代床 | 右键床可以设置出生点,并模拟玩家入睡姿势后进入下一天。 |
| 家具方块 | 床头柜 | 装饰 |
| 家具方块 | 清理玩家 | 清理玩家手上的一件道具 |
| 家具方块 | 榨汁机 | 榨取蔬菜水果汁 |
| 家具方块 | 木椅子 | 可以坐 |
| 家具方块 | 马桶 | 可以坐 |
| 家具方块 | 水龙头 | 装饰 |
| 家具方块 | 浴缸 | 可以泡澡 |
| 家具方块 | 桌子 | 装饰 |
| 家具方块 | 小寸电视 | 点击会显示【网易我的世界LOGO】再点击关闭 |
| 家具方块 | 熊玩偶 | 装饰 |
| 家具方块 | 复古台灯 | 右键会亮,再点击一次变暗 |
| 家具方块 | 马克杯 | 装饰 |
| 家具方块 | 扶手沙发 | 贴在一起可以变成加长沙发;可以坐 |
| 装饰装备 | 棒球外套 | 穿搭装饰 |
| 装饰装备 | 短袖横纹T恤 | 穿搭装饰 |
| 装饰装备 | 【我的世界】T恤 | 穿搭装饰 |
| 装饰装备 | 牛仔裤 | 穿搭装饰 |
| 装饰装备 | 运动健身裤 | 穿搭装饰 |
| 装饰装备 | 粗花呢裙 | 穿搭装饰 |
| 装饰装备 | 篮球鞋 | 穿搭装饰 |
| 装饰装备 | 休闲鞋 | 穿搭装饰 |
| 装饰装备 | 马丁靴 | 穿搭装饰 |
| 装饰装备 | 蝴蝶结 | 穿搭装饰 |
| 装饰装备 | 牛仔布帽 | 穿搭装饰 |
| 装饰装备 | 针织帽 | 穿搭装饰 |
| 物品 | 白萝卜种子 | 种植白萝卜 |
| 物品 | 茼蒿种子 | 种植茼蒿 |
| 物品 | 竹笋种子 | 种植竹笋 |
| 物品 | 豌豆种子 | 种植豌豆 |
| 物品 | 菠菜种子 | 种植菠菜 |
| 物品 | 茄子种子 | 种植茄子 |
| 物品 | 柠檬种子 | 种植柠檬 |
| 物品 | 玉米种子 | 种植玉米 |
| 物品 | 香蕉种子 | 种植香蕉 |
| 物品 | 菠菜汁 | 副产品 |
| 物品 | 玉米汁 | 副产品 |
| 物品 | 柠檬汁 | 副产品 |
| 物品 | 白萝卜 | 农产品 |
| 物品 | 茼蒿 | 农产品 |
| 物品 | 竹笋 | 农产品 |
| 物品 | 豌豆 | 农产品 |
| 物品 | 菠菜 | 农产品 |
| 物品 | 茄子 | 农产品 |
| 物品 | 柠檬 | 农产品 |
| 物品 | 玉米 | 农产品 |

View File

@@ -0,0 +1,37 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 10分钟
---
# 好用的工具是一切的起点
在过去许多基岩版的地图制作者凭借跨平台的优势在手机上创作他们自己的玩法地图。这里我们更多会推荐使用PC平台上的工具以帮助您加快地图的开发速度。无论您是JAVA版建筑作者、基岩版组件作者、命令方块地图作者使用以下工具可以更快的转换你现在的资源格式、快速制作配套资源、亦或是优化撰写行为包的体验。
## Chunker[地图工具]
Chunker是由国际版Minecraft团队HiveMC推出的在线地图转换方案它向上支援最新的基岩版1.17.x和JAVA版1.17.x的世界格式。选择直接上传地图文件夹或地图压缩档您可以快速修改地图的设定预览地图已加载区域的2D平面图并一键转换成对应版本格式。[点击链接](https://chunker.app/)可以快速跳转Chunker网站。
![0](./images/0.jpg)
## Visual Studio Code[编码工具]
Visual Studio Code简称 VS Code是一款由微软开发且跨平台的免费源代码编辑器。对于电脑性能配置不高的开发者使用这种轻量级的编辑器可以使您在创作MC内容时对电脑产生的压力负担至最小。无论是快速编辑数据驱动的附加包JSON内容亦或是导入Modsdk补全库进行mod开发它都足以胜任这些工作。
![1](./images/1.jpg)
## Blockbench/Photoshop/Aesprite[美术工具]
Blockbench是由Jennis开发的一款免费开源的我的世界美术资产制作工具。它支援JAVA版和基岩版的模型格式并为基岩版提供实时动画编辑预览和粒子音效预览的功能以及贴图绘制功能。基于跨平台的特性它提供网页在线版和本地版。为了避免资产在网页上进行编辑时出现丢失的情况这里强烈推荐下载本地版并设置自动备份时间。
Photoshop是由Adobe开发和发行的图片设计软件它同样可以进行贴图绘制以及像素素材制作。但绝大部分时候对于我的世界的模型贴图纹理设计都可以在Blockbench内完成。只有当需要对贴图纹理进行高级改造如将贴图纹理支持基岩版的发光材质时使用Photoshop会更有优势。
Aseprite是一款动态像素精灵与像素贴图制作工具。相较于Photoshop它更小巧迷你并为像素绘制提供更好的图像算法使您在旋转、缩放贴图时拿到更清爽的结果。动态像素精灵即带有多张像素贴图的序列帧制作会播放序列帧的粒子效果时使用Aseprite可以更快得到你想要的结果。
## Bridge.[附加包编辑工具]
Bridge.是一款一键生成附加包提供附加包JSON语法高亮的文本编辑器与工具链。Bridge.不提供模拟运行附加包的运行环境,但它可以帮助你的附加包以合法的内容格式在游戏内正常运作。
## Pycharm[Modsdk IDE]
Pycharm是一个用于Python语言开发的集成开发环境简称IDE由捷克公司JetBrains开发。当前中国版会持续维护基于Python语言的Modsdk框架帮助开发者能够创建更加灵活且耐玩的游戏玩法使用IDE可以帮助您管理项目版本、提供代码补全、以及语法检查等。但Modsdk当前尚不支持IDE的断点调试因此只能通过在不同的地方打Log来调试代码。

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,47 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 15分钟
---
# 在MCSTUDIO内创建地图工程
玩法地图教程为您提前准备好了地图存档资源,包括**Structure存档**和**FarmWorld存档**。其中**Structure存档**是一张超平坦地图,此次所有的单个建筑都罗列在该地图的出生点周围。这么做的处理是因为在超平坦世界中建造独栋建筑时,不会受到**地形影响建筑建造**的困扰,同时可以更加直观地看到**所有建筑的风格和规模**,方便建筑师进行统一规划和调整。**FarmWorld存档**则是主题玩法地图的地形存档。我们将会使用这两张地图来合成出最终完整的地图大观。
## 本地导入地图资源
进入MCSTUDIO作品库选择基岩版组件。点击右上角的本地导入资源类型选择地图并通过选择功能在窗口中选择**Structure存档**和**FarmWorld存档**,最后再点击导入按钮即可完成导入操作。
<img src="./images/0.jpg" alt="0" style="zoom:150%;" />
在未来,新版编辑器将多方位覆盖开发者的日常开发流程。我们推荐您将地图作品升级成新版格式,提前体验新版编辑器的魅力。
<img src="./images/1.jpg" alt="1" style="zoom:120%;" />
## 使用chunker改变地图的属性
为了降低《我的海滨农场》的开发上手难度,在这张地图内,我们只考虑玩家在**单人环境**中游玩的情况。因此我们选择不将玩家房间暴露在本地局域网或者联网环境中。
我们在对地图资源进行预处理的时候已经提前关闭了这些设定,但你也可以在未来对其他地图通过同样的方法再次重复这个操作。
您必须使用相关的地图处理工具去处理地图。这里推荐使用[chunker](https://chunker.app/)对地图进行NBT修改。NBT在基岩版中同样存在并且在地图文件里会管理地图的一些基本设定。在将地图上传成功后请选择右下角的Advance Settings接着选择WORLD SETTINGS手动关闭**Multiplayer Game Enabled**、**Visible to Multiplayer**、**LAN Broadcast Enabled**、**Visible to LAN Players**等开关。
![6](./images/6.png)
我们同样需要关掉**Do Insomnia**、**Do Mob Spawning**与**TNT Explodes**这样可以避免玩家在地图里遇到TNT爆炸、自然生物生成和幻翼的干扰。
![17](./images/17.png)
接着点击CONVERT再次转换源文档并DOWNLOAD下载到本地。
## 添加常加载区域
通常情况下世界上正处于被加载的区域都围绕在玩家周围。在玩家移动的过程中进入范围内的新区块区域会被加入区块更新的列表里而离开范围内的旧区块则会停止更新。为了在后续教程中我们可以使用ModSDK获取加载区域内的方块与实体信息在这里需要将主岛所横跨的区块都放进常加载区域里。使用**/tickingarea**指令可以帮助我们完成这个效果:
```
/tickingarea add 64 0 64 200 0 180
```
在关卡或地图编辑器里按下/键呼出输入框,即可插入这段指令。
![18](./images/18.png)

View File

@@ -0,0 +1,33 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 15分钟
---
# 在地图工程中保存资源素材
由于独立建筑与地形大观分开在两张地图存档内,我们需要先通过地图编辑器将超平坦内的建筑导出成自定义素材,让我们看看应该怎么做吧!
## 导出自定义建筑素材
对新版格式的**Structure地图作品**点击编辑,进入编辑器世界。
<img src="./images/8.jpg" alt="0" style="zoom:105%;" />
使用选区工具,在超平台地形上对建筑底部进行框选。
![7](./images/7.png)
通过点击框选格的上部面,使用鼠标拖拉可以抬高框选格的高度,将整个**玩家之家**与地下一格的草方块全部选中。
![10](./images/10.gif)
使用坐标轴对框选方格整体抬高一格,并点击上部方格面降低一格高度,以去除地下一格草方块的空间间隙。
![11](./images/11.gif)
点击**保存素材**素材支持10字符内的中文、英文与数字组合并且导出的mdl格式只支持在编辑器内流通或者使用预设API在游戏内进行放置。默认选择下会勾选去除空气和**去除流体**,若建筑内带有流体,可以去掉**去除流体**的勾选。
![12](./images/12.png)
保存的自定义素材会放在**素材库**的**自定义素材**下。**自定义素材**存放于公共的编辑器文件夹,不同地图的地图编辑器都可以检索到之前准备好的**自定义素材**。

View File

@@ -0,0 +1,32 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 30分钟
---
# 尝试熟悉基本的地图编辑器功能
进入**FarmWorld存档**作品时,地形中预放的地基为我们提供了素材的放置坐标。通过几个基础的编辑地图步骤,我们也能够轻松地将建筑装饰进世界当中。
## 调整能见度
原来的地图编辑器提供的能见度非常低,通过上帝视角俯瞰地形时,只有一小部分的地形会进入视野。我们使用**能见度功能**可以快速增加能够看见的区块的格数。
点击**能见度**可以拖拉可见区块的数量,进度越长,代表可以看到的范围越大。
**注意:请不要过度增加可见区块的数量,这将会对您的电脑设备造成极大的负荷压力!**
![14](./images/14.png)
## 调整时间
地图编辑器中的世界也会随着时间流逝进行昼夜更替。您可以点击**时间功能**切换游戏时间,让世界一直保持白天。
![15](./images/15.png)
## 放置自定义素材
我们通过单击素材选项后即可在游戏区域任意拖动。接着点击游戏任意坐标并再次点击确认生成即可将素材建筑重新加入世界。若在改动后想要撤回可以使用CRTL+Z快捷键撤回到前一次的操作步骤。
![16](./images/13.gif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

View File

@@ -0,0 +1,49 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 10分钟
---
# 规划地图的功能与区域
一个理想的海岛农场最终会以下图的方式进行罗列。地图一共分为两块岛屿9个独立建筑。
**游戏的通关目标是在100天的时间内赚到25万金币和获得全部14种家具。**
![0](./images/0.png)
## 农场之家
农场之家会分为修复前与修复后两种状态,在初期时需要通过消耗**10个木头和5个石头**才能得到修复。玩家会在初期就拥有系统赠送的**石稿**、**石斧**和**石锄**,并且可以**重复领取**。**石稿**可以帮助玩家挖掘石材,**石斧**用来帮助玩家挖掘木材,而**石锄**主要用来复原家门口的耕地和收获庄稼。木材与石材则作为**修复畜牧场和农场之家**的固定材料。
## 畜牧场
畜牧场分为修复前与修复后两种状态,在初期时需要通过消耗**20个木头、10个石头与5000金币**才能得到修复。在游戏期间,这里会作为畜牧动物的生活地点。
## 新手关卡
作为引导玩家了解地图的基础玩法的场景通过完成NPC的小游戏解锁主岛屿。
## 图书馆
玩家可以在图书馆里获得地图玩法指引书。指引书使用原版书与笔的格式,方便我们将玩法要点记录下来供玩家参考。
## 畜牧店
玩家可以在畜牧店找牧场商人购买生物蛋,在畜牧场内的围栏后面点击地面可以召唤出动物。
## 杂货店
玩家可以在杂货店购买种子,种子可以用来播种作物,收获的农产品是玩家主要的经济来源。
## 收购商人
每天日落时会来返玩家之家的 **回收箱**内收购一天的农副产品。
## 家具店
玩家可以在家具店内购买家具,部分家具具有特殊的能力。
## 服装店
玩家可以在服装店内购买服装,打扮个性的自己。

View File

@@ -0,0 +1,115 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 进阶
time: 40分钟
---
# 规划不可变动区域
不可变动区域意为地图中不可破坏、不可放置方块的区域。由于**素材地图**在前期处理时,已将世界中的全部群系设置为海洋群系,世界缺少让玩家修复资源点、建筑的材料。若场景遭到玩家无意的破坏,这一行为容易导致整体的玩法流程、美观性大打折扣。为了防止这样的行为带来的糟糕影响,我们需要对主要建筑、地形、或者资源点做无法被破坏、无法在上面加盖方块的功能。
同时,在游戏过程中,玩家需要基本的工具和食物才能生存。玩家获取金币的途径除了通过种植庄稼,还可以通过挖掘场景中资源点的**木材石头**来取得。因此我们需要提供玩家一个基础的**工具食物箱**,以使玩家能够在容错率更高的情况下顺利度过前期游戏。
## 使用Function文件固定玩家的游戏模式为冒险模式
> 在过去制作玩法地图时对于游戏模式的处理上命令方块作者们常会使用一个循环型命令方块设置周遭玩家的游戏模式。现在使用行为包中的function文件可以达到同样的效果并将工作模式放入行为包中达到更加隐蔽的效果。
在MC STUDIO编辑器中一张玩法地图工程会由编辑器自动生成行为包和资源包的工程文件夹。Function文件会在行为包根目录下的functions文件夹内生效若在该文件夹目录内新建一个tick.json文件它可以命令function文件内的指令在世界加载后循环运行。
首先在电脑的任意一处新建一个以**.mcfunction**为结尾的function文件。这里推荐使用任意一种携带mcfunction智能补全的第三方编辑器示例里我们会用到**Visual Studio Code**简称vs code。在开始工作前需要我们在扩展区域下载Blockception's Minecraft Bedrock Development插件。
![2](./images/2.png)
可以直接拖拉文件图标至编辑器页面内编辑器将自动吞入并打开。简单输入其中一个指令后续的参数就会智能提示并按enter或tab键自动补全。
![](./images/3.gif)
与在游戏中的对输入框和命令方块打出指令时需要斜杠不同的是在function文件内无需加入**/**符号。同时命令的执行顺序会**从上往下按顺序执行**若function文件内的指令被循环执行则**命令执行到末尾时,会从文件头重新跑起**。
将玩家设置成冒险模式可以防止玩家破坏和加盖方块因此我们首先只需在function文件内写入以下指令来判断玩家是否为非冒险模式是则将他们的游戏模式转变成冒险模式。
```
execute @a[m=!adventure] ~~~ gamemode adventure @s
```
接着新建functions文件夹并将function文件放入其中在同目录下新建tick.json文件。
<img src="./images/4.png" alt="4" style="zoom:160%;" />
在tick.json文件中打入以下JSON内容以"values"为键它接受一个以function文件名为主的数组。function文件里面的命令就会像循环型命令方块一样在每一个游戏逻辑帧下执行一遍。
```json
{
"values": [
"gamemode" //gamemode是gamemode.mcfunction的文件名
]
}
```
最后使用编辑器打开地图工程在资源管理栏下点击折叠的行为包文件夹在右侧的窗口预览区域导入functions文件夹即可。
![4](./images/4.gif)
## 使用minecraft:can_destroy定制带有破坏特定方块能力的农具
在开启冒险模式后玩家虽然无法任意放置方块与破坏方块但玩法地图里依然需要为玩家提供必要的农具以便收集资源点的素材以及当玩家不慎踩到耕地时还能够拥有一把锄头将耕地复原回去。在give指令里添加minecraft:can_destroy标签可以让使用者在冒险模式下依旧有挖掘特定方块的能力
```
/give @s stone_pickaxe 1 0 {"minecraft:can_destroy": {"blocks":["log", "log2"]}}
# 给予玩家一个满耐久度的石斧blocks对应的数组支持多个自定义方块和原版方块但自定义方块需要写全namespace:identifier格式的方块名称域。
```
**原版的锄头会允许使用者无视等级权限,将全部的草类方块变为耕地**。为了防止玩家对于地形进行无节制的开垦在下一个章节中我们会开始学习如何使用Mod来阻止玩家做出这一行为。
## 增加工具食物箱
在地图编辑器内,我们选择选取模式,并保持单选模式。
<img src="./images/8.png" alt="8" style="zoom:200%;" />
接着点选地面并拖动Y轴将选格向上提升一格。
![9](./images/9.png)
选择填充功能,对格子内的区域进行箱子方块填充。
![10](./images/10.png)
我们需要提前记下起始坐标,这里以[72,66,81]为例。通过使用scoreboard指令新增一个chest计分项我们可以设计一个每隔1200帧往箱子内添加农具的指令集。即每一帧都会给chest计数递增1通过目标选择器的scores参数来判断玩家的chest分数是否达到1200以执行一次填充工具食物到箱子里的动作。最后再将玩家分数重置为0等待下一个周期后再执行重复的工作。详细指令如下
```
scoreboard objectives add chest dummy "箱子"
scoreboard players set @a[m=!2] chest 0
execute @a[m=!2] ~~~ gamemode 2 @s
scoreboard players add @a[m=2] chest 1
execute @a[m=2,scores={chest=1200..}] ~~~ replaceitem block 72 66 81 slot.container 0 stone_axe 1 0 {"minecraft:can_destroy": {"blocks": ["log"]}}
execute @a[m=2,scores={chest=1200..}] ~~~ replaceitem block 72 66 81 slot.container 1 stone_pickaxe 1 0 {"minecraft:can_destroy": {"blocks": ["stone"]}}
execute @a[m=2,scores={chest=1200..}] ~~~ replaceitem block 72 66 81 slot.container 2 stone_hoe 1 0 {"minecraft:can_destroy": {"blocks": ["自定义植物方块名称"]}}
execute @a[m=2,scores={chest=1200..}] ~~~ replaceitem block 72 66 81 slot.container 3 bread 10
scoreboard players set @a[m=2,scores={chest=1200}] chest 0
```
## 为建筑增加内饰
玩法地图中会使用到的建筑模板都没有自带精装内饰。现在我们以服装店为例添加一部份内饰:
首先我们切换至地图编辑器,我们将功能切换为选取,并打开多选模式。
<img src="./images/6.png" alt="6" style="zoom:190%;" />
点选房梁与房顶间的空隙,并逐个往上拖。
![5](./images/5.gif)
最后用灯作为材质预设,并选择填充功能对选取的区域进行统一填充。
![6](./images/6.gif)
由于基岩版存在部分方块是需要用物品才能放置出来的。当前地图编辑器暂未支持放置物品与方块处于分离状态的方块类型。我们通过点击地图编辑器右上方的**游戏模式**退出自由摄像状态,就能够打开背包并在世界里直接放置方块。这里以放置一张床为例:
![7](./images/7.gif)

View File

@@ -0,0 +1,546 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 进阶
time: 70分钟
---
# 规划可轻微变动区域
小岛上的农场之家会为玩家提供一处开辟好的农田供玩家种植庄稼,同时地图还会涉及多个供玩家采集木头、石头、的资源点。它们可以帮助玩家有足够的材料修复农场设施。在上一章节中,我们为玩家提供了一个固定刷新的工具食物箱。借助原版游戏的玩法系统,我们初步为玩家营造了一个可持续生产的生产环境。但这里也同时存在一些问题:
- 锄头可以无视等级权限开垦所有泥土。
- 木头、石头资源点被玩家挖空后无法再生。
- 玩家需要通过回收资源解锁农场之家和畜牧场。
为了解决以上问题使用ModSDK框架可以帮助我们非常迅速地解决以上问题。让我们来看看怎么做吧
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6152b923b8a81f8fa07dc899" height="600" width="800" allow="fullscreen" />
## 接入Mod环境
编辑器会在地图工程内新建Mod脚本文件夹文件夹会以**Script_NeteaseMod**加上一串随机字符组成。
<img src="./images/11.png" alt="11" style="zoom:135%;" />
> - modMain.py文件是该Mod脚本的主入口一个脚本文件夹的主目录下有且只能有一个入口文件我们也只能在这里注册自定义的服务端与客户端系统。
> - 自定义系统是个类在没有学习到Python编程中面向对象的环节前您可以简单理解为自创一个系统入口。这个入口会依据跑在服务端还是客户端的环境下被挂接到由@Mod.InitServer()或Mod.InitClient()装饰的函数里。在客户端上运行则使用clientApi.RegisterSystem接口在服务端上运行则使用serverApi.RegisterSystem接口。
> - RegisterSystem接受3个参数第一个参数是该Mod的唯一通信标识符。第二个是自定义系统名称。第三个是自定义系统类的路径。唯一通信标识符和自定义系统名称建议使用可读性强、令人印象深刻且独一无二的名字这会帮助游戏引擎能够更好地分辨出来自不同Mod的各种系统入口。使用重复性高的名字可能会导致脚本引擎在注册自定义系统时无法辨识来源而造成Mod加载失败。路径参数会合并文件路径与系统类的名字以下图代码为例若系统类在py文件里用**Main**作名称,系统文件以**blockListene**r为py文件名并且它还被包裹在Mod主目录的server文件夹内。则路径会以**Script_Netease{随机字符串}.server.blockListener.Main**来排序。
```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="NeteaseModw7ijjGNn", version="0.1")
class NeteaseModw7ijjGNn(object):
def __init__(self):
pass
@Mod.InitClient()
def NeteaseModw7ijjGNnClientInit(self):
# type: () -> None
"""Mod被挂载时在这里注册自定义MOD客户端系统"""
pass
@Mod.InitServer()
def NeteaseModw7ijjGNnServerInit(self):
# type: () -> None
"""Mod被挂载时在这里注册自定义MOD服务端系统"""
serverApi.RegisterSystem("FarmMod", "ServerBlockListenerServer",
"Script_NeteaseModw7ijjGNn.server.blockListener.Main")
pass
@Mod.DestroyClient()
def NeteaseModw7ijjGNnClientDestroy(self):
# type: () -> None
"""Mod被卸下时销毁自定义MOD客户端系统"""
pass
@Mod.DestroyServer()
def NeteaseModw7ijjGNnServerDestroy(self):
# type: () -> None
"""Mod被卸下时销毁自定义MOD客户端系统"""
pass
```
> - 自定义系统内可以监听原版事件和自定义Mod事件。
> - 根据服务端与客户端的区别 ,我们在<a href="../../../mcdocs/1-ModAPI/事件/世界.html" rel="noopenner"> 模组SDK文档 </a>用相应的原版事件来定义一个回调函数。回调函数内会返回这个事件传递的数据信息,通过对数据的提取、类型判断、创建接口,可以实现丰富的玩法逻辑。
> - 在自定义系统类里,我们也可以将常用的代码块用函数封装,实现更高的开发效率。
## 阻止玩家对其他区域做出耕地行为
在地形大观里,一共有三种方块会被锄头耕耘,它们分别是**草地**、**泥土**和**土径**。其中**泥土**是允许玩家还耕的方块类型。
我们使用**ServerBlockUseEvent**事件来监听玩家交互方块的行为,并在玩家手持石锄时阻止他们进一步操作。
![12](./images/12.png)
为了阻止玩家能够任意翻弄草地和土径,我们需要将他们加入监听方块被交互的白名单里,并在事件中取消石锄与方块的交互行为。完整代码如下:
```python
# -*- coding: UTF-8 -*-
from mod.server.system.serverSystem import ServerSystem
from mod.common.minecraftEnum import ItemPosType
import mod.server.extraServerApi as serverApi
# 自定义Mod服务端系统类
class Main(ServerSystem):
def __init__(self, namespace, system_name):
# 继承父类
ServerSystem.__init__(self, namespace, system_name)
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
# 监听交互方块事件
self.ListenForEvent(namespace, system_name,
'ServerBlockUseEvent', self, self.using_item)
# 根据文档描述原版方块需要通过添加进交互方块的白名单内才能触发ServerBlockUseEvent
block_comp = serverApi.GetEngineCompFactory().CreateBlockUseEventWhiteList(serverApi.GetLevelId())
# 在地图的方块结构里,一共受到锄头影响的两种地形方块是
self.blocked_list = ["minecraft:grass", "minecraft:grass_path"]
for block_name in self.blocked_list:
# 加入白名单
block_comp.AddBlockItemListenForUseEvent(block_name)
# 交互方块事件
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)
# 获取事件里交互的方块类型
block_name = event['blockName']
# 判断方块类型是否是土径或草地,并判断玩家手持物品是否是石锄
if carried_item and carried_item['newItemName'] == 'minecraft:stone_hoe' and block_name in self.blocked_list:
# 取消交互
event['cancel'] = True
```
最后,我们将编辑器界面切换至地图编辑器,通过游戏模式功能进入内嵌的游戏环境。现在使用锄头无法耕耘泥土和土径,但玩家还能将泥土还原成耕地!
![8](./images/8.gif)
## 循环可再生的资源点
获取金币是玩家解锁更多家具、衣服、扩大农业规模的重要途径。直接的经济来源来自玩家进行农业生产活动的收益。但农产品的成熟存在着一个客观地生长周期,作物会随着游戏随机刻的递进而提升生长阶段,这可能会使玩家觉得时间过得很枯燥。因此,加入木头与石材的资源点设定,是一种改变玩家游戏节奏的方式。我们为玩家提供额外的金币获取渠道的同时,也让他们能够更加有充实感地利用时间。
首先是使用地图编辑器预制资源区域。点击笔刷功能。
![13](./images/13.png)
若**笔刷预设面板**处于折叠状态,可以点击与其他面板的连接区域进行拉伸。这里我们使用**圆预设**默认使用高度5长度5宽度5的大小。
![14](./images/14.png)
在**混合设置**里对**笔刷**的立体区域方块进行成分设置。点击**添加成分**按钮将设定调整为笔刷形状会混合50%石头和50%木头。
![15](./images/15.png)
在教程里我们只设置5个资源点并用选取工具资源点的最左下角的坐标记录下来。最后再通过保存结构的方式将资源点保存至本地行为包内并使用ModSDK定时重置它们。
> **结构**与**素材**的区别在于前者是我的世界基岩版的通用格式而后者是MCSTUDIO所保存的模板格式。开发者和玩家可以通过分享结构在游戏里或者使用ModSDK生成出来。而素材大多情况下是围绕着MCSTUDIO去使用的。但两者功能都是将地图建筑作为模板方便我们在改变场景方块的时候能够直接调用它们。
```python
# -*- coding: UTF-8 -*-
from mod.server.system.serverSystem import ServerSystem
from mod.common.minecraftEnum import ItemPosType
import mod.server.extraServerApi as serverApi
class Main(ServerSystem):
def __init__(self, namespace, system_name):
# 继承父类
ServerSystem.__init__(self, namespace, system_name)
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
# 监听交互方块事件
# ...........
self.resources_pos = [
# 资源点1的起始坐标
(73, 64, 57),
# 资源点2的起始坐标
(51, 63, 101),
# 资源点3的起始坐标
(82, 68, 136),
# 资源点4的起始坐标
(198, 65, 102),
# 资源点5的起始坐标
(82, 68, 136)
]
# 结构名称,以行为包根目录/structures内的[文件夹名称:结构名称]为格式
self.resource_identifier = 'design:resource'
# 添加一个60秒重置资源点的定时任务
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
game_comp.AddRepeatedTimer(60.0, self.resource_placed)
# 交互方块事件
def using_item(self, event):
# .....
pass
def resource_placed(self):
# 创建放置结构的接口
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
for pos in self.resources_pos:
# 放置资源点结构
game_comp.PlaceStructure(None, pos, self.resource_identifier)
```
![9](./images/9.gif)
## 加入农场之家和畜牧场的修复方案
首先在海面上拖出完整的农场之家和畜牧场建筑模板并将它们保存成结构并去除空气方块。之后使用Delete快捷键直接删除。这么做可以最大程度减少临时建筑对地形造成的影响如吃掉部分方块把草地退回成泥土
![16](./images/16.png)
![17](./images/17.png)
接着,我们使用地图编辑器,在两个待修复的独立建筑旁利用游戏模式放置木牌,为木牌写上单独的文字提示。
![18](./images/18.png)
接下来我们就可以用ModSDK来监听玩家点击木牌根据玩家的资源储量和木牌内容来决定是否有条件修复相应建筑以下是详细代码与注释
```python
# -*- coding: UTF-8 -*-
from mod.server.system.serverSystem import ServerSystem
from mod.common.minecraftEnum import ItemPosType
import mod.server.extraServerApi as serverApi
class Main(ServerSystem):
def __init__(self, namespace, system_name):
# 继承父类
ServerSystem.__init__(self, namespace, system_name)
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
# 监听交互方块事件
self.ListenForEvent(namespace, system_name,
'ServerBlockUseEvent', self, self.using_item)
# 根据文档描述原版方块需要通过添加进交互方块的白名单内才能触发ServerBlockUseEvent
block_comp = serverApi.GetEngineCompFactory().CreateBlockUseEventWhiteList(serverApi.GetLevelId())
# 在地图的方块结构里,一共受到锄头影响的两种地形方块是
self.blocked_list = ["minecraft:grass", "minecraft:grass_path"]
for block_name in self.blocked_list:
# 加入白名单
block_comp.AddBlockItemListenForUseEvent(block_name)
# 非常重要告示牌的方块实体ID是minecraft:standing_sign而不是minecraft:sign
block_comp.AddBlockItemListenForUseEvent("minecraft:standing_sign:*")
# 储存资源点坐标
self.resources_pos = [
(73, 64, 57),
(51, 63, 101),
(82, 68, 136),
(198, 65, 102),
(82, 68, 136)
]
# 结构名称
self.resource_identifier = 'design:resource'
# 添加一个60秒重置资源点的定时任务
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
game_comp.AddRepeatedTimer(60.0, self.resource_placed)
# 交互方块事件
def using_item(self, event):
# 获取玩家ID
player_id = event['playerId']
# 创建玩家的物品接口
item_comp = serverApi.GetEngineCompFactory().CreateItem(player_id)
# 获取玩家手持物品信息
# .....
# 获取事件里交互的方块类型
block_name = event['blockName']
x = event['x']
y = event['y']
z = event['z']
# 判断方块类型是否是土径或草地,并判断玩家手持物品是否是石锄
# ......
# 判断是否是木牌
if block_name == 'minecraft:standing_sign':
# 创建方块信息接口
block_comp = serverApi.GetEngineCompFactory().CreateBlockInfo(player_id)
# 获取文告示牌字
text = block_comp.GetSignBlockText((x, y, z))
if '升级小屋' not in text:
return
# 木头数量
log_count = 10
# 石头数量
stone_count = 5
# 获取玩家背包的全部物品
item_dict_list = item_comp.GetPlayerAllItems(ItemPosType.INVENTORY)
# 通过枚举列表内的信息,遍历列表下标与物品信息
for index, item_dict in enumerate(item_dict_list):
# 如果物品是木头且剩余量大于0
if item_dict and item_dict['itemName'] == 'minecraft:log' and log_count > 0:
# 将木头数量赋值一个临时变量
temp = item_dict['count']
# 该槽的木头数量扣去余额数量
temp -= log_count
# 如果该槽的木头数量不足以吃掉全部的木头剩余量
if temp < 0:
# 设置该槽的木头数量为0即代表该槽为空
item_dict['count'] = 0
# 扣去临时贮存的木头数量
log_count -= temp
# 直接跳过后面代码进入下一次循环
continue
# 否则,扣除对应槽位的木头数量
item_dict['count'] = temp
# 清零木头所需剩余量
log_count = 0
# 如果物品是石头且剩余量大于0
if item_dict and item_dict['itemName'] == 'minecraft:stone' and stone_count > 0:
# 将木头数量赋值一个临时变量
temp = item_dict['count']
# 该槽的石头数量扣去余额数量
temp -= log_count
# 如果该槽的石头数量不足以吃掉全部的木头剩余量
if temp < 0:
# 扣去临时贮存的木头数量
item_dict['count'] = 0
# 扣去临时贮存的石头数量
stone_count -= temp
# 直接跳过后面代码进入下一次循环
continue
# 否则,扣除对应槽位的石头数量
item_dict['count'] = temp
# 清零石头所需剩余量
stone_count = 0
# 如果log_count非0且stone_count非0则放置修复的农场之家并将木牌清除
if not log_count and not stone_count:
"""
使用字典推导式,下方等价于
item_dict_map = {}
for index in range(len(item_dict_list)):
item_dict_map[(ItemPosType.INVENTORY, index)] = item_dict_list[index]
"""
item_dict_map = {(ItemPosType.INVENTORY, index): item_dict_list[index] for index in range(len(item_dict_list))}
# 设置玩家的全部槽内物品
item_comp.SetPlayerAllItems(item_dict_map)
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
# 放置家
game_comp.PlaceStructure(None, (76, 66, 80), 'design:home')
# 清除木牌
block_comp.SetBlockNew((x, y, z), {
'name': 'minecraft:air'
}, 0)
def resource_placed(self):
# 创建放置结构的接口
# ...
pass
```
可以看到,同样在下次判断玩家点击升级方块时是否满足升级要钱时,如果将判定代码块用函数封装起来,可以使得代码更加简洁,减少重复代码。这里我们用函数对一部分代码进行封装。
```python
# -*- coding: UTF-8 -*-
from mod.server.system.serverSystem import ServerSystem
from mod.common.minecraftEnum import ItemPosType
import mod.server.extraServerApi as serverApi
class Main(ServerSystem):
def __init__(self, namespace, system_name):
# ....
pass
# 交互方块事件
def using_item(self, event):
# .....
pass
def resource_placed(self):
# ...
pass
def can_upgrade_structure(self, player_id, requirement):
# type: (str, dict) -> (bool, list)
"""
:param player_id: 玩家ID
:param requirement: 物品需求,例->{"minecraft:log": 10, "minecraft:stone": 5}
:return (bool, list): 是否可以升级建筑,玩家背包信息
"""
# 创建玩家的物品接口
item_comp = serverApi.GetEngineCompFactory().CreateItem(player_id)
# 获取玩家背包的所有物品
item_dict_list = item_comp.GetPlayerAllItems(ItemPosType.INVENTORY)
# 通过枚举列表内的信息,遍历列表下标与物品信息
for index, item_dict in enumerate(item_dict_list):
# 如果该槽存在物品且物品在所需物品字典内并且所需物品对应的数量大于0时
if item_dict and item_dict['itemName'] in requirement and requirement[item_dict['itemName']] > 0:
temp = item_dict['count']
# 该槽物品数量扣去所需物品剩余数量
temp -= requirement[item_dict['itemName']]
# 如果该槽的物品数量不足以吃掉所需物品剩余数量
if temp < 0:
# 设置该槽的物品数量为0即代表该槽为空
item_dict['count'] = 0
# 扣去临时贮存的物品数量
requirement[item_dict['itemName']] -= temp
# 直接跳过后面代码进入下一次循环
continue
# 否则,扣除对应槽位的物品数量
item_dict['count'] = temp
# 清零所需物品
requirement[item_dict['itemName']] = 0
# 返回是否满足升级条件,以及清零所需物品后的背包情况
return not all(requirement.values()), item_dict_list
```
最后附上完整代码:
```python
# -*- coding: UTF-8 -*-
from mod.server.system.serverSystem import ServerSystem
from mod.common.minecraftEnum import ItemPosType
import mod.server.extraServerApi as serverApi
class Main(ServerSystem):
def __init__(self, namespace, system_name):
# 继承父类
ServerSystem.__init__(self, namespace, system_name)
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
# 监听交互方块事件
self.ListenForEvent(namespace, system_name,
'ServerBlockUseEvent', self, self.using_item)
# 根据文档描述原版方块需要通过添加进交互方块的白名单内才能触发ServerBlockUseEvent
block_comp = serverApi.GetEngineCompFactory().CreateBlockUseEventWhiteList(serverApi.GetLevelId())
# 在地图的方块结构里,一共受到锄头影响的两种地形方块是
self.blocked_list = ["minecraft:grass", "minecraft:grass_path"]
for block_name in self.blocked_list:
# 加入白名单
block_comp.AddBlockItemListenForUseEvent(block_name)
# 非常重要告示牌的方块实体ID是minecraft:standing_sign而不是minecraft:sign
block_comp.AddBlockItemListenForUseEvent('minecraft:standing_sign:*')
# 储存资源点坐标
self.resources_pos = [
(73, 64, 57),
(51, 63, 101),
(82, 68, 136),
(198, 65, 102),
(82, 68, 136)
]
# 结构名称
self.resource_identifier = 'design:resource'
# 添加一个60秒重置资源点的定时任务
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
game_comp.AddRepeatedTimer(60.0, self.resource_placed)
# 交互方块事件
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']
x = event['x']
y = event['y']
z = event['z']
# 判断方块类型是否是土径或草地,并判断玩家手持物品是否是石锄
if carried_item and carried_item['newItemName'] == 'minecraft:stone_hoe' and block_name in self.blocked_list:
# 取消交互
event['cancel'] = True
# 判断是否是告示牌
if block_name == 'minecraft:standing_sign':
block_comp = serverApi.GetEngineCompFactory().CreateBlockInfo(player_id)
text = block_comp.GetSignBlockText((x, y, z))
# 需求列表
requirement = {}
# 结构名称
structure_name = ''
# 结构放置位置
structure_pos = ()
# 小屋升级方块坐标
if '升级小屋' in text:
requirement = {'minecraft:log': 10, 'minecraft:stone': 5}
structure_name = 'design:home'
structure_pos = (76, 66, 80)
# 畜牧场升级方块坐标
elif '升级畜牧场' in text:
requirement = {'minecraft:log': 20, 'minecraft:stone': 10}
structure_name = 'design:farm'
structure_pos = (169, 66, 83)
result, items = self.can_upgrade_structure(player_id, requirement)
# 是否满足要求
if result and structure_pos and structure_name and requirement:
"""
使用字典推导式,下方等价于
item_dict_map = {}
for index in range(len(item_dict_list)):
item_dict_map[(ItemPosType.INVENTORY, index)] = item_dict_list[index]
"""
item_dict_map = {(ItemPosType.INVENTORY, index): items[index] for index in range(len(items))}
item_comp = serverApi.GetEngineCompFactory().CreateItem(player_id)
# 设置玩家的全部槽内物品
item_comp.SetPlayerAllItems(item_dict_map)
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
# 放置家
game_comp.PlaceStructure(None, structure_pos, structure_name)
# 清除木牌
block_comp.SetBlockNew((x, y, z), {
'name': 'minecraft:air'
}, 0, 0)
def resource_placed(self):
# 创建放置结构的接口
game_comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
for pos in self.resources_pos:
# 放置资源点结构
game_comp.PlaceStructure(None, pos, self.resource_identifier)
def can_upgrade_structure(self, player_id, requirement):
# type: (str, dict) -> (bool, list)
"""
:param player_id: 玩家ID
:param requirement: 物品需求,例->{"minecraft:log": 10, "minecraft:stone": 5}
:return (bool, list): 是否可以升级建筑,玩家背包信息
"""
# 创建玩家的物品接口
item_comp = serverApi.GetEngineCompFactory().CreateItem(player_id)
# 获取玩家背包的所有物品
item_dict_list = item_comp.GetPlayerAllItems(ItemPosType.INVENTORY)
# 通过枚举列表内的信息,遍历列表下标与物品信息
for index, item_dict in enumerate(item_dict_list):
# 如果该槽存在物品且物品在所需物品字典内并且所需物品对应的数量大于0时
if item_dict and item_dict['itemName'] in requirement and requirement[item_dict['itemName']] > 0:
temp = item_dict['count']
# 该槽物品数量扣去所需物品剩余数量
temp -= requirement[item_dict['itemName']]
# 如果该槽的物品数量不足以吃掉所需物品剩余数量
if temp < 0:
# 设置该槽的物品数量为0即代表该槽为空
item_dict['count'] = 0
# 扣去临时贮存的物品数量
requirement[item_dict['itemName']] -= temp
# 直接跳过后面代码进入下一次循环
continue
# 否则,扣除对应槽位的物品数量
item_dict['count'] = temp
# 清零所需物品
requirement[item_dict['itemName']] = 0
# 返回是否满足升级条件,以及清零所需物品后的背包情况
return not all(requirement.values()), item_dict_list
```
![10](./images/10.gif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@@ -0,0 +1,134 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 20分钟
selection: true
---
# 自定义NPC的基本行为
现在已经基本具备海滨小岛的内容不过还缺乏生机岛上只有玩家自己是不行的所以让我们来给小岛上添加各种各样的NPC吧。在添加NPC前需要了解什么是[自定义生物](../../10-addon教程/第07章自定义生物/课程01.认识自定义生物.md),当我们创建一个基本的自定义生物后,就该把它变换一个“形态”,让其成为可以闲逛、交易、且生机勃勃的海滨岛民。
![0](./images/0.png)
## 创建一个自定义生物
我们打开这个实体的行为文件,添加一些最基本的行为,让他们可以出现在地图上。除了基本的跳跃、开门、行走、碰撞体积、移动速度、闲逛外,还添加了一些稍微特殊的行为:
- 继承原版村民的特性,使其更加“真实”。
- 给NPC命名并且永久显示在头上。
- 让NPC永远不会消失。
> 如果对自定义生物完全不了解,建议点击文章开头的“自定义生物”超链接 深入了解。
```json
{
"format_version": "1.13.0",
"minecraft:entity": {
"description": {
"identifier": "farm:animal_shop",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false,
"runtime_identifier": "minecraft:villager" //可以理解为继承某一个生物的特性,这里指向原版村民(攻击生物会飘出生气的粒子),如果你想完全自定义一个生物,不建议添加,直接删除此行即可。
},
"components": {
"minecraft:nameable": {
"allow_name_tag_renaming":true, //是否可用命名牌改名
"always_show":true //是否永久显示
},
"minecraft:annotation.open_door": {
},
"minecraft:jump.static": {
},
"minecraft:can_climb": {
},
"minecraft:persistent":{}, //使生物永远不会消失
"minecraft:collision_box": {
"width": 0.8,
"height": 2
},
"minecraft:movement": {
"value": 0.25
},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"can_pass_doors": true,
"can_open_doors": true,
"avoid_water": true
},
"minecraft:movement.basic": {
},
"minecraft:health": {
"value": 5,
"max": 5
},
"minecraft:behavior.random_stroll": {
"priority": 7,
"speed_multiplier": 1
},
"minecraft:behavior.look_at_player": {
"priority": 8,
"look_distance": 6,
"probability": 0.02
},
"minecraft:behavior.random_look_around": {
"priority": 9
},
"minecraft:physics": {
}
},
"component_groups": {
},
"events": {
}
}
}
```
除了打开行为文件修改,还可以从**新版关卡编辑器**中直接添加,比起修改文件,后者更方便直观,所有的行为都有详细的介绍。为了更好且便利的使用,接下来简单介绍一下基本功能
![2](./images/1.gif)
图中用红色框出的部分从左至右依次是:**资源配置**、**资源管理**和**属性窗口**,所有创建的自定义内容都可以在配置中找到,点击配置就可以在右侧属性中自由修改,也可以在下方资源管理窗口找到相应的文件查看。
![3](./images/2.png)
点击左下角资源管理中选择新建-配置-实体,可以选择覆盖一个原版生物的模板,也可以创建空生物,所有的行为都由我们自己来添加,这里我们选择 "空"。
![3](./images/3.gif)
新建一个实体后,在左侧的配置窗口就可以看到了,点击打开并修改其属性,这里重点介绍。
- 整体模板:选择一个原版生物并继承其所有的行为和资源
- 配套文件 - 行为包json仅继承某一原版生物的行为部分行为、AI等
- 配套文件 - 资源包json仅继承某一原版生物的资源部分模型、动画等
- 配套文件 - 语言文件:修改生物、物品等的文字描述
- 基础属性 - 标识符:用此来定义生物,不可以与其它生物重复(前缀:名称)
- 基础属性 - 自然生成:生物是否可在世界中自然生成
- 基础属性 - 召唤生成:在创造物品栏中出现该生物的刷怪蛋
- 行为包组件:创建生物的行为,可添加多个组件(主要使用)
因为我们创建的是空,所以整体模板遗迹配套文件都为空,跳过这些,直接添加行为包组件
<img src="./images/4.png" alt="4" style="zoom:115%;" />
添加需要基本行为组件后,点击某个组件的加号,还可以继续添加或修改更精细的行为,以“可被命名”举例,点击后面的加号,添加可以被命名且永久显示。
![5](./images/5.png)
基本的行为添加完后,在编辑器中点击**开发测试**进入到游戏,这里需要注意,如果点击开发测试进入游戏修改内容,地图是不会有变化的,点击**编辑**进入地图编辑器才行,如果有特殊需要,也可以把修改后的测试存档导出再重新导入到编辑器中。
![42](./images/42.png)
把NPC生成到地图中并且使用命名牌改名试一下这里有个小细节改完名字后可以回到行为包中将**可以被命名牌修改名称**改为**False**这样NPC的名字就锁定为目前修改的名称了。
![6](./images/6.gif)

View File

@@ -0,0 +1,49 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 20分钟
---
# 为NPC设置工作区域
当所有的NPC准备就绪后需要让他们回到自己的工作岗位并且不能乱跑满足这个条件可以有如下解决方案
- 直接删除下方的的移动行为,使其只能待在原地 这种方法最简单粗暴将NPC放置在哪就一直会在原地非常安全但我们的NPC也就失去了“生机”非常死板不是很推荐此方法
```json
"minecraft:movement": {
"value": 0.25
},
"minecraft:navigation.walk": {
"can_path_over_water": true,
"can_pass_doors": true,
"can_open_doors": true,
"avoid_water": true
},
"minecraft:movement.basic": {
},
```
- 删除NPC闲逛的行为使其只能走去睡觉但是白天他就会一直待在醒来的地方
```json
"minecraft:behavior.random_stroll": {
"priority": 7,
"speed_multiplier": 1
},
```
- 最好的方法也最简单,就是利用方块将其封在一个固定的区域内,让其可以在区域内闲逛
![4](./images/7.gif)
除了以上的方法,肯定还有更好的方案等待发掘,本篇教程点到为止,为大家提供简单的解决方案。

View File

@@ -0,0 +1,748 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 进阶
time: 60分钟
---
# 给NPC添加对应的交易表
现在我们的NPC已经有了基本的行为并且会在自己的区域内等待玩家的光临接下来我们需要给NPC赋予“灵魂”使其成为一个真正的商人可以兑换各式各样的物品实现的方法也有很多种本篇我们将介绍两种实现的方法你可以选择更适合自己或者更适合地图的一种。
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=6152b9cfb8a81f8fa07dc89f" height="600" width="800" allow="fullscreen" />
## 使用Add-on添加交易表组件
第一种方法比较简单,直接在生物的行为文件中添加如下组件即可
```json
"minecraft:economy_trade_table": { //让NPC有交易表
"display_name": "entity.villager.anmial_shop", //交易表上显示的内容
"table": "trading/custom/animal_shop.json", //交易表的路径位置
"new_screen": true //是否使用新版的交易界面
}
```
然后根据写下的路径位置"trading/custom/animal_shop.json",新建交易表,交易表的路径和名称必须一致,否则是无效的。由于我们目前还没有添加交易的物品,如作物种子、家具等,所以我们先随便添加一条交易来进行演示。
```json
{
"tiers": [
{
"groups": [ //组
{
"num_to_select": 1, //从下方列表中选择1条出现在交易表中
"trades": [
{
"wants": [ //交易所需
{
"item": "minecraft:gold_ingot", //物品名称(金锭)
"quantity": 1 //数量
}
],
"gives": [ //交易所得
{
"item": "minecraft:spawn_egg:10", //物品名称(刷怪蛋:鸡)
"quantity": 1 //数量
}
]
}
]
}
]
}
]
}
```
完成以上内容后接下来进入到游戏右键NPC
![11](./images/11.gif)
可以看到,打开交易表后上面有一串名字,这里就对应行为组件里的**"display_name"**,我们可以对其直接进行修改:
```json
"minecraft:economy_trade_table": {
"display_name": "交易表", //直接对其修改
"table": "trading/custom/animal_shop.json",
"new_screen": true
}
```
![12](./images/12.png)
## 使用界面编辑器制作交易表的UI
我们这次地图的交易货币并不是存放在背包的而是用数据存储没有实体所以就不能采用第一种Add-on的方式所以为了实现NPC的交易功能必须使用UI制作可交互的界面所以我们将使用界面编辑器制作一个简易的合成表界面
![13](./images/13.png)
新建一个新的UI并尽量输入一个比较独特且有意义的名字这样在后续使用的时候更容易调试也不易冲突
![14](./images/14.png)
新建一个UI后左上可以看到控件结构我们添加的所有UI控件都将在这里点击某一控件可以在右侧的属性中进行修改调整每次创建新的UI界面都会有一个名为**main**的主画布一般为默认就好建议除主画布main外把所有的UI空间都改为有意义且容易辨识的名称
接下来在主画布下添加一个**面板控件**,将尺寸设置为合成表合适的大小,再添加**图片控件**,大小跟随上一个面板控件并添加贴图背景;
![15](./images/15.png)
随后依次添加多个**图片控件**用来当作滚动列表、关闭按钮、购买按钮以及商品信息的背景并调整到合适的大小和贴图以使UI更加美观如果在添加UI控件时出现这种层级顺序错误的情况可以将上方**自动设定层级**选项关闭,再逐一调整控件的层级即可。
![16](./images/16.png)
铺好所有背景后,依次添加需要的控件,每个控件要放到相应的背景下。
![17](./images/17.png)
点击滚动列表控件,可以在属性中看到**内容**,这里指的是滚动列表里应该放些什么,合成表需要有很多的商品,所以这里需要添加一排按钮用来选择商品,我们添加新的画布用来存储按钮模板。
![18](./images/18.png)
为了能让按钮一个个的排列,我们还需要单独创建一个网格,把按钮放进网格里并设置需要的数量。
![19](./images/19.png)
最后将网格添加到滚动列表里就获得一个完整的合成表UI了
![20](./images/20.png)
## 使用MODSDK实现交易功能
1. 新建一个py文件导入ScreenNode类新增Main类并继承ScreenNode
2. 在ClietnSystem客户端的**UiInitFinished**事件中初始化UI
3. 在ServerSystem服务端中利用**PlayerAttackEntityEvent**事件获得特定NPC和玩家的id
4. 设定玩家在点击此NPC时传达创建合成表UI的事件给客户端。
```python
class FarmClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
super(FarmClientSystem, self).__init__(namespace, systemName)
namespace = clientApi.GetEngineNamespace()
system_name = clientApi.GetEngineSystemName()
self.ListenForEvent(namespace, system_name,
'UiInitFinished', self, self.ui_init)
self.ListenForEvent("FarmMod", "ServerSystem", "create_shop_ui",
self, self.Create_Shop_UI)
def Create_Shop_UI(self,event):
self.ui = clientApi.PushScreen("Farm","new_shop")
def ui_init(self,args):
clientApi.RegisterUI("Farm","new_shop","Script_NeteaseModw7ijjGNn.uiscreen.FarmUIScreen","new_shop.main")
```
```python
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent("FarmMod", "ClientSystem", "buy_item",
self,self.PlayerBuyItem)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "PlayerAttackEntityEvent",
self,self.PlayerAttack)
def PlayerAttack(self,args):
print "攻击了"
entityid = args["victimId"]
self.playername = args["playerId"]
if entityid == "-120259084268":
self.NotifyToClient(self.playername,"create_shop_ui", args)
```
```python
class FarmUIScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
```
接下来我们点击编辑器的**开发测试**进入游戏点击对应id的NPC即可打开合成表UI不过此时还不能对UI进行任何交互接下来我们简单修改ui文件并绑定py函数实现一系列功能。
在ui文件的关闭按钮控件下修改 **"$pressed_button_name"** 这样在py中给函数添加绑定的装饰器可以直接响应按钮的交互在函数内添加关闭UI的接口。
> 需要删除ui文件按钮内的 "button_mappings" : []
```json
"close_button@common.button" : {
"$control_alpha" : 1.0,
"$default_texture" : "textures/ui/close_button_default",
"$hover_texture" : "textures/ui/close_button_default_light",
"$is_new_nine_slice" : false,
"$label_color" : [ 1, 1, 1 ],
"$label_font_scale_factor" : 1.0,
"$label_font_size" : "large",
"$label_layer" : 3,
"$label_offset" : [ 0, 0 ],
"$label_text" : "",
"$nine_slice_buttom" : 0,
"$nine_slice_left" : 0,
"$nine_slice_right" : 0,
"$nine_slice_top" : 0,
"$nineslice_size" : [ 0, 0, 0, 0 ],
"$pressed_button_name" : "%uiscreen.clicked_close_button", //修改按下的按钮的名称
"$pressed_texture" : "textures/ui/close_button_default_light",
"$texture_layer" : 2,
"alpha" : 1.0,
"anchor_from" : "center",
"anchor_to" : "center",
"bindings" : [
{
"binding_collection_name" : "",
"binding_condition" : "always_when_visible",
"binding_type" : "collection_details"
}
],
"clip_offset" : [ 0, 0 ],
"clips_children" : false,
"controls" : [
{
"default@new_shop.default" : {}
},
{
"hover@new_shop.hover" : {}
},
{
"pressed@new_shop.pressed" : {}
}
],
"draggable" : "not_draggable",
"enabled" : true,
"is_handle_button_move_event" : true,
"layer" : 1,
"max_size" : [ 0, 0 ],
"min_size" : [ 0, 0 ],
"offset" : [ 0, 0 ],
"propagate_alpha" : false,
"size" : [ "100.0%+0.0px", "100.0%+0.0px" ],
"visible" : true
},
```
```python
#关闭按钮的绑定函数,按钮按下再松开后触发
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def clicked_close_button(self,args):
print "按关闭了!"
clientApi.PopScreen()
```
![26](./images/26.gif)
然后我们需要绑定滚动列表按钮的**button_label**,也就是对按钮上的文字进行修改,并且点击按钮后会在商品信息处显示对应信息:
1. 创建一个列表变量,在里面添加文字和描述
2. ui文件的button_label中添加集合绑定数组用装饰器绑定函数修改按钮文字
3. 利用导入的模块截取按下按钮的具体index
4. 给玩家点击的按钮返回对应的描述
首先创建一个列表变量,在里面添加多个按钮上需要显示的文字和描述,因为目前还没有添加作物和种子,就先随便写一些东西用作测试:
```python
# coding=utf-8
self.item_button_text = [
{
"itemtext": "点击选择:小麦", # 商品的名称,显示在按钮上
"information": "品种:小麦\n生长周期3\n价格5", # 商品的详细信息展示在UI右侧
},
{
"itemtext": "点击选择:茼蒿",
"information": "品种:茼蒿\n生长周期3\n价格10"
},
{
"itemtext": "点击选择:玉米",
"information": "品种:玉米\n生长周期3\n价格15"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5"
},
{
"itemtext": "点击选择:苹果",
"information": "品种:苹果\n生长周期3\n价格25"
}
]
```
在button_label控件也就是按钮上的文字添加一个绑定并且给绑定函数返回的值赋予在“text”上这样就可以实现获取集合中按钮的index值并返回文字同时还要在按钮的控件下添加集合的变量名称
```json
"button_label" : {
"alpha" : "$control_alpha",
"color" : "$label_color",
"font_scale_factor" : "$label_font_scale_factor",
"font_size" : "$label_font_size",
"font_type" : "smooth",
"layer" : "$label_layer",
"max_size" : [ "100%", "100%" ],
"offset" : "$label_offset",
"shadow" : false,
"text" : "#text",
"text_alignment" : "center",
"type" : "label",
"bindings" : [
{
"binding_collection_name" : "$shop_grid_collection_name", //绑定的集合变量名
"binding_name":"#item_button_text", //绑定名称,用在装饰器上
"binding_name_override":"#text", //对应上面的#text
"binding_condition" : "visible", //绑定条件:显示
"binding_type" : "collection" //绑定类型:集合
}
]
},
```
```json
"button@common.button" : {
"$control_alpha" : 1.0,
"$default_texture" : "textures/ui/pocket_button_default",
"$hover_texture" : "textures/ui/pocket_button_hover",
"$is_new_nine_slice" : false,
"$label_color" : [ 1, 1, 1 ],
"$label_font_scale_factor" : 1.0,
"$label_font_size" : "large",
"$label_layer" : 3,
"$label_offset" : [ 0, 0 ],
"$label_text" : "",
"$nine_slice_buttom" : 0,
"$nine_slice_left" : 0,
"$nine_slice_right" : 0,
"$nine_slice_top" : 0,
"$nineslice_size" : [ 0, 0, 0, 0 ],
"$shop_grid_collection_name" : "shop_grid", //集合变量的名称
"$pressed_button_name" : "%uiscreen.clicked_item_button",
"$pressed_texture" : "textures/ui/pocket_button_pressed",
"$texture_layer" : 2,
"alpha" : 1.0,
"anchor_from" : "center",
"anchor_to" : "center",
"bindings" : [
{
"binding_collection_name" : "",
"binding_condition" : "always_when_visible",
"binding_type" : "collection_details"
}
],
"clip_offset" : [ 0, 0 ],
"clips_children" : false,
"controls" : [
{
"default@new_shop.default" : {}
},
{
"hover@new_shop.hover" : {}
},
{
"pressed@new_shop.pressed" : {}
},
{
"button_label@new_shop.button_label" : {}
}
],
"draggable" : "not_draggable",
"enabled" : true,
"is_handle_button_move_event" : true,
"layer" : 1,
"max_size" : [ 0, 0 ],
"min_size" : [ 0, 0 ],
"offset" : [ 0, 0 ],
"propagate_alpha" : false,
"size" : [ "100.0%+0.0px", "100.0%+0.0px" ],
"visible" : true
},
```
```python
#集合绑定字符串,给网格内的按钮添加对应的文字
@ViewBinder.binding_collection(ViewBinder.BF_BindString,"shop_grid","#item_button_text")
def binding_item_button_text(self,index):
print index
return self.item_button_text[index]["itemtext"] #返回此函数对应变量index的文字
```
导入re模块并使用**compile**函数和**findall**函数截取玩家实际点击按钮的index值并且在ui文件中修改这个按钮控件的**"$pressed_button_name"**,使装饰器绑定这个按钮:
```json
"button@common.button" : {
"$control_alpha" : 1.0,
"$default_texture" : "textures/ui/pocket_button_default",
"$hover_texture" : "textures/ui/pocket_button_hover",
"$is_new_nine_slice" : false,
"$label_color" : [ 1, 1, 1 ],
"$label_font_scale_factor" : 1.0,
"$label_font_size" : "large",
"$label_layer" : 3,
"$label_offset" : [ 0, 0 ],
"$label_text" : "",
"$nine_slice_buttom" : 0,
"$nine_slice_left" : 0,
"$nine_slice_right" : 0,
"$nine_slice_top" : 0,
"$nineslice_size" : [ 0, 0, 0, 0 ],
"$shop_grid_collection_name" : "shop_grid",
"$pressed_button_name" : "%uiscreen.clicked_item_button", //修改按下的按钮的名称
"$pressed_texture" : "textures/ui/pocket_button_pressed",
"$texture_layer" : 2,
"alpha" : 1.0,
"anchor_from" : "center",
"anchor_to" : "center",
"bindings" : [
{
"binding_collection_name" : "",
"binding_condition" : "always_when_visible",
"binding_type" : "collection_details"
}
],
"clip_offset" : [ 0, 0 ],
"clips_children" : false,
"controls" : [
{
"default@new_shop.default" : {}
},
{
"hover@new_shop.hover" : {}
},
{
"pressed@new_shop.pressed" : {}
},
{
"button_label@new_shop.button_label" : {}
}
],
"draggable" : "not_draggable",
"enabled" : true,
"is_handle_button_move_event" : true,
"layer" : 1,
"max_size" : [ 0, 0 ],
"min_size" : [ 0, 0 ],
"offset" : [ 0, 0 ],
"propagate_alpha" : false,
"size" : [ "100.0%+0.0px", "100.0%+0.0px" ],
"visible" : true
},
```
```python
#按钮的绑定函数按下时通过re函数计算出按下的是滚动列表中的第几个按钮
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def clicked_item_button(self,args):
print "按下啦"
buttonpath = args["ButtonPath"].split('/')[-2] #使用导入的re模块截取按钮的index数
reg = re.compile(r'\d+')
button_index = reg.findall(buttonpath)
if button_index:
self.clicked_button_index = int(button_index[0])-1 #获取到的index数保存在此变量中
```
当玩家点击某个按钮时就会获取具体的index值同时就可以为这个按钮添加描述当然information对应的ui控件也需要添加绑定
```python
#绑定字符串,给商品信息添加对应的描述
@ViewBinder.binding(ViewBinder.BF_BindString,"#shop_information")
def binding_shop_information(self):
if self.clicked_button_index == -1: #如果点击按钮的index是-1则返回一个空字符串
return ""
return self.item_button_text[self.clicked_button_index]["information"] #返回对应index的描述信息
```
```json
"item_information" : {
"alpha" : 1.0,
"anchor_from" : "top_left",
"anchor_to" : "top_left",
"clip_offset" : [ 0, 0 ],
"clips_children" : false,
"color" : [ 1, 1, 1 ],
"enabled" : true,
"font_scale_factor" : 1.0,
"font_size" : "normal",
"font_type" : "smooth",
"layer" : 1,
"line_padding" : 0.0,
"max_size" : [ 0, 0 ],
"min_size" : [ 0, 0 ],
"offset" : [ 5, 5 ],
"propagate_alpha" : false,
"shadow" : false,
"size" : [ "75.0%+0.0px", "75.0%+0.0px" ],
"text" : "#text",
"text_alignment" : "left",
"type" : "label",
"visible" : true,
"bindings" : [
{
"binding_name":"#shop_information", //用在装饰器上绑定
"binding_name_override":"#text", //对应上面的#text
"binding_condition" : "always_when_visible" //绑定条件:总是显示
}
]
},
```
完成以上步骤后合成表UI已经有了大致的内容
![35](./images/35.gif)
现在我们来添加最后也是最关键的内容通过交易表UI进行购买首先通过装饰器和修改ui文件绑定购买的按钮在绑定的函数下写相关的逻辑
1. 如果玩家没有选择左侧的某样物品则返回
2. 获取玩家现在有的钱数并对比商品价格
3. 如果玩家的钱可以购买则继续,如果不能则返回
4. 购买成功后向服务端通信,给予玩家购买的物品
代码如下:
```python
# -*- coding: utf-8 -*-
self.item_button_text = [
{
"itemtext":"点击选择:小麦", #商品的名称,显示在按钮上
"information":"品种:小麦\n生长周期3\n价格5", #商品的详细信息展示在UI右侧
"coin": 5, #购买商品的价格
"itemname":"minecraft:grass" #用于读取实际商品以给予玩家
},
{
"itemtext": "点击选择:茼蒿",
"information": "品种:茼蒿\n生长周期3\n价格10",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:玉米",
"information": "品种:玉米\n生长周期3\n价格15",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:苹果",
"information": "品种:苹果\n生长周期3\n价格25",
"coin": 5,
"itemname": "minecraft:grass"
}
]
```
先给商品的列表变量添加价格和实际商品的名称用于接下来的逻辑判断和物品发放;
```json
"buy_button@common.button" : {
"$control_alpha" : 1.0,
"$default_texture" : "textures/ui/pocket_button_default",
"$hover_texture" : "textures/ui/pocket_button_hover",
"$is_new_nine_slice" : false,
"$label_color" : [ 1, 1, 1 ],
"$label_font_scale_factor" : 1.0,
"$label_font_size" : "large",
"$label_layer" : 3,
"$label_offset" : [ 0, 0 ],
"$label_text" : "点击购买",
"$nine_slice_buttom" : 0,
"$nine_slice_left" : 0,
"$nine_slice_right" : 0,
"$nine_slice_top" : 0,
"$nineslice_size" : [ 0, 0, 0, 0 ],
"$pressed_button_name" : "%uiscreen.buy_button_clicked", //和前面的按钮一样,修改按下按钮的名称
"$pressed_texture" : "textures/ui/pocket_button_pressed",
"$texture_layer" : 2,
"alpha" : 1.0,
"anchor_from" : "center",
"anchor_to" : "center",
"bindings" : [
{
"binding_collection_name" : "",
"binding_condition" : "always_when_visible",
"binding_type" : "collection_details"
}
],
"clip_offset" : [ 0, 0 ],
"clips_children" : false,
"controls" : [
{
"default@new_shop.default" : {}
},
{
"hover@new_shop.hover" : {}
},
{
"pressed@new_shop.pressed" : {}
},
{
"buy_button_label@new_shop.buy_button_label" : {}
}
],
"draggable" : "not_draggable",
"enabled" : true,
"is_handle_button_move_event" : true,
"layer" : 1,
"max_size" : [ 0, 0 ],
"min_size" : [ 0, 0 ],
"offset" : [ 0, 0 ],
"propagate_alpha" : false,
"size" : [ "100.0%+0.0px", "100.0%+0.0px" ],
"visible" : true
},
```
修改购买按钮ui文件的**$pressed_button_name**用于在py文件中绑定
```python
class FarmUIScreen(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.clientsystem = clientApi.GetSystem("FarmMod", "ClientSystem") #获取客户端实例
self.clicked_button_index = -1 #按钮的index值
self.coin = 0 #定义玩家的钱数
self.item_button_text = [
{
"itemtext":"点击选择:小麦", #商品的名称,显示在按钮上
"information":"品种:小麦\n生长周期3\n价格5", #商品的详细信息展示在UI右侧
"coin": 5, #购买商品的价格
"itemname":"minecraft:grass" #用于读取实际商品以给予玩家
},
{
"itemtext": "点击选择:茼蒿",
"information": "品种:茼蒿\n生长周期3\n价格10",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:玉米",
"information": "品种:玉米\n生长周期3\n价格15",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:小麦",
"information": "品种:小麦\n生长周期3\n价格5",
"coin": 5,
"itemname": "minecraft:grass"
},
{
"itemtext": "点击选择:苹果",
"information": "品种:苹果\n生长周期3\n价格25",
"coin": 5,
"itemname": "minecraft:grass"
},
]
@ViewBinder.binding(ViewBinder.BF_ButtonClickUp)
def buy_button_clicked(self,args):
print "点击购买了"
if self.clicked_button_index == -1:
print "玩家还没选物品"
return
price = self.item_button_text[self.clicked_button_index]['coin']
print "他想买的东西价值:",price
print "你有的钱数:",self.coin
if self.coin >= price:
self.coin -= price
print "买完以后你还剩:",self.coin
#向服务端通信将玩家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 "你买不起"
```
使用装饰器绑定函数注意此时的函数名和上方ui文件的**$pressed_button_name**必须一致;获取客户端实例并在玩家购买完成后通知服务端发放物品;
```python
class FarmServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
self.ListenForEvent("FarmMod", "ClientSystem", "buy_item",
self,self.PlayerBuyItem)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "PlayerAttackEntityEvent",
self,self.PlayerAttack)
def PlayerBuyItem(self, args):
print "玩家买到了"
player_id = args['playerid'] #传过来的玩家id
item_name = args['buy_item'] #传过来的实际商品名称
serverApi.GetEngineCompFactory().CreateItem(player_id).SpawnItemToPlayerInv( #发放物品
{
'newItemName': item_name,
'count': 1
},
player_id
)
```
```python
def Create_Shop_UI(self,event):
self.ui = clientApi.PushScreen("Farm","new_shop")
self.ui.coin = 100 #修改这个ui实例的硬币数量
```
在创建合成表时先将玩家的钱数设定为100然后进入到游戏中测试一下
![41](./images/41.gif)

View File

@@ -0,0 +1,123 @@
---
front: https://nie.res.netease.com/r/pic/20210730/ee109f39-8987-46e0-9fe7-40ebb23060fa.png
hard: 入门
time: 20分钟
---
# 给NPC设置睡觉的床
现在我们已经有一个任劳任怨并且可以和我们交易的商人啦但是商人们只能工作不能休息也太惨了吧所以我们需要让NPC们和玩家一样拥有自然的作息而不是一个冷冰冰的NPC这样也可以一定程度上增加游戏的代入感让玩家更好的享受乐趣。
![8](./images/8.png)
## 让NPC绑定属于自己的床
每个NPC都要有一个自己的床不然他们就要开始互相抢床导致混乱这就麻烦了。我们只需要在NPC行为里面添加一个行为组件就行了。
```json
"minecraft:dweller": {
"dwelling_type": "village", //类型
"dweller_role": "inhabitant", //角色因为这张地图的NPC都是无职业的如果想要添加给NPC添加职业可以参考原版Villager_v2的行为组件
"update_interval_base": 20, //基础更新间隔
"update_interval_variant": 10, //变种更新间隔
"can_find_poi": true //是否可以寻找poi
}
```
进入到游戏中试一下把NPC放在床的附近当他和床同时发出绿色粒子的时候就成功啦现在这个床就属于这个NPC了当然我们玩家还是可以把他的床抢过来。
![9](./images/9.png)
## 让NPC到床上睡觉
现在NPC已经有属于自己的床了不过他们还不会去睡觉接下来再加一个行为组件
```json
"minecraft:behavior.sleep": {
"speed_multiplier": 1.5, //去睡觉时的移动速度倍增
"sleep_collider_height": 0.3, //睡觉时生物碰撞箱的高度
"sleep_collider_width": 1.0, //睡觉时生物碰撞箱的宽度
"sleep_y_offset": 0.6, //睡觉时生物Y轴的偏移量
"timeout_cooldown": 10.0 //发生意外起床后,再过多长时间可以再次睡觉
}
```
添加这个行为组件后,生物就会跑去睡觉啦,当然别忘了留下可以移动的行为组件。
这里还有一个小问题,如果我们给生物添加基本的睡觉行为,那么当生物找到床以后不管是白天还是晚上都会一直睡觉,所以我们需要利用**事件**和**组件组**配合让NPC只有在晚上的时候才会睡觉。
有关components、events与component_groups三者之间的关系可以参考往期教程[定义生物行为的三种结构](https://g.126.fm/04a9tkE)。
首先我们添加一个可以检测时间的组件,分别在白天和晚上触发不同的事件:
```json
"minecraft:scheduler": {
"min_delay_secs": 0, //最小延迟数
"max_delay_secs": 10, //最大延迟数
"scheduled_events" : [ //调度事件
{
"filters": { //过滤器(第一个)
"all_of": [ //满足所有条件
{ "test": "hourly_clock_time", "operator": ">=", "value": 0 }, //当事件大于等于0时代表白天开始了
{ "test": "hourly_clock_time", "operator": "<", "value": 12000 } //小于12000时代表白天还没有结束
]
},
"event": "minecraft:work" //触发事件"minecraft:work"
},
{
"filters": { //过滤器(第二个)
"all_of": [ //满足所有条件
{ "test": "hourly_clock_time", "operator": ">=", "value": 12000 }, //大于等于12000代表夜晚开始了
{ "test": "hourly_clock_time", "operator": "<", "value": 24000 } //小于24000代表夜晚还没有结束
]
},
"event": "minecraft:sleep" //触发事件"minecraft:sleep"
}
]
},
```
加上这个行为组件后,白天的时候就会触发**work**的事件,晚上就会触发**sleep**的事件接下来添加这两个事件分别在触发work事件的时候让村民起床在触发sleep事件的时候让村民睡觉。
```json
"events": { //事件所有的事件都放在这下面和components行为组件在同一排
"minecraft:sleep": { //当满足过滤器(第二个)时触发的事件
"remove": { "component_groups": [ "villager_work" ] }, //移除"villager_work"组件组
"add": { "component_groups": [ "villager_sleep" ] }
}, //添加"villager_sleep"组件组
"minecraft:work": { //当满足过滤器(第一个)时触发的事件
"remove": { "component_groups": [ "villager_sleep" ] }, //移除"villager_sleep"组件组
"add": { "component_groups": [ "villager_work" ] } //添加"villager_work"组件组
}
}
```
```json
"component_groups": { //组件组,触发事件后可随意移除和添加一组或多个组
"villager_sleep": { //与事件一起看,当满足过滤器(第二个)时就会添加这个组件组,而这个组件组下只有一个组件,就是去睡觉
"minecraft:behavior.sleep": {
"priority": 3,
"goal_radius": 1.5,
"speed_multiplier": 1.5,
"sleep_collider_height": 0.3,
"sleep_collider_width": 1.0,
"sleep_y_offset": 0.6,
"timeout_cooldown": 10.0
}
},
"villager_work": { //当满足过滤器第一个时就会添加这个组件组并且移除睡觉的组件组这时候生物就会起床现在这个组件组里什么都没有所以NPC起床后也没什么变化
}
},
```
把上面的内容添加到NPC的行为文件里以后我们的商人就会在白天起床晚上去睡觉啦。进入游戏来看一下实际效果
![10](./images/10.gif)

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%;" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

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