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,33 @@
# 开服工具2.0简介
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818855ef3bb6958baf37fc" width="800" height="600" allow="fullscreen"/>
开服工具2.0是基于网络游戏开服工具、Geyser等框架的基础上改进而来的一种开服技术。开发者可以使用此技术通过Java版Spigot服务端来架设我的世界中国版手机端服务器。
玩家可以利用网络游戏开服工具制作专用于我的世界中国版手机端的Mod来代替原来Java服的Forge Mod实现客户端UI、特效、音效、模型等效果。
## 架构
![](./images/frame.png)
框架图如上图所示其中Java服部分和传统的Spigot服务器一致主要包括以下一些服务器
- Spigot用来承载玩家游戏
- BC代理服
- DB数据库
剩余部分主要分为以下服务器:
- Geyser协议转换服务器用来将基岩版协议转换到Java版协议
- master网络游戏开服工具的master端主要用于控制整个集群开发者无需过多关注
- proxy代理服用来分发玩家连接可以理解为基岩版的BC服
- client中国版基岩版客户端
想要制作开服工具2.0的服务器,至少需要掌握以下开发技术:
- Spigot插件开发
- ModSDK模组制作
- 任意一款数据库的使用

View File

@@ -0,0 +1,89 @@
# 小小云的申请和连接
小小云是我的世界中国版免费发放给开发者用来制作手机版服务器玩法的一台云服务器。需要开发者提前在开发者平台上进行申请。
小小云使用的是Debian操作系统需要使用任意一款SSH连接工具进行连接。本教程中将会使用XShell和XFtp这两款软件介绍小小云的申请和使用的相关流程。
## 如何申请
### 生成RSA密钥对
提前安装好XShell打开XShell之后在菜单栏中找到工具选择**用户密钥管理者**。
![](./images/01.png)
在弹出的窗口中点击右侧生成按钮。
![](./images/02.png)
保持选项默认,点击下一步,直到出现下图的界面,填写密钥的名称。名称可以任意填写,仅作区分用途。
密码可以留空,也可以自行进行设置。**需要注意的是,如果忘记密码,没有任何渠道找回。**
![](./images/03.png)
点击下一步,在公钥处,点击保存为文件,并记住文件位置。随后点击完成。
![](./images/04.png)
### 提交申请
前往开发者平台,提交手机版网络游戏入驻申请。[地址](https://mcdev.webapp.163.com/#/pe-server/application/edit/new)
勾选Spigot集成支持并自行填写基本信息。
![](./images/05.png)
在下一步上传刚刚保存的公钥文件。正式服和测试服上传同一文件。
> 正式服和测试服可以使用不同的公钥。
>
> 在开发阶段,我们可以将正式服和测试服上传同一个公钥,在正式运营前,再重新生成并上传一个正式服所用的公钥。
![](./images/06.png)
## 如何连接
在申请到开发机之后可以进入开发者平台服务器使用情况处查看服务器IP地址等信息。
![](./images/07.png)
在开始连接服务器之前,首先需要访问白名单网页来获取临时白名单。
找到服务器地址后,打开浏览器,输入网址:**http://temp-white-list.mc.netease.com:9999/?machine=机器IP**,来获取白名单。
例如机器IP是42.186.1.1,那么网址就是: http://temp-white-list.mc.netease.com:9999/?machine=42.186.1.1。
正确访问后应该出现以下提示:
![](./images/08.png)
访问过后等待1分钟即可为当前IP添加白名单。
打开XShell点击 文件->新建。其中名称可以任意填写主机填写服务器IP地址或自己解析的域名端口号32200。
![](./images/09.png)
接下来切换到用户身份认证用户名填写fuzhu将方法的Password取消勾选并勾选Public Key点击右侧设置。
![](./images/10.png)
在弹出的窗口中选择之前创建的用户密钥,如有密码输入密码,点击确定。
![](./images/11.png)
并点击”新建会话属性“的确定按钮,并在会话管理器中选择刚刚创建的会话,双击即可连接。
连接成功如下图:
![](./images/12.png)
同时小小云内置了MySQLRedis和MongoDB。
默认数据库连接信息如下:
- MySQL 主机127.0.0.1 端口3306 用户名 minecraft 密码 minecraft 数据库 minecraft
- Redis 主机127.0.0.1 端口 6379 密码 mcnetgame
- Mongo 主机127.0.0.1 端口 27017 用户名 minecraft 密码 minecraft 数据库 minecraft
要使用前端工具例如Navicat来连接并管理数据库可以[点我参考](../../../30-网络服插件教程/1-准备知识/3-数据库的概念.md)。

View File

@@ -0,0 +1,84 @@
# Java服的搭建与部署
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818951c31a9c0f360dc572" width="800" height="600" allow="fullscreen"/>
本章将会介绍如何部署开服工具2.0架构中的Java服部分。部署服务器和传统的Java服类似唯一的区别就是
在制作和开发开服工具2.0的玩法时,必须将服务器运行在小小云中。
因此我们需要提前准备好BC端和对应的Spigot端将其上传到小小云的SFTP中。
## 准备BC端和Spigot端
首先下载对应的BC端和Spigot端jar文件。
部分需要下载的插件,可以自行提前下载 [链接](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-手机网络游戏/课程10使用Spigot开服/99-下载内容.html?catalog=1)
ViaVersion [链接](https://www.spigotmc.org/resources/viaversion.19254/)
### BC
BC端下载前往 [CI构建站](https://ci.md-5.net/job/BungeeCord/) 下载最新的BungeeCord.jar并存放到一个空文件夹中。
打开cmd输入启动指令`java -jar BungeeCord.jar`,等待下载资源并生成默认配置文件。
![](./images/13.png)
随后进入`plugins`文件夹下载BungeeMaster插件并拖入`plugins`文件夹
随后新建BungeeMaster文件夹新建`config.yml`为BungeeMaster的配置文件。
填入以下内容
```yaml
master_port: 29001
server_id: 12001
```
填写要求
- server_id 取值区间在[12000, 14000)内并且保持单次部署唯一即可代表部署时的服务器id
- master_port端口范围要求[29000,31000),代表控制服端口
- **master_port**是指BC服监听Master服连接所用端口请注意与下述用于原生游戏的**query_port**区分,以免造成端口冲突
- 填写完毕后请记下相关参数后面需要在studio中填写
接下来需要配置BC端的配置文件`config.yml`其位置在与BungeeCord.jar的同级目录中。配置bc服监听的端口端口范围要求[29000,31000)
并且**关闭**正版验证`online_mode`
![](./images/14.png)
剩下bc配置部分和传统Java服类似BC配置参考文档[点我](https://www.spigotmc.org/wiki/bungeecord-configuration-guide/)。
### Spigot
开服工具2.0官方推荐使用Spigot1.12.2需要使用官方构建工具进行构建具体构建教程见SpigotMC官网。[下载链接](https://hub.spigotmc.org/jenkins/job/BuildTools/)
创建一个空文件夹并准备最新版Spigot的jar文件并在cmd输入`java -jar 文件名`,启动服务器。
- 同意EULA
- 下载SpigotMaster插件放入plugins文件夹
- 下载ViaVersion插件放入plugins文件夹
- 关闭正版验证
随后关闭服务器将两个文件夹打包通过SFTP上传到小小云。
根据官方开发规范要求,将文件解压到指定目录。
> - **BungeeCord服需要部署在以下路径**`~/bc/*/BungeeCord.jar`
>
> 例如:`~/bc/bc1/BungeeCord.jar`
>
> - **Spigot服需要部署在以下路径**`~/spigot/*/spigot-1.12.2.jar`
>
> 例如:`~/spigot/lobby1/spigot-1.12.2.jar`
## 启动服务器
使用screen命令新建一个名为bc的screen`screen -S bc`cd到BungeeCord.jar的目录后输入`java8 -jar BungeeCord.jar`启动服务器。Java命令行参数可以自行配置。
![](./images/15.png)
服务器启动成功后,按下组合键`Ctrl A D`退出screen再使用同样的操作新建一个screen来运行spigot端。
要切换到以前的screen输入`screen -rx screen名`即可。

View File

@@ -0,0 +1,28 @@
# 开发者工作台的使用
在完成了小小云上Java服的搭建与上传后我们需要继续前往开发者工作台完成开服工具2.0其他服务器的部署。
登陆开发者工作台之后,切换到基岩版服务器分类。点击新建,创建一个**空白Spigot服**。
![](./images/16.png)
在服务器配置窗口中确认机器列表中的机器为自己的开发机IP地址。
切换到游戏配置标签页中确认控制服、代理服、协议服、BC服都至少有一个没有的点击增加。除了BC服之外其他配置保持默认即可。
BC服的配置信息根据在上一节中BC服的相关配置填写如果填写时完全按照教程的步骤操作那么可以参考下方填写。
![](./images/17.png)
填写完成后点击完成,并部署。
![](./images/18.png)
出现下方提示时代表部署完成,就可以点击启动测试,进入游戏进行测试了。
![](./images/19.png)
配置正确的情况下可以正常进入Java服并且BC服的控制台可以看到BungeeMaster插件的输出。
![](./images/20.png)

View File

@@ -0,0 +1,242 @@
# Spigot插件编写
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818bb8c31a9c0f360dc5b2" width="800" height="600" allow="fullscreen"/>
## 语言基础
编写Spigot插件需要一定的Java语言基础并在后续开发中会使用到Maven或Gradle来构建Spigot插件在此仅提供部分链接供参考需要开发者自行学习。
- [Java](https://www.runoob.com/java/java-tutorial.html)
- [Maven](https://maven.apache.org/)
- [Gradle](https://gradle.org/)
在掌握Java语言基础后需要继续学习Spigot、BC的API并进行插件开发。同样也需要开发者自行进行学习。
- [Spigot插件开发教程](https://www.spigotmc.org/wiki/spigot-plugin-development/)
- [BC插件开发教程](https://www.spigotmc.org/wiki/bungeecord-plugin-development/)
## 开始编写Spigot插件
### 创建项目
> 本教程会使用IntelliJ IDEA来进行插件开发并使用Minecraft Development插件来快速创建项目。
>
> 如果没有安装的可以提前进行安装
>
> ![](./images/01.png)
首先点击创建项目找到Minecraft分类选择Spigot插件。
![](./images/02.png)
接着自行填写GroupId和ArtifactId点击下一步后进行如下更改
1. 选择Minecraft Version为1.12.2
2. 填写Depend为SpigotMaster
![](./images/03.png)
点击下一步后,选择目录和目录名进行项目创建。
### 添加依赖
在下载SpigotMaster插件后复制文件路径。将下方指令中的path-to-jar替换为路径x.x.x替换为版本号。
SpigotMaster插件和之前安装到服务器里的是同一个可以点[](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-手机网络游戏/课程10使用Spigot开服/99-下载内容.html?catalog=1)查看下载方式。
```
mvn install:install-file -Dfile=path-to-jar -DgroupId=com.neteasemc -DartifactId=SpigotMaster -Dversion=x.x.x-SNAPSHOT -Dpackaging=jar
```
在IDEA的Maven指令中执行
![](./images/04.png)
执行成功后会看到BUILD SUCCESS的输出。
![](./images/05.png)
接下来在pom.xml中配置dependency添加依赖。
```
<dependency>
<groupId>com.neteasemc</groupId>
<artifactId>SpigotMaster</artifactId>
<version>1.3.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
```
其中版本处需要替换为自己安装的版本。添加完成后dependency如下。
![](./images/06.png)
### 代码编写
完成了依赖的添加就可以使用SpigotMaster来和中国版基岩版客户端进行通信了。
在SpigotMaster的接口中涉及到与客户端通信的方法主要有两个。
- listenForEvent
- notifyToClient
使用SpigotMaster实例下的这两个方法将可以实现绝大多数通信的需求。
其他方法可以参考:[SpigotMaster文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-手机网络游戏/课程10使用Spigot开服/81-SpigotMasterAPI文档.html?catalog=1)
SpigotMaster的实例可以通过下方代码获取。
```java
public void onEnable() {
SpigotMaster spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
}
```
#### listenForEvent
使用该方法可以监听客户端发送来的事件,需要提供参数:
- `namespace` - 来源客户端系统的namespace
- `system` - 来源客户端系统的systemName
- `event` - 事件名
- `handler` - 回调函数
在这里我们先暂时编写一个简单的监听事件的函数。
```java
package me.zhanshi123.apollo2example;
import com.neteasemc.spigotmaster.SpigotMaster;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
public final class Apollo2Example extends JavaPlugin {
private static Apollo2Example instance;
public static Apollo2Example getInstance() {
return instance;
}
private SpigotMaster spigotMaster;
public SpigotMaster getSpigotMaster() {
return spigotMaster;
}
@Override
public void onEnable() {
instance=this;
spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
spigotMaster.listenForEvent("testMod", "testModBeh", "TestEvent", (player, map) -> {
getLogger().info(player.getName());
map.forEach((key, value) -> getLogger().info("k: " + key + "v: " + value));
});
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}
```
调用这个方法将会注册一个命名空间为testMod系统名为testModBeh的TestEvent事件并在收到数据时打印发送的玩家名和事件的信息字典。
Java的listenForEvent的回调函数会在Python端调用NotifyToServer时触发。对应的代码如下
```python
self.NotifyToServer("TestEvent", {"data": "测试数据"})
```
在**ClientSystem**内调用**NotifyToServer**方法传递事件和对应参数。完整的Python代码见下一节。
#### notifyToClient
该方法可以主动给客户端发送事件,需要提供参数:
- `player` - 接收事件的玩家
- `namespace` - 在客户端系统使用ListenForEvent监听的namespace
- `system` - 在客户端系统使用ListenForEvent监听的systemName
- `event` - 事件名
- `data` - 事件参数。注意,要使用-2指代本地玩家的entityId。
我们同样编写一个指令用来后续测试Python端是否能正常收到消息
首先在plugin.yml中注册指令
```yaml
commands:
apollotest:
```
接下来新建一个类实现CommandExecutor接口在玩家执行指令时调用SpigotMaster实例并notifyToClient
```java
package me.zhanshi123.apollo2example;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class Commands implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
return true;
}
Player player = (Player) sender;
Map<String, Object> data = new HashMap<>();
data.put("msg", "这是一条来自Java服的消息");
Apollo2Example.getInstance().getSpigotMaster()
.notifyToClient(player, "testMod", "testModDev", "TestServerEvent", data);
player.sendMessage("notifyToClient已执行");
return true;
}
}
```
在这里我们将作为testMod这个命名空间的testModDev的系统发送事件事件名为TestServerEvent数据为一个Map它会在客户端被读取为Python的字典类型进行处理其中的数据也会被转换为Python类型
| Java类型 | Python类型 |
| ------------------------ | ---------- |
| null | None |
| boolean | bool |
| int | int |
| long | long |
| BigInteger(2^63到2^64-1) | long |
| float | float |
| double | float |
| String | str |
| List\<Object\> | list |
| Map<String, Object> | dict |
对于Python端需要接受来自Java端的notifyToClient发送的数据需要调用**ClientSystem**的**ListenForEvent**方法,对应的代码如下:
这样就会在收到事件时打印参数。完整的Python代码见下一节。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent(ModConst.ModName, ModConst.ServerSystemName, "TestServerEvent", self, self.OnServerEvent)
def OnServerEvent(self, args):
print "OnServerEvent", args
def Destroy(self):
self.UnListenForEvent(ModConst.ModName, ModConst.ServerSystemName, "TestServerEvent", self, self.OnServerEvent)
```
接下来在onEnable方法中进行注册。
```java
Bukkit.getPluginCommand("apollotest").setExecutor(new Commands());
```
随后执行`mvn package`即可对插件进行打包随后可以上传至小小云对应Spigot端的plugins文件夹。
然后重启服务器,即可让插件加载。

View File

@@ -0,0 +1,188 @@
# 客户端模组编写
本节将主要介绍如何制作客户端模组与Java服插件进行通信。
## 语言基础
编写中国版基岩版客户端模组需要掌握Python2.7、模组SDK。
该部分较为基础,需要开发者自行安装并学习。推荐提前安装[补全库](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/20-%E7%8E%A9%E6%B3%95%E5%BC%80%E5%8F%91/13-%E6%A8%A1%E7%BB%84SDK%E7%BC%96%E7%A8%8B/2-Python%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91/0-%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8.html?key=%E8%A1%A5%E5%85%A8&docindex=1&type=0#%E5%AE%89%E8%A3%85mod-sdk%E8%A1%A5%E5%85%A8%E5%BA%93)。
- [Python2.7](https://www.python.org/downloads/release/python-2718/)
- [模组SDK](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/20-%E7%8E%A9%E6%B3%95%E5%BC%80%E5%8F%91/13-%E6%A8%A1%E7%BB%84SDK%E7%BC%96%E7%A8%8B/1-Mod%E5%BC%80%E5%8F%91%E7%AE%80%E4%BB%8B/1-Mod%E7%AE%80%E4%BB%8B.html?catalog=1)
## 项目创建
在开始代码编写之前,首先需要创建项目。
切换到插件标签页,点击新建插件。
![](./images/07.png)
然后我们按照团队名,插件名称来填写信息,并且勾选大厅服/游戏服。
因为我们在之前已经在Spigot插件中定义好了插件的命名空间和系统名所以我们这里按照下方截图填写方便后面直接和Java服通信。
![](./images/08.png)
创建完成后,对插件右键,打开目录。就可以看到插件的目录结构。
- behavior_packs - 行为包目录
- developer_mods - 在开服工具2.0中无用
- resource_packs - 资源包目录
- worlds - 存档在开服工具2.0中仅用来配置行为包和资源包)
在这里,我们主要需要编写的地方就是**行为包目录、资源包目录**。
- **行为包**主要用来存放客户端模组的代码、物品定义、实体定义等等。
- **资源包**主要用来存放客户端模组的美术资源,文本资源等等。
因为developer_mods在开服工具2.0中没有用途,所以我们可以打开文件夹,将截图所示内容删除。
![](./images/09.png)
完成删除后我们可以将整个testMod文件夹剪切到服务器配置中的Mod目录文件夹。
![](./images/12.png)
接下来打开装有Python插件的IDEA或者PyCharm对客户端模组进行脚本编辑。
在File->Open中复制文件路径打开这个模组文件夹。
![](./images/10.png)
然后对`testModBehavior`右键,将其标记为`Sources Root`,这样补全库才能正常工作。
![](./images/11.png)
接下来,我们可以打开`modConst.py`,在这里可以看到这个模组的一些常量。
- `ModName` 代表 模组命名空间
- `ClientSystemName` 代表 模组客户端系统名
可以回顾一下Java服插件中的命名空间和客户端系统名可以看到这它们是一一对应的。
**只有在服务器和客户端通信时使用相同命名空间和系统名,通信数据才会被成功处理。**
```python
# -*- coding: utf-8 -*-
# 整个Mod的一些绑定配置
ModVersion = "1.0.0"
ModName = "testMod"
ClientSystemName = "testModBeh"
ClientSystemClsPath = "testModScript.modClientSystem.ModClientSystem"
ServerSystemName = "testModDev"
ServerSystemClsPath = "testModScript.modServerSystem.ModServerSystem"
# 引擎事件
UiInitFinishedEvent = "UiInitFinished"
```
接下来打开`modClientSystem.py`
```python
import client.extraClientApi as clientApi
```
将文件顶部的代码修改为,方便正常使用补全库。
```python
import mod.client.extraClientApi as clientApi
```
## 代码编写
功能需求:
- 在玩家客户端UI初始化完成时向服务器发送TestEvent事件参数任意。
- 监听服务器TestServerEvent并打印信息到控制台。
会用到以下两个函数:
- [NotifyToServer](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E9%80%9A%E7%94%A8/%E4%BA%8B%E4%BB%B6.html?key=NotifyToServer&docindex=1&type=0)
- [ListenForEvent](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E9%80%9A%E7%94%A8/%E4%BA%8B%E4%BB%B6.html?key=ListenForEvent&docindex=5&type=0)
除此之外,还有更多的事件相关的接口,可以参考[官方文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E9%80%9A%E7%94%A8/%E4%BA%8B%E4%BB%B6.html?catalog=1)。
### 向服务器发送TestEvent事件
在创建项目后的模板中已经生成了监听UI初始化完成的事件我们可以直接在这个事件的回调函数中向服务器通信。
直接使用NotifyToServer函数即可。发送的数据是一个Python字典。
```python
# UI加载完成
def OnUiInitFinished(self, args):
logger.info("%s OnUiInitFinished", ModConst.ClientSystemName)
self.NotifyToServer("TestEvent", {"data": "测试数据"})
```
Python的类型会被转换成Java的类型对照表如下
| Python类型 | Java类型 |
| ---------------------------------------- | ------------------- |
| None | null |
| bool | Boolean |
| int/long-2^31到2^31-1 | Integer |
| int/long-2^63到-2^31-12^31到2^63-1 | Long |
| int/long2^63到2^64-1 | BigInteger |
| float | Double |
| str | String |
| list | List\<Object\> |
| dictkey必须为str | Map<String, Object> |
### 监听服务器TestServerEvent事件
我们可以在客户端系统初始化时监听这个事件并注册回调函数。在Destroy时注销监听。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.mUIMgr = uiMgr.UIMgr()
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), ModConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
self.ListenForEvent(ModConst.ModName, ModConst.ServerSystemName, "TestServerEvent", self, self.OnServerEvent)
def OnServerEvent(self, args):
print "OnServerEvent", args
def Destroy(self):
self.UnListenForEvent(ModConst.ModName, ModConst.ServerSystemName, "TestServerEvent", self, self.OnServerEvent)
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), ModConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
if self.mUIMgr:
self.mUIMgr.Destroy()
```
在这里我们监听的命名空间引用了ModConst中的ModName对应Spigot插件中的命名空间`testMod`。还引用了ModConst中的ServerSystemName对应Spigot插件中的系统名`testModDev`。因此这个监听函数将会正常监听来自服务器的信息。
## 部署测试
至此我们完成了客户端与服务端之间双端通信的最基础的实现。接下来将客户端模组进行部署,进入服务器测试。
找到服务器配置->游戏配置->协议服勾选testMod。进行部署。
![](./images/13.png)
随后点击启动测试,进入游戏。并输入指令/apollotest
可以看到服务器控制台正常输出
![](./images/14.png)
客户端控制台也正常输出。
![](./images/15.png)
Python命令行执行
```python
"OnServerEvent {'msg': '\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe6\x9d\xa1\xe6\x9d\xa5\xe8\x87\xaaJava\xe6\x9c\x8d\xe7\x9a\x84\xe6\xb6\x88\xe6\x81\xaf'}".decode("utf-8")
```
```python
u"OnServerEvent {'msg': '\u8fd9\u662f\u4e00\u6761\u6765\u81eaJava\u670d\u7684\u6d88\u606f'}"
```
消息经过utf8解码是我们传输的消息

View File

@@ -0,0 +1,159 @@
# 作业
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818c70c31a9c0f360dc5c0" width="800" height="600" allow="fullscreen"/>
学习了服务端与客户端之间的基本通信方法之后我们可以尝试来完成一个使用模组SDK来实现的全息字的功能。
## 要求
在玩家加入游戏之后,在玩家客户端的指定位置生成一个文字面板(全息字)。
整个通信流程应该如下图所示:
```mermaid
sequenceDiagram
客户端->>服务端: 客户端加载完毕
服务端-->>客户端: 生成文字面板的参数
客户端->>服务端: 生成文字面板,返回生成是否成功
```
> **为什么需要在客户端加载完毕的时候主动通知服务端而不是直接监听服务端的PlayerJoinEvent**
>
> 因为在PlayerJoinEvent触发的时候基岩版客户端可能还没有完全加载完毕。
>
> 在这个时候给客户端发送事件,有可能客户端模组还没有初始化完成,无法处理请求。
## 实现过程
### Spigot插件
首先新建Spigot项目操作步骤和之前一致。这里新建了一个名为TutorialHologram的项目并配置pom.xml添加SpigotMaster插件的maven依赖。
在开始编写插件之前,我们预先定义好,客户端的命名空间为`testHologram`,因此,根据[开发规范](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-%E6%89%8B%E6%9C%BA%E7%BD%91%E7%BB%9C%E6%B8%B8%E6%88%8F/%E8%AF%BE%E7%A8%8B5%EF%BC%9A%E6%8F%92%E4%BB%B6%E6%95%99%E5%AD%A6/%E7%AC%AC1%E8%8A%82%EF%BC%9A%E5%AE%98%E7%BD%91%E6%8F%92%E4%BB%B6%E8%A7%84%E8%8C%83.html?catalog=1),客户端系统名应为`testHologramBeh`,服务端系统名应为`testHologramDev`
- 客户端通知加载完毕的事件定义为`ClientLoadFinishEvent`
- 传输生成文字面板的参数的事件定义为`HologramParameterEvent`
- 返回生成结果的事件定义为`HologramGeneratedEvent`
推荐将这部分命名空间和事件名定义为常量。
> 教程为了方便将所有代码都写入主类,在实际开发过程中,不推荐将所有代码写入一个类中!!
```java
private final String NAMESPACE = "testHologram";
private final String CLIENT_SYSTEM_NAME = "testHologramBeh";
private final String SERVER_SYSTEM_NAME = "testHologramDev";
private final String CLIENT_LOAD_FINISH_EVENT = "ClientLoadFinishEvent";
private final String HOLOGRAM_PARAMETER_EVENT = "HologramParameterEvent";
private final String HOLOGRAM_GENERATED_EVENT = "HologramGeneratedEvent";
```
首先,生成一个文字面板,需要提供这个文字面板的坐标,文本的信息,我们在作业中可以简单地硬编码坐标的位置和文本的内容。例如坐标为`(0,100,0)`,内容为`这是一个文字面板`
在监听ClientLoadFinishEvent后发送文字面板参数信息。同时监听HologramGeneratedEvent输出结果。
```java
@Override
public void onEnable() {
spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_LOAD_FINISH_EVENT, (player, map) -> {
Map<String, Object> data = new HashMap<>();
data.put("x", 0);
data.put("y", 100);
data.put("z", 0);
data.put("text", "这是一个文字面板");
spigotMaster.notifyToClient(player, NAMESPACE, SERVER_SYSTEM_NAME, HOLOGRAM_PARAMETER_EVENT, data);
});
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, HOLOGRAM_GENERATED_EVENT, (player, map) -> {
boolean success = (boolean) map.get("suc");
getLogger().info("生成全息字 " + player.getName() + " " + success);
});
}
```
在HologramGeneratedEvent中我们可以监听来自客户端的事件并从中获取suc的值来判断生成是否成功可以继续拓展插件的功能。
这样我们的Spigot插件部分的代码就编写完成了可以构建后装入服务器。
### 客户端模组
和之前的操作一样新建一个插件团队名称填写test模组名填写hologram勾选游戏服和大厅服。
生成完成后打开插件文件夹,删除`developer_mods`文件夹里的内容,并将整个插件文件夹复制到部署设置的模组目录中。
#### 文字面板的生成
文字面板的API文档 [点我](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E7%89%B9%E6%95%88/%E6%96%87%E5%AD%97%E9%9D%A2%E6%9D%BF.html?catalog=1)
通过文档的查阅,我们需要先后
1. 创建文字面板
2. 设置文字面板的位置
3. 如有需要的话 返回服务器文字面板的ID
#### 常量定义
之前在Spigot服插件编写的过程中已经定义了一些事件名常量方便起见我们也需要在客户端模组的常量文件中定义相同的常量。
`hologramConst.py`文件中定义下列变量
```python
ClientLoadFinishEvent = "ClientLoadFinishEvent"
HologramParameterEvent = "HologramParameterEvent"
HologramGeneratedEvent = "HologramGeneratedEvent"
```
#### 功能实现
首先在OnUiInitFinished函数中向服务端系统发送事件。
```python
# UI加载完成
def OnUiInitFinished(self, args):
logger.info("%s OnUiInitFinished", HologramConst.ClientSystemName)
self.NotifyToServer(HologramConst.ClientLoadFinishEvent, {})
```
接下来监听来自服务端系统的`HologramParameterEvent`事件,并设置回调函数。获取坐标,创建文字面板。
部分代码如下
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.mUIMgr = uiMgr.UIMgr()
self.mTextBoardComp = clientApi.GetEngineCompFactory().CreateTextBoard(clientApi.GetLevelId())
self.ListenForEvent(HologramConst.ModName, HologramConst.ServerSystemName, HologramConst.HologramParameterEvent, self, self.OnHologramParameter)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), HologramConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
def OnHologramParameter(self, args):
x = args["x"]
y = args["y"]
z = args["z"]
text = args["text"]
boardId = self.mTextBoardComp.CreateTextBoardInWorld(text, (1, 1, 1, 1), (0.5, 0.5, 0.5, 0.1), True)
if not boardId:
self.NotifyToServer(HologramConst.HologramGeneratedEvent, {"suc": False})
return
self.mTextBoardComp.SetBoardPos(boardId, (x, y, z))
self.NotifyToServer(HologramConst.HologramGeneratedEvent, {"suc": True, "boardId": boardId})
def Destroy(self):
self.UnListenForEvent(HologramConst.ModName, HologramConst.ServerSystemName, HologramConst.HologramParameterEvent, self, self.OnHologramParameter)
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), HologramConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
if self.mUIMgr:
self.mUIMgr.Destroy()
```
OnHologramParameter函数会解析来自服务端的数据在指定xyz坐标创建文字面板如果失败返回信息中suc是False。如果成功suc为True并附带文字面板的id。
## 部署测试
前往服务器配置协议服勾选刚刚编写的testHologram模组重新部署后进入游戏进行测试。
![](./images/16.png)
进入游戏后,传送到坐标(0,100,0)附近,可以看到我们生成的文字面板。
![](./images/17.png)

View File

@@ -0,0 +1,39 @@
# 创建界面的两种方式
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818ce3c31a9c0f360dc5d0" width="800" height="600" allow="fullscreen"/>
在我的世界中国版的客户端模组开发中,主要有两种创建界面的方式。使用这两种不同的方式创建的界面效果和用途略有区别。在实际开发过程中,应该选择合适的方式来创建界面。
> 在开服工具2.0的开发中,除了在客户端模组处创建界面,还可以在服务端预定义界面并发送到客户端。[链接](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-手机网络游戏/课程10使用Spigot开服/30-Spigot服Demo详解/6-ServerFormDemo详解.html?catalog=1)
>
> 它可以使用Java代码来生成简单的一些界面并通过SpigotMaster插件将UI请求发送到客户端不需要编写任何客户端代码。
## CreateUI
CreateUI方式创建的界面是直接叠加在游戏界面之上的一种界面创建方式。
这种界面在游戏中有非常多的体现:
- 快捷栏
- 血量条
- 饥饿度条
- ···
使用它创建的界面可以设置是否属于Hud。使用Hud模式创建的界面不会影响游戏的正常操作。反之关闭Hud模式界面会屏蔽游戏输入方向、视角
上方所述的界面均是Hud界面。
## PushScreen
PushScreen方式使用堆栈来管理界面。即每次只能有一个处于栈顶的界面显示在游戏中。
这种界面在游戏中也有非常多的体现:
- 箱子界面
- 熔炉界面
- 铁砧界面
- ···
这种方式创建的界面,不会和其他界面同时显示。也会默认屏蔽游戏输入,同时支持手柄的摇杆操作。
一般在制作玩法功能不希望玩家在打开界面时进行移动或其他操作的情况下推荐使用PushScreen来创建界面。

View File

@@ -0,0 +1,87 @@
# 界面编辑器的使用
使用我的世界开发者工作台的界面编辑器功能,可以轻松地对界面进行设计。在本节课程中,将会教你如何制作一个简易的计分板界面。
要使用界面编辑器,首先应该创建一个空白附加包项目。切换到基岩版组件分类,点击新建,选择空白附加包。
作品名称随意填写,仅用于区分不同的项目,修改完成后启动编辑。
![](./images/01.png)
启动完成后,需要点击左上角的**界面**按钮,切换到界面编辑器。
随后点击左侧空间结构的新建,或者底部资源管理的新建按钮,创建一个界面文件。
![](./images/02.png)
![](./images/03.png)
文件命名,没有特殊要求,推荐使用`团队名_界面名`,方便管理。
在这里,因为我们要尝试制作一个计分板模组,所以命名为`test_scoreboard`
创建完成后,在默认窗口布局下:
- 左上角**控件结构**,即这个界面的树形展示区
- 左下角**控件库**,在这里可以使用原生的控件或自定义控件,对界面进行设计
- 中间为预览区域,可以实时展示正在编辑的界面
- 中下为资源管理,可以在需要时查找,使用资源
- 右侧为**属性**面板,在预览区域中选中某个控件,属性面板会显示其对应的属性并可以修改
![](./images/04.png)
原版计分板:
![](./images/06.png)
要制作一个计分板界面,首先我们可以对其组成进行分析,计分板可以由两个控件构成:
- 图片
- 文本
首先新建一个图片控件设置其父子锚点到右边修改名称为bottom。修改尺寸X为`最大子控件尺寸X`尺寸Y为`最大子控件尺寸Y`
![](./images/05.png)
这个控件会作为计分板底部的颜色较浅的区域,可以设置其使用贴图为原生图片`textures/ui/white_background.png`,勾选填充,并修改颜色和透明度。具体参数可以自行根据喜好调整。
![](./images/07.png)
接下来我们需要向这个名为bottom的图片控件内添加一个文本用来显示计分板的文本内容。
`尺寸X``尺寸Y`修改为适应,这样尺寸大小就会根据文字内容动态调整。
![](./images/08.png)
为了还原原版计分板,可以将`对齐`改为左,内容可以先随意填写几行文字,用来查看效果。
![](./images/09.png)
接下来开始制作计分板的标题,在`bottom`控件下,新增一个图片控件,命名为`title`
父锚点设置为上,子锚点设置为下。这样这个图片控件就会显示在`bottom`控件的上方。
此外,标题的`尺寸X`应该和文本控件一样大,所以设置`尺寸X`为最大兄弟控件尺寸X即文本控件的尺寸X。
`尺寸Y`设置为子控件尺寸Y的100%,根据标题高度动态调整大小。
![](./images/10.png)
使用贴图同样适用纯色贴图`textures/ui/white_background.png`,同时标题应该颜色稍深一点,透明度和颜色可以根据自己的感觉调整。
![](./images/11.png)
接下来,继续在`title`控件内添加一个文本控件同样使用适应尺寸X适应尺寸Y。
完成后,结构控件和效果如下图所示。
![](./images/12.png)
如果需要在界面上添加其它控件都是大同小异的。
**要更深入的了解界面编辑器的其他用法,可以参考[官方教程](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/1-%E7%95%8C%E9%9D%A2%E7%BC%96%E8%BE%91%E5%99%A8%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.html?catalog=1)。**

View File

@@ -0,0 +1,360 @@
# 界面逻辑的编写
## 注册界面
在上一节中,我们使用界面编辑器简单制作了一个计分板界面。但是它的内容目前还是固定的,想要让它动起来,就需要编写界面逻辑的代码。
在[第一节](./0-创建界面的两种方式.md)中已经介绍过创建UI有两种方式分别是CreateUI和PushScreen。根据计分板的这种界面的特性我们应该使用CreateUI方式来创建Hud界面。
使用开发者工作台创建的模板插件默认包含一个客户端的界面注册管理类,我们可以先新建一个插件,命名为`testScoreboard`,并将其剪切到服务器模组目录,删除`developer_mods`文件。
然后找到上一节编辑的附加包项目,右键打开目录。
`resource_pack`文件夹中的ui文件夹里的内容复制到`testScoreboard\resource_packs\testScoreboardResource\ui`中。这里都是我们在附加包中编辑的界面文件的相关资源。
- _ui_defs.json资源包中所使用的ui文件的定义引用到的界面都需要在这个文件里定义
- netease_editor_template_namespace.json 网易编辑器模板文件,必须保留
- test_scoreboard.json上一节编辑的计分板界面文件
![](./images/13.png)
接下来使用IDE打开这个模组文件夹观察代码。在客户端初始化的时候实例化了uiMgr.UIMgr。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.mUIMgr = uiMgr.UIMgr()
```
观察UIMgr的代码。可以发现只要我们调用Init方法并传入ClientSystem就会自动帮我们注册uiDef中的所有界面。
```python
class UIMgr(object):
def __init__(self):
super(UIMgr, self).__init__()
self.mUIDict = {}
self.mClientSystem = None
def Destroy(self):
pass
def Init(self, system):
self.mClientSystem = system
for uiKey, config in uiDef.UIData.iteritems():
self.InitSingleUI(uiKey, config)
def InitSingleUI(self, uiKey, config):
cls, screen = config["cls"], config["screen"]
extraClientApi.RegisterUI(ModName, uiKey, cls, screen)
extraParam = {}
if config.has_key("isHud"):
extraParam["isHud"] = config["isHud"]
ui = extraClientApi.CreateUI(ModName, uiKey, extraParam)
if not ui:
print "InitSingleUI %s fail" % uiKey
return
if config.has_key("layer"):
ui.GetBaseUIControl("").SetLayer(config["layer"])
self.mUIDict[uiKey] = ui
def GetUI(self, uiKey):
return self.mUIDict.get(uiKey, None)
def RemoveUI(self, uiKey):
ui = self.mUIDict.get(uiKey, None)
if ui:
del self.mUIDict[uiKey]
ui.SetRemove()
return True
return False
```
注册界面的流程是:
1. `extraClientApi.RegisterUI(ModName, uiKey, cls, screen)`
2. `extraClientApi.CreateUI(ModName, uiKey, extraParam)`
即注册了界面之后直接调用了CreateUI方法使用CreateUI的方式创建了界面。
如果我们后期需要使用PushScreen界面也可以对这里的代码进行简单的修改让它RegisterUI后直接返回。后续我们需要创建界面的时候直接调用PushScreen方法。
因此我们可以提前编辑好uiDef并且在UI初始化完成后调用Init方法即可完成对界面的注册。
UIDef中主要定义了几个参数
- cls逻辑代码类的路径
- screen界面json的命名空间和画布画布默认为main
- layer注册ui后ui所在的显示层级
- isHud是否为Hud
我们这里可以根据需要修改修改screen的定义删除layer
```python
UIDef.ScoreboardScreen : {
"cls":"testScoreboardScript.ui.test_scoreboard_screen.ScoreboardScreen",
"screen":"test_scoreboard.main",
"isHud":1
}
```
接下来在ClientSystem中UI初始化完毕的事件中调用Init方法注册界面。
```python
def OnUiInitFinished(self, args):
logger.info("%s OnUiInitFinished", ScoreboardConst.ClientSystemName)
self.mUIMgr.Init(self)
```
UI注册到此完毕如果这时进入游戏将可以看到静态的UI会被显示在游戏中。
想要让它根据需要动起来就需要编写界面逻辑也就是之前所提到的uiDef中cls对应的类。接下来将介绍界面逻辑的两种方式
- 使用模组SDK
- 使用数据绑定
## 使用模组SDK
找到`test_scoreboard_screen.py`文件可以看到它是一个继承了ScreenNode的类。
所有UI逻辑相关的代码都需要继承ScreenNode并且基于界面的操作的很多方法都在ScreenNode的成员方法中。[文档链接](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#screennode)
其中最为常用的方法便是[GetBaseUIControl](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#getbaseuicontrol)它可以用来根据路径获取BaseUIControl示例从而对界面控件进行修改。
对UI中的内容进行操作的一般步骤如下
- 定义各个控件的路径
- 通过GetBaseUIControl来获取各个控件的实例
- 根据控件的不同类型,调用[asXXX](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#getbaseuicontrol),转换为具体的实例
- 根据要求设置数据,例如修改文本、图片等
拿我们要制作的计分板UI来说我们主要需要对两个地方进行设置标题、内容。
因此我们可以先定义好两个控件的路径。
```python
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
print '==== %s ====' % 'init ScoreboardScreen'
self.mBottom = "/bottom"
self.mBottomLabel = self.mBottom + "/label"
self.mTitle = self.mBottom + "/title"
self.mTitleLabel = self.mTitle + "/label"
```
接下来可以编写一个方法,用来设置计分板的标题和内容。
要设置文本内容首先应该先通过GetBaseUIControl获取实例再asLabel获取文本的控件实例。
```python
self.GetBaseUIControl(self.mBottomLabel)
```
接下来查阅[文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E6%8E%A7%E4%BB%B6.html?catalog=1#labeluicontrol)找到LabelUIControl的方法可以看到使用SetText来设置文本内容。
完整的方法如下:
```python
def SetText(self, title, content):
self.GetBaseUIControl(self.mBottomLabel).asLabel().SetText(content)
self.GetBaseUIControl(self.mTitleLabel).asLabel().SetText(title)
```
这样就完成了文本的设置,后续如果需要在服务器上同步计分板信息给玩家,只需要客户端监听来自服务端的事件,并且每次收到事件时调用这个方法即可。
我们也可以在Create方法中调用这个函数来测试修改是否生效。
> Create方法是ScreenNode的生命周期之一它会在UI界面被创建完成的时候被调用这时可以获取控件并修改控件属性了。
>
> 其他生命周期可以参考[文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/30-UI%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html?docindex=1&type=0#%E7%95%8C%E9%9D%A2%E5%88%9B%E5%BB%BA%E6%B5%81%E7%A8%8B%E5%8F%8A%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F)
参考代码如下:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
ScreenNode = clientApi.GetScreenNodeCls()
class ScoreboardScreen(ScreenNode):
"""
Scoreboard
"""
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
print '==== %s ====' % 'init ScoreboardScreen'
self.mBottom = "/bottom"
self.mBottomLabel = self.mBottom + "/label"
self.mTitle = self.mBottom + "/title"
self.mTitleLabel = self.mTitle + "/label"
def SetText(self, title, content):
self.GetBaseUIControl(self.mBottomLabel).asLabel().SetText(content)
self.GetBaseUIControl(self.mTitleLabel).asLabel().SetText(title)
# Create函数是继承自ScreenNode会在UI创建完成后被调用
def Create(self):
print '==== %s ====' % 'ScoreboardScreen Create'
self.SetText("这是一个标题", "第一行\n第二行\n第三行\n第四行\n\n上面有一个空行\n某某某服务器")
```
## 使用数据绑定
除了使用模组SDK在数据更变时主动调用接口来设置控件数据之外还可以使用数据绑定的方式将Python代码和界面某个属性进行绑定。
数据绑定[说明文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/70-UI%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.html?catalog=1)
我们可以先将之前编写的使用模组SDK方式来修改界面的`test_scoreboard_screen.py`文件复制一份保留,然后再在原文件上做修改。
### Json文件修改
首先我们需要明确,需要绑定修改数据的控件,分别是`/bottom/label``/bottom/title/label`
打开ui文件`resource_packs/ui/test_scoreboard.json`,找到两个文本控件。
例如下方截图的是bottom控件下的label控件即路径为`/buttom/label`的控件。
![](./images/15.png)
修改字段:
```json
"bindings" : [
{
"binding_condition" : "always_when_visible",
"binding_name" : "#testScoreboard.content",
"binding_name_override" : "#testScoreboard.content"
}
],
"text" : "#testScoreboard.content"
```
根据[UI说明文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/30-UI%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html?catalog=1#label)可以查阅到text字段的值对应了label控件显示的文本因此我们可以给它创建一个绑定名为`#testScoreboard.content`并把这个值赋值给text字段。
同样的,继续修改`/bottom/title/label`控件,也给它添加一个绑定,名为`#testScoreboard.title`。最终修改后的JSON文件如下
```json
{
"main" : {
"controls" : [
{
"bottom" : {
"alpha" : 0.80,
"anchor_from" : "right_middle",
"anchor_to" : "right_middle",
"clip_direction" : "left",
"clip_ratio" : 0.0,
"color" : [ 0.1686274509803922, 0.1686274509803922, 0.1686274509803922 ],
"controls" : [
{
"label" : {
"font_type" : "default",
"layer" : 1,
"text" : "#testScoreboard.content",
"type" : "label",
"bindings" : [
{
"binding_condition" : "always_when_visible",
"binding_name" : "#testScoreboard.content",
"binding_name_override" : "#testScoreboard.content"
}
]
}
},
{
"title" : {
"alpha" : 0.90,
"anchor_from" : "top_middle",
"anchor_to" : "bottom_middle",
"clip_direction" : "left",
"clip_ratio" : 0.0,
"color" : [ 0.1372549019607843, 0.1372549019607843, 0.1372549019607843 ],
"controls" : [
{
"label" : {
"font_type" : "default",
"layer" : 1,
"text" : "#testScoreboard.title",
"text_alignment" : "center",
"type" : "label",
"bindings" : [
{
"binding_condition" : "always_when_visible",
"binding_name" : "#testScoreboard.title",
"binding_name_override" : "#testScoreboard.title"
}
]
}
}
],
"fill" : true,
"layer" : 2,
"size" : [ "100.0%sm+0.0px", "100.0%c+0.0px" ],
"texture" : "textures/ui/white_background",
"type" : "image"
}
}
],
"fill" : true,
"layer" : 1,
"size" : [ "100.0%cm+0.0px", "100.0%cm+0.0px" ],
"texture" : "textures/ui/white_background",
"type" : "image"
}
}
],
"type" : "screen"
},
"namespace" : "test_scoreboard"
}
```
### Python文件修改
我们可以直接在ScreenNode中定义两个变量分别将其和`#testScoreboard.title``#testScoreboard.content`进行绑定。
具体代码如下:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
ScreenNode = clientApi.GetScreenNodeCls()
ViewBinder = clientApi.GetViewBinderCls()
class ScoreboardScreen(ScreenNode):
"""
Scoreboard
"""
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
print '==== %s ====' % 'init ScoreboardScreen'
self.mContent = "第一行\n第二行\n第三行\n第四行\n\n上面有一个空行\n某某某服务器"
self.mTitle = "这是一个标题"
@ViewBinder.binding(ViewBinder.BF_BindString, "#testScoreboard.title")
def ReturnTitleText(self):
return self.mTitle
@ViewBinder.binding(ViewBinder.BF_BindString, "#testScoreboard.content")
def ReturnContentText(self):
return self.mContent
```
通过给方法添加`@ViewBinder.binding`注解,可以给函数设置数据绑定。其中第一个参数为数据类型,第二个参数为绑定名。要更新数据时,直接修改变量的值即可。
除此之外还可以对其他类型的数据进行绑定,可以参考文档深入了解。
## 测试
部署到服务器进行测试:
![](./images/14.png)
可以看到实现了原版计分板的效果。

View File

@@ -0,0 +1,229 @@
# 作业
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818d47c31a9c0f360dc5da" width="800" height="600" allow="fullscreen"/>
要求依赖客户端Mod制作一个简易的菜单插件。
- 进入游戏自动生成一个在Hud界面上的菜单
- 服务器可以配置每个菜单按钮:
- 索引位置
- 图片、文本
- 点击后以玩家身份执行指令
示例图:
![](./images/18.png)
## Spigot插件
Spigot插件的逻辑较为简单在玩家UI加载完成之后向玩家客户端发送提前加载好的菜单配置文件即可。
配置文件格式:
```yaml
button1:
idx: 1
# 该按钮在菜单中的位置
texture: 'textures/items/apple'
# 该按钮显示的贴图
text: '按钮1'
# 该按钮下方的提示文本
commands:
- 'say 点击了按钮1'
# 点击后执行的玩家指令
button2:
idx: 2
texture: 'textures/items/diamond_sword'
text: '按钮2'
commands:
- 'say 点击了按钮2'
```
读取配置文件的代码不过多介绍,需要参考可以下载源码进行查看。
主类中包含了与客户端通讯的关键代码,供参考:
```java
package me.zhanshi123.tutorialmenu;
import com.neteasemc.spigotmaster.SpigotMaster;
import me.zhanshi123.tutorialmenu.config.ConfigManager;
import me.zhanshi123.tutorialmenu.config.MenuButtonConfig;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
public final class TutorialMenu extends JavaPlugin {
private static TutorialMenu instance;
private SpigotMaster spigotMaster;
private final String NAMESPACE = "testMenu";
private final String SERVER_SYSTEM_NAME = "testMenuDev";
private final String CLIENT_SYSTEM_NAME = "testMenuBeh";
private final String CLIENT_UI_LOADED_EVENT = "ClientUiLoadedEvent";
private final String SERVER_MENU_EVENT = "ServerMenuEvent";
private final String CLIENT_MENU_CLICKED_EVENT = "ClientMenuClickedEvent";
public static TutorialMenu getInstance() {
return instance;
}
@Override
public void onEnable() {
instance = this;
ConfigManager.getInstance().loadConfig();
spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_UI_LOADED_EVENT, (player, map) ->
spigotMaster.notifyToClient(player, NAMESPACE, SERVER_SYSTEM_NAME, SERVER_MENU_EVENT, ConfigManager.getInstance().getClientData()));
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_MENU_CLICKED_EVENT, (player, map) -> {
int index = (int) map.get("index");
MenuButtonConfig menuButtonConfig = ConfigManager.getInstance().getMenuConfigs().get(index);
if (menuButtonConfig == null) {
getLogger().warning("玩家 " + player.getName() + " 发送了一个不正确的菜单数据");
return;
}
menuButtonConfig.dispatchCommand(player);
});
}
@Override
public void onDisable() {
}
}
```
## 客户端模组
创建一个命名空间为testMenu的插件删除`developer_mods`文件夹之后,就可以先创建一个空白附加包,开始编辑菜单界面。
### 界面编辑
对于这种有序排列的界面,我们可以直接使用布局面板来自动给按钮排版。
1. 新建一个布局面板,将其锚点都设置在左上方,并设置`尺寸X`为适应,修改排列方式为水平排布。
2. 新建一个面板,命名为`btn_tpl`作为按钮的模板。自行调整其尺寸教程中设置为60x60。
3.`btn_tpl`下新建一个按钮这就是按钮本体。自行调整其尺寸教程中设置为40x40。
4. 在空间结构中展开界面`button`,找到`button_label`,它被用来显示按钮上的文本,将它的父锚点设置到下,子锚点设到上。这样它就会整个显示在按钮图片的下方。
![](./images/16.png)
5. 最后将`btn_tpl`设置为隐藏。后面我们会把它作为模板来克隆别的按钮,添加到`stack_panel`中。
![](./images/17.png)
> 除了使用布局面板+克隆模板的方式来制作这种多按钮的界面。
>
> 还可以使用网格来实现。需要注意的是网格的内容在Create生命周期的时候有可能还没有被生成。
>
> 需要监听[GridComponentSizeChangedClientEvent](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E4%BA%8B%E4%BB%B6/UI.html#gridcomponentsizechangedclientevent)在其数量由0变为其他的时候才能对网格下的控件进行操作。
### 界面逻辑
将刚刚编辑好的文件,复制到对应的客户端资源`testMenu/resource_packs/testMenuResource/ui`处。
接下来编辑UiDef修改screen的值为刚刚编辑的json文件。
```python
UIData = {
UIDef.MenuScreen: {
"cls": "testMenuScript.ui.test_menu_screen.MenuScreen",
"screen": "test_menu.main",
"isHud": 1
}
}
```
客户端界面初始化完成后,加载`UiMgr`,并向服务器通知。
```python
def OnUiInitFinished(self, args):
logger.info("%s OnUiInitFinished", MenuConst.ClientSystemName)
self.mUIMgr.Init(self)
self.NotifyToServer("ClientUiLoadedEvent", {})
```
监听来自服务器的`ServerMenuEvent`事件,并通过`mUIMgr`获取`ScreenNode`实例,调用`SetData`方法来设置来自服务端的数据。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.mUIMgr = uiMgr.UIMgr()
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), MenuConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
self.ListenForEvent(MenuConst.ModName, MenuConst.ServerSystemName, "ServerMenuEvent", self, self.OnServerMenu)
def OnServerMenu(self, args):
print "OnServerMenu", args
if self.mUIMgr:
self.mUIMgr.GetUI(UIDef.MenuScreen).SetData(args["data"])
else:
print "UIMgr还没有加载!"
```
编写MenuScreen的`SetData`方法,通过传入的数据来克隆按钮。并给按钮添加回调。
[Clone](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html?key=Clone&docindex=2&type=0#clone)具体参数见文档。
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
from testMenuScript.menuConst import ModName, ClientSystemName
ScreenNode = clientApi.GetScreenNodeCls()
class MenuScreen(ScreenNode):
"""
Menu
"""
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mStackPanel = "/stack_panel"
self.mTemplate = "/btn_tpl"
print '==== %s ====' % 'init MenuScreen'
# Create函数是继承自ScreenNode会在UI创建完成后被调用
def Create(self):
print '==== %s ====' % 'MenuScreen Create'
def OnBtnClick(self, args):
print args["ButtonPath"]
path = args["ButtonPath"] # 路径为 /stack_panel/btn_x/button
buttonId = int(path[path.rindex("_") + 1:path.index("/button")]) # 截取路径中的x它就是按钮的索引
clientApi.GetSystem(ModName, ClientSystemName).NotifyToServer("ClientMenuClickedEvent", {"index": buttonId}) # 发送给服务器
def SetData(self, data):
data = sorted(data, key=lambda x: x["index"])
# 对按钮顺序进行排序
for btn in data:
newName = "btn_{}".format(btn["index"]) # 把按钮对应的index存在路径中后面按钮点击时根据路径判断是哪个按钮
self.Clone(self.mTemplate, self.mStackPanel, newName)
newPath = self.mStackPanel + "/" + newName
self.GetBaseUIControl(newPath).SetVisible(True) # 设置可见
newPath += "/button"
btnControl = self.GetBaseUIControl(newPath).asButton()
btnControl.AddTouchEventParams({"isSwallow": True}) # 先开启按钮回调功能
btnControl.SetButtonTouchUpCallback(self.OnBtnClick) # 再设置按钮回调函数
# 按钮的贴图有3个分别对应默认、按下、悬浮。这里三个都设置。
self.GetBaseUIControl(newPath + "/default").asImage().SetSprite(btn["texture"])
self.GetBaseUIControl(newPath + "/pressed").asImage().SetSprite(btn["texture"])
self.GetBaseUIControl(newPath + "/hover").asImage().SetSprite(btn["texture"])
self.GetBaseUIControl(newPath + "/button_label").asLabel().SetText(btn["text"]) # 设置按钮下的文本
```
## 效果展示
![](./images/19.png)
## 代码下载
Spigot插件[点我](https://g79.gdl.netease.com/TutorialMenu-Spigot.zip)
客户端模组:[点我](https://g79.gdl.netease.com/testMenu-Python.zip)

View File

@@ -0,0 +1,118 @@
# 中国版特效的使用
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818d8ec31a9c0f360dc5de" width="800" height="600" allow="fullscreen"/>
在我的世界中国版中的特效,有两种:
- 微软特效
- 中国版特效
微软特效是游戏原生支持的特效,使用`/particle`原版指令来生成。
中国版特效基于特效编辑器来制作,可以实现更多效果的特效。
中国版特效主要有两种形式,它们应用的场景各不相同
- 序列帧特效:通过在场景中绘制一张单面片的贴图,在这个贴图上不断的更换新的图片,这个过程形成完整的序列帧特效。
- 粒子特效:通过在场景中生成大量粒子图像来产生视觉效果,每个粒子都代表着效果中的单个元素,所有的粒子组合起来就形成了完整的粒子特效。
特效编辑器的使用教程,可以参考[官方文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/16-%E7%BE%8E%E6%9C%AF/9-%E7%89%B9%E6%95%88/00-%E7%89%B9%E6%95%88%E7%BC%96%E8%BE%91%E5%99%A8%E7%AE%80%E4%BB%8B.html?catalog=1)。本教程将主要介绍如何使用官方内容库中的现成的特效将其使用到开服工具2.0制作的网络游戏中。
## 素材下载和导入
开发者工作台的内容库中,包含了众多已经编辑好的特效包,可以供开发者自由使用。
本教程中,下载`代号羲和特效包`,并以该特效包为例,介绍如何在游戏中使用这些特效。
![](./images/01.png)
点击下载按钮后,我们可以新建一个空白基岩版附加包,用来导入、调试这个特效。
打开编辑器后,在左上角选择`特效`,切换到特效编辑器。
![](./images/02.png)
在编辑器打开的情况下,切换到开发者工作台的内容库,点击导入按钮,即可导入特效包。
![](./images/03.png)
弹出的对话框,全选导入即可。
![](./images/04.png)
这时在资源管理窗口中,切换到中国版特效分类,就可以看到所有我们导入的特效。
![](./images/05.png)
如果需要预览特效,可以将特效拖动到模型挂接的窗口中,然后点击播放按钮进行播放。
比如将Attack_2这个特效拖动到`head`上,可以看到它是粒子特效。点击时间轴的播放按钮,就可以看到特效在玩家模型头部被播放。
![](./images/06.png)
选中Attack_2这个特效后右侧属性窗口可以看到粒子特效的相关属性设置。
主要包括粒子的尺寸、速度、旋转、发射器的设置等等参数。
![](./images/32.png)
除此之外,资源包中还有部分序列帧特效,例如`test_xuanyun`这个特效。挂接播放后可以看到,它就是一个不断变化的图片。
属性窗口中也只有贴图相关选项,没有粒子的发射器等参数。
![](./images/31.png)
## 使用代码控制播放
我们可以首先新建一个插件,比如这里名为`testEffects`,删除其`developer_mods`的内容后复制到Mod目录。
然后对刚刚创建的空白附加包右键,打开目录
![](./images/07.png)
将资源包目录中的`effects`,`particles`,`textures`文件夹,复制到插件的资源包目录。
这三个文件夹 分别存放了`中国版特效``原版特效``材质`,如果需要完整的使用特效包内的特效,缺一不可。
![](./images/08.png)
### 粒子
接下来就是代码编写的部分模组SDK内提供了丰富的接口来控制特效的播放参考[链接](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E7%89%B9%E6%95%88/%E7%B2%92%E5%AD%90.html?catalog=1#createengineparticle)。
例如我们可以将播放特效的代码封装到一个函数内,然后另外编写监听来自服务器的事件的代码,通过服务器控制粒子特效的播放。
```python
def PlayEffect(self, effectName, pos):
particleEntityId = self.CreateEngineParticle(effectName, pos)
particleControlComp = clientApi.GetEngineCompFactory().CreateParticleControl(particleEntityId)
particleControlComp.Play()
return particleEntityId
```
例如监听来自服务端的PlayEffectEvent播放特效服务端的部分大同小异这里不再介绍。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent(EffectsConst.ModName, EffectsConst.ServerSystemName, "PlayEffectEvent", self, self.OnPlayEffect)
def OnPlayEffect(self, args):
name = args["name"]
pos = tuple(args["pos"])
particleId = self.PlayEffect(name, pos)
print "播放了特效实体id", particleId
```
### 序列帧
序列帧的播放控制代码和粒子略有不同,参考[文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E7%89%B9%E6%95%88/%E5%BA%8F%E5%88%97%E5%B8%A7.html?catalog=1)。
主要通过两个接口创建序列帧特效:
- CreateEngineSfx
- CreateEngineSfxFromEditor
推荐使用`CreateEngineSfxFromEditor`,可以按照编辑器中编辑好的参数创建序列帧。支持环状序列帧。
在完成创建后,也可以使用其他接口对序列帧特效进行控制,使用上和粒子特效大同小异。

View File

@@ -0,0 +1,121 @@
# BlockBench模型的使用
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818e09ef3bb6958baf3886" width="800" height="600" allow="fullscreen"/>
在制作游戏玩法时经常会遇到需要修改玩家模型或者实体模型的场景。我们可以使用Blockbench制作的模型配合模组SDK来轻松实现修改实体模型的功能。
## 本地玩家模型的修改
在开始之前,首先前往内容库,找到`Spigot示例Demo`下载并打开。本节课将使用官方Demo中提供的模型素材来进行演示。
接下来创建一个插件,这里命名为`testModel`复制到Mod部署目录。
打开Demo的目录找到`SpigotDemo\CustomHumanModelDemo\DemoClientMod\resource_packs\resource_pack_geyser_demo_mod`为Demo中的资源包文件夹将其中的`models``textures`复制到`testModel`的资源包文件夹中。
![](./images/10.png)
通过IDE打开这个目录可以看到目录结构中主要是复制了2个`.geo.json`后缀的模型文件和1个贴图。
接下来我们尝试,将玩家的模型替换为`player.geo.json`所存储的模型,贴图替换为`player.png`的贴图。
![](./images/11.png)
首先要替换模型我们要知道模型的identifier打开`player.geo.json`可以看到identifier字段的值为`geometry.player`,这个值就是模型的识别符,需要保证它在所有资源包中是唯一的。
![](./images/12.png)
接下来我们还需要知道模型所对应的贴图的路径。在资源包中对应路径`textures/entity/player`
![](./images/13.png)
接下来我们就可以调用在[OnLocalPlayerStopLoading](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E4%BA%8B%E4%BB%B6/%E4%B8%96%E7%95%8C.html?key=OnLocalPlayerStopLoading&docindex=3&type=0)事件触发时,为本地玩家[重建数据渲染器](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E7%8E%A9%E5%AE%B6/%E6%B8%B2%E6%9F%93.html?key=AddPlayerGeometry&docindex=1&type=0#rebuildplayerrender),修改其参数。
```python
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "OnLocalPlayerStopLoading", self, self.OnStopLoading)
def OnStopLoading(self, args):
playerId = args.get("playerId")
# 更换模型贴图
actorRenderComp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
actorRenderComp.AddPlayerGeometry('default', "geometry.player")
actorRenderComp.AddPlayerTexture('default', "textures/entity/player")
actorRenderComp.RebuildPlayerRender()
def Destroy(self):
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "OnLocalPlayerStopLoading", self, self.OnStopLoading)
```
> 在这里所调用的接口实际上是为单独的实体修改了它的客户端实体json并重建。自定义模型以及json详细参数详见 [自定义生物](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/20-玩法开发/15-自定义游戏内容/3-自定义生物/01-自定义基础生物.html)
>
> 因此只能客户端本地看到这个修改,如果需要让**所有玩家都看到**新模型,那么需要借助服务器**广播给所有客户端**,让其他客户端也为这个实体修改参数并重建数据渲染器。
>
> 同理,如果需要为不同玩家设置不同模型,也可以由服务端来控制,什么玩家显示什么模型。并在新玩家加入时给所有在线客户端广播,通知其他客户端修改新加入的玩家的模型。
进入游戏后,可以看到本地玩家的模型被修改。
![](./images/14.png)
## 本地实体模型的修改
仍继续使用官方Demo中的模型资源复制下方资源文件到testModel插件的对应资源包目录
- `SpigotDemo\CustomPigModelDemo\CustomPigModelClientMod\resource_packs\resource_pack_pig_model\textures\entity\pig` 贴图文件
- `SpigotDemo\CustomPigModelDemo\CustomPigModelClientMod\resource_packs\resource_pack_pig_model\render_controllers` 渲染控制器
- `SpigotDemo\CustomPigModelDemo\CustomPigModelClientMod\resource_packs\resource_pack_pig_model\entity` 客户端实体定义
修改的思路如下:
1. 修改客户端实体定义,添加贴图文件引用。修改渲染控制器
```json
"textures": {
"default": "textures/entity/pig/pig",
"saddled": "textures/entity/pig/pig_saddle",
// 黄猪贴图引用
"default_yellow": "textures/entity/pig/yellow_pig",
// 黄猪上鞍贴图引用
"saddled_yellow": "textures/entity/pig/yellow_pig_saddle"
},
```
```json
// 替换成自定义的猪渲染控制器,可变成黄猪
"render_controllers": [ "controller.render.pig_custom" ],
```
2. 修改渲染控制器根据molang选择贴图的使用。
> molang的解释参考[Wiki](https://zh.minecraft.wiki/w/Molang)
```json
// 自定义猪渲染器,增加黄猪皮肤
"controller.render.pig_custom": {
"arrays": {
"textures": {
"Array.skins": [ "Texture.default", "Texture.saddled" ],
"Array.skins_custom": ["Texture.default_yellow", "Texture.saddled_yellow"]
}
},
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
// 在Python脚本内注册query.mod.custom_pig节点默认值为0.0。该处表达式意思为当某只猪的实例的query自定义节点不为1时则皮肤组切换至黄猪组。
"textures": [ "query.mod.custom_pig == 0 ? Array.skins[query.is_saddled] : Array.skins_custom[query.is_saddled]" ]
}
```
3. Python脚本注册molang节点通过脚本控制molang的值来显示不同的模型或贴图。
在修改玩家模型的客户端的代码基础上做修改,框选部分为新增部分:
![](./images/15.png)
同样 如果需要指定某个实体ID为某个模型则需要通过服务端存储实体ID和对应模型之间的映射关系并广播给所有客户端对指定实体进行molang值的修改。
进入游戏生成猪,可以看到随机的猪的颜色。
![](./images/16.png)

View File

@@ -0,0 +1,201 @@
# 自定义物品制作
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818e58c31a9c0f360dc5f2" width="800" height="600" allow="fullscreen"/>
基于目前Spigot服相关接口、自定义物品流程Spigot服的自定义物品实际上是原生Java物品的换皮物品客户端Mod利用字段**java_identifier**来标识
在玩家通过Geyser转发之后利用**java_identifier**实现基岩版自定义物品和Java版物品的映射。
因此要实现自定义物品,我们不仅需要在服务端编写相关逻辑,还需要定义客户端行为包和资源包文件。
## 客户端定义物品
首先提前准备好一个服务端插件,这里命名为`testItem`。除此之外为了更加方便地编辑客户端物品我们可以另外再创建一个空白Addon用来完成物品之后复制到插件中。
编辑新创建的空白附加包,在编辑器中,首先在右上角作品菜单中,修改命名空间。教程中命名空间修改为`testitem`
![](./images/18.png)
找到资源管理的新建按钮->配置->物品。
![](./images/17.png)
接下来填写文件命名,同时它也是物品名,例如这里填写`example`。模板选择自定义武器。
**那么这个物品的标识符就是`命名空间:物品名`,即`testitem:example`。**
![](./images/19.png)
接下来我们可以给这个物品设置贴图贴图我们可以使用Spigot示例Demo中的武器贴图。
在资源管理中,点击导入,选择贴图,物品贴图。
![](./images/20.png)
找到目录`SpigotDemo\CustomItemDemo\CustomItemClientMod\resource_packs\CustomItemsMod_resource\textures\items`,导入`customitems_test_sword`,作为自定义武器的贴图。
如果你足够熟悉附加包的目录结构,也可以直接打开作品文件夹,将贴图复制到`资源包/textures/items`目录中,也可以不使用编辑器的贴图导入功能。
接下来在右侧属性窗口中,选择刚刚导入的贴图,也可以根据自己的需要,修改`游戏内名字`,它将会作为物品的默认物品名。
![](./images/21.png)
编辑完成后,点击`配套文件`中的`物品行为文件`后面的`打开文件`按钮。
![](./images/24.png)
在文件中添加`java_identifier`字段。这个字段用来表示在Java服中它所对应的实际的物品的标识符。
```json
{
"format_version": "1.10",
"minecraft:item": {
"components": {
"minecraft:max_damage": 10,
"netease:weapon": {
"attack_damage": 12,
"enchantment": 10,
"level": 3,
"speed": 5,
"type": "sword"
}
},
"description": {
"category": "Equipment",
"identifier": "testitem:example"
},
"java_identifier": "wooden_sword"
}
}
```
例如这里填写`wooden_sword`那么在Java服中木剑将尝试换皮为我们创建的自定义物品。
在编辑器的属性窗口中还包含了一些行为包组件。需要注意的是因为自定义物品本质上是Java版物品换皮这里的大多数属性都只是用来给客户端设置物品的表现的很多属性都无法生效。
### **需要注意的点**
- 字段工具挖掘速度、挖掘等级需要和Java物品对应不然会出现方块破坏速率不一致导致的卡方块现象。对应物品json中 `"netease:weapon":{ "type":"shovel", "level":0, "speed":2 }`
具体对应关系如下:
| 键 | 类型 | 默认值 | 解释 |
| ----- | ---- | ------ | ------------------------------------------------------------ |
| type | str | | 武器/工具的类型,目前支持类型有: sword剑 shovel铲 pickaxe镐 hatchet斧 hoe锄头 |
| level | int | | level为0当速度为2对应木板,否则对应金锭 level为1对应石头 level为2对应铁锭 level为3对应钻石 level大于3无法使用铁砧修复 |
| speed | int | 0 | 对采集工具生效,表示挖掘方块时的基础速度 木头2 石头4 铁6 钻石8 金12 |
使用编辑器,可以直接在`行为包组件中`进行修改,在`武器属性`中,选择对应的`类型``工具等级``挖掘基础速度`
- 同理盔甲字段json中需要和Java物品对应 `"netease:armor":{ "armor_slot":2 }` 盔甲槽位,详见[ ArmorSlotType](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/枚举值/ArmorSlotType.html)。使用编辑器,可以直接在`盔甲穿戴属性`类别中就可以选择槽位。
也有部分行为包组件不会生效,目前已知的有
````
- 基岩版自定义物品中用于物品防火的组件
```
设置物品是否防火
"netease:fire_resistant"{ "value" : true}
```
- 基岩版自定义物品中用于物品是否可做燃料的组件
```
设置物品是否可作为燃料
"netease:fuel" { "value" : true}
```
- 基岩版自定义物品中用于物品的使用间隔
```
设置物品使用间隔
"netease:cooldown" : { "duration" : 5}
```
````
完成了客户端物品的配置,我们就可以关闭编辑器。
打开附加包文件夹,将资源包和行为包目录中的部分涉及到自定义物品的内容,复制到`testItem`插件中的对应位置。
### 行为包
- netease_items_beh
![](./images/22.png)
### 资源包
- netease_items_res
- texts
- textures
![](./images/23.png)
至此,我们就完成了客户端物品的定义。
## 服务端逻辑
首先新建一个项目依赖SpigotMaster。
### 创建物品
```java
ItemStack itemStack = new ItemStack(Material.WOOD_SWORD);
itemStack = spigotMaster.setCustomItemIdentifier(itemStack, "testitem:example");
```
像正常Spigot插件一样直接实例化ItemStack来新建一个物品物品的Material为`java_identifier`字段中的值,即木剑。
然后调用spigotMaster的`setCustomItemIdentifier`方法,为这个物品设置自定义物品标识符,即`testitem:example`。
### 获取物品
```java
String customIdentifier = spigotMaster.getCustomItemIdentifier(itemStack);
```
调用spigotMaster的`getCustomItemIdentifier`方法来获取自定义物品标识符。如果是自定义物品返回标识符否则返回null。
我们可以编写测试一些测试用监听器,查看自定义物品如何生效。
```java
public class Listeners implements Listener {
private final SpigotMaster spigotMaster = TutorialItem.getInstance().getSpigotMaster();
@EventHandler
public void onJoin(PlayerJoinEvent e) {
Player player = e.getPlayer();
ItemStack itemStack = new ItemStack(Material.WOOD_SWORD);
itemStack = spigotMaster.setCustomItemIdentifier(itemStack, "testitem:example");
player.getInventory().addItem(itemStack);
}
@EventHandler
public void onInteract(PlayerInteractEvent e) {
Player player = e.getPlayer();
ItemStack itemStack = player.getInventory().getItemInMainHand();
if (itemStack == null) {
return;
}
String customIdentifier = spigotMaster.getCustomItemIdentifier(itemStack);
if (customIdentifier == null) {
player.sendMessage("你手持的物品不是自定义物品");
return;
}
player.sendMessage("你手持的物品的自定义ID为: " + customIdentifier);
}
}
```
安装插件到服务器,然后勾选`testItem`,重新部署。
进入游戏后,可以看到背包发送了一个我们的自定义物品。
![](./images/25.png)
对物品交互可以看到正常输出成功获取到了自定义物品的identifier。
![](./images/26.png)

View File

@@ -0,0 +1,161 @@
# 作业
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818ea7c31a9c0f360dc5f4" width="800" height="600" allow="fullscreen"/>
制作2个自定义物品可以长按空白处使用有不同的效果
- 物品1播放一个中国版特效
- 物品2修改玩家模型
## 客户端实现
客户端的部分将基于之前所制作的`testEffect``testModel``testItem`客户端模组来实现。制作过程见[中国版特效的配置](./0-中国版特效的配置.md)、[BlockBench模型的使用](./1-BlockBench模型的使用.md)、[自定义物品的制作](./2-自定义物品制作.md)。
其中`testEffect`模组已经实现了监听服务端的事件来播放特效的功能。
### testModel
`testModel`模组 需要让其监听服务端事件再做出修改即可。
修改的部分:
- OnLocalPlayerStopLoading 后通知服务端
- 监听服务端事件OnChangeLocalModelEvent事件触发后再更改模型
修改后的代码:
```python
class ModelClientSystem(ClientSystem):
"""
该mod的客户端类
根据服务端推送下来的数据显示通用显示界面
"""
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "OnLocalPlayerStopLoading", self, self.OnStopLoading)
self.ListenForEvent(ModelConst.ModName, ModelConst.ServerSystemName, "OnChangeLocalModelEvent", self, self.OnChangeLocalModel)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddEntityClientEvent", self, self.OnAddEntity)
def OnChangeLocalModel(self, args):
playerId = clientApi.GetLocalPlayerId()
# 更换模型贴图
actorRenderComp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
actorRenderComp.AddPlayerGeometry('default', "geometry.player")
actorRenderComp.AddPlayerTexture('default', "textures/entity/player")
actorRenderComp.RebuildPlayerRender()
def OnStopLoading(self, args):
# query节点注册时为全局属性创建的QueryComp应传入世界参数
query_comp = clientApi.GetEngineCompFactory().CreateQueryVariable(clientApi.GetLevelId())
query_comp.Register('query.mod.custom_pig', 0.0)
self.NotifyToServer("ClientStopLoadingEvent", {})
def OnAddEntity(self, args):
entity_id = args['id']
identifier = args['engineTypeStr']
if identifier != 'minecraft:pig':
return
# query节点在某个实体实例被设置是创建的QueryComp应传入实体ID参数
query_comp = clientApi.GetEngineCompFactory().CreateQueryVariable(entity_id)
# 50%概率创建一个黄猪。若需要全部玩家看到该实体都为黄猪,需在服务端做好同步处理,并广播至每个客户端。
if random.randint(0, 100) < 50:
query_comp.Set('query.mod.custom_pig', 1.0)
def Destroy(self):
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "OnLocalPlayerStopLoading", self, self.OnStopLoading)
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddEntityClientEvent", self, self.OnAddEntity)
self.UnListenForEvent(ModelConst.ModName, ModelConst.ServerSystemName, "OnChangeLocalModelEvent", self, self.OnChangeLocalModel)
```
### testItem
使用编辑器打开之前所编辑的附加包,继续添加新物品。方便起见,直接使用自定义剑的配置,进行复制。
![](./images/27.png)
复制两个新物品,识别符分别为`item1``item2`,物品名分别为`物品1``物品2`
![](./images/28.png)
因为编辑器可能会覆盖字段,对**所有**的物品,打开`物品行为文件`,添加`java_identifier`,都为`wooden_sword`
> 需要注意的是:
>
> 添加完`java_identifier`后,再次使用编辑器打开,可能会将字段覆盖。
>
> 建议编辑完成后将文件进行备份,防止字段丢失。
完成后打开文件夹将行为包和资源包对应内容复制到testItem插件中。
## 服务端实现
新建一个项目,命名为`TutorialItemDemo`并导入SpigotMaster作为依赖。
- 创建监听器,监听玩家长按空白处(鼠标右键点击)
- 获取物品基岩版标识符,根据不同的标识符执行不同的逻辑
```java
@EventHandler
public void onInteract(PlayerInteractEvent e) {
Action action = e.getAction();
if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) {
return;
}
ItemStack itemStack = e.getItem();
String identifier = spigotMaster.getCustomItemIdentifier(itemStack);
Player player = e.getPlayer();
if (identifier.equalsIgnoreCase("testitem:item1")) {
sendEffect(player);
} else if (identifier.equalsIgnoreCase("testitem:item2")) {
changeModel(player);
}
}
```
### 特效播放
实现特效播放的事件信息发送。
需要注意的是,根据之前所编写的客户端模组,命名空间为`testEffects`,系统名为`testEffectsDev`,事件为`PlayEffectEvent`
```java
private void sendEffect(Player player) {
Map<String, Object> data = new HashMap<>();
data.put("name", "effects/Attack_2.json");
Location location = player.getLocation();
data.put("pos", Arrays.asList(location.getX(), location.getY(), location.getZ()));
spigotMaster.notifyToClient(player, "testEffects", "testEffectsDev", "PlayEffectEvent", data);
}
```
### 模型更改
发送`OnChangeLocalModelEvent`事件给客户端,命名空间为`testModel`,系统名为`testModelDev`
```java
private void changeModel(Player player) {
spigotMaster.notifyToClient(player, "testModel", "testModelDev", "OnChangeLocalModelEvent", new HashMap<>());
}
```
## 效果测试
进入游戏后打开创造物品栏拿出物品1、物品2。分别手持物品右键。
- 物品1播放特效
![](./images/29.png)
- 物品2切换模型
![](./images/30.png)
## 参考插件下载
Java插件下载 [点我](https://g79.gdl.netease.com/TutorialItemDemo.zip)
Python模组下载 [点我](https://g79.gdl.netease.com/TutorialItemDemo-Python.zip)

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