上手 Minestom: 新时代 Minecraft 服务端实现

上手 Minestom: 新时代 Minecraft 服务端实现

笔者观望 Minestom 已经有一段时间了. 以前他的线程模型还不够成熟, 曾经联系 @TheMode 想帮他翻译那块的文档结果鸽了.
正好, 今天摸鱼的时候看了一眼 Minestom 官方, 发觉现在或许是时候上手尝试一下了.

简中圈子里吹 Minestom 的很多, 然而真正上手 / 普及 Minestom 开发的很少. 官方文档已经足够详尽, 因此本篇博文只作引路贴, 希望对 Minestom 感兴趣的你自己上手尝试.

配置环境

Minestom 主要托管在 JitPack 上.

build.gradle:

1
2
3
4
5
6
7
8
9
10
repositories {
maven {
name 'JitPack'
url 'https://jitpack.io'
}
}

dependencies {
implementation 'com.github.Minestom:Minestom:VERSION' // 版本号自己去 JitPack 上面找
}

静静等待依赖图下载完成, 下载的过程中, 我们不难发现一些老熟人.

启动

Minestom 启动实现很简单, 只需要两行就搞定了.

1
2
3
4
public static void main(String[] args) {
var mc = MinecraftServer.init();
mc.start("0.0.0.0", 25565);
}

一个空的 Minestom 实现在一秒钟内就能初始化完毕.
打开 Minecraft, 添加一个 localhost 到服务器列表中就能看到你的新 Minestom 服务器.

Minestom in ServerList

注意: Minestom 语境下的 实例实现 可能和你想象中的不一样
例如: 实现/Implementation 是基于 Minestom 开发的服务端软件, 而 实例/Instance 不只指对象.

你很快就会发现你卡在登入中. 先别急着去 Issues 找骂, 看看日志怎么说:

Logs

You need to specify a spawning instance in the PlayerLoginEvent 嗯… 有意思.
如果想加入我们刚刚创建的新鲜 Minestom 实现, 我们首先要设置玩家加入的 实例. 那么, 实例是什么?

实例

Instances are what replace “worlds” from Minecraft vanilla, those are lightweight and should offer similar properties. There are multiple instances implementation, currently InstanceContainer and SharedInstance (both are explained below)

The Minestom Wiki

简单的说, 在 Minestom 的世界里, 实例 和我们先前在 Bukkit / Forge 上开发时的 世界 是相同的概念. 不同的是, 比起世界来说, 一个 实例 通常更加轻量一些.

那么怎么创建实例呢? 你可能会发现你刚刚得到的 MinecraftServer 对象除了能监听端口什么都不会干, 这是因为 Minestom 的大部分功能…

Method Complements

都被 MinecraftServer 的静态方法包装起来了. 我觉得这样做的意图可能是模拟其他 JVM 语言上 “object“ 的做法, object 类型的 “类” 默认就是单例, 因此这种类的静态方法不复存, 所有的方法和字段实际上都直接指向那个单例.
虽然在 Java 的语境下这样的做法难免令人感觉奇怪, 但是这毕竟不是重点.

我们拿到 InstanceManager, 然后创建一个新的实例:

1
2
var manager = MinecraftServer.getInstanceManager();
var instanceContainer = manager.createInstanceContainer();

噢! 不要忘记设置默认的 世界生成器, 不然你会一直掉下去虚空的.

1
2
instanceContainer.setGenerator(unit ->
unit.modifier().fillHeight(0,1, Block.GRASS_BLOCK));

然后要注册一个事件监听器, 用于告诉 Minestom 我们想让玩家出生在什么地方.

1
2
3
MinecraftServer.getGlobalEventHandler().addListener(PlayerLoginEvent.class, evt ->{
evt.setSpawningInstance(instanceContainer);
});

但是! 虽然现在已经可以进入服务器了, 我们会出生在 (0,0,0), 然后无尽掉虚空.
所以, 还需要额外补几行防止这种情况.

1
2
3
MinecraftServer.getGlobalEventHandler().addListener(PlayerSpawnEvent.class, evt ->{
evt.getPlayer().teleport(new Pos(0,3,0));
});

启动服务器, 进去将会发现一大片草方块.

或者, 你也可以稍微更换一些参数…

莫名其妙的压迫感

Minestom 生成世界的速度很快(可能是懒加载而已), 你几乎感受不到平时在 Notchian 服上最常见的世界生成卡顿. (可能是因为空 Minestom 处理的数据比较少, Minestom 就算直接加载 Minecraft 地图速度也是远超 Notchian.)
虽然 Minestom 支持直接加载 Anvil 格式的存档, 但是官方文档没有提到要怎么做.

加载 Anvil 格式的地图

注: 以下使用的主要 API 被官方标记为不稳定

不难发现, createInstanceContainer 其实有支持传入 IChunkLoader 的重载方法. 只需要搜索片刻…

你就能找到 AnvilLoader.

1
2
3
4
5
    var manager = MinecraftServer.getInstanceManager();
- var instanceContainer = manager.createInstanceContainer();
+ var instanceContainer = manager.createInstanceContainer(new AnvilLoader("/path/to/A New World"));

instanceContainer.setGenerator(unit ->

只需要这样, 就可以加载你的 Minecraft 地图了.

半秒以内就齐刷刷闪出来了!!

聊天与命令

Minestom 似乎内置一个简单的聊天功能实现 (连聊天格式都和原版一样), 处理信息的方法应该和在 Bukkit 上的相差不大, 只不过 Minestom 大量运用了 Kyori 的 Adventure API我有点反胃. 写代码时最好留个心眼在返回值上.

`getUsername` 才是正解

所以比较想提一嘴的是命令, 毕竟其他教程也有自古以来从命令入手写功能的习俗.

注册一个新的命令很简单:

1
2
3
4
5
var commandNew = new Command("new");
commandNew.setDefaultExecutor((sender, context)->{
// your business logic...
});
MinecraftServer.getCommandManager().register(commandNew);

接着你就可以在你的实现里用 /new 了, 正如你想象的那样运行. 这个命令框架看起来并不新奇, 笔者甚至觉得有些奇怪.

不过, 上面给的例子只是为了你三行快速上手, 官方推崇的写法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
package demo.commands;

import net.minestom.server.command.builder.Command;

public class TestCommand extends Command {

public TestCommand() {
super("my-command", "hey");
// "my-command" 是这个命令的主要名字.
// "hey" 是命令的别名, 使用 /hey 和 /my-command 是一样的.
}
}

之后一样的套路: MinecraftServer.getCommandManager().register(new TestCommand())
接着是, 有参数的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package demo.commands;

import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentType;

public class TestCommand extends Command {

public TestCommand() {
super("command", "alias");

// 如果没有匹配到其他 Executor 就回落到这上面.
setDefaultExecutor((sender, context) -> {
sender.sendMessage("You executed the command");
});

// 全部的默认参数类型都在 ArgumentType 类里
// 这些静态工厂的参数是一些 `标识符`, 用于让程序分辨参数 (并且在 Minestom 内部用于创建节点)
var numberArgument = ArgumentType.Integer("my-number");

// 最后, 定义指令的 语法. (Syntax)
addSyntax((sender, context) -> {
final int number = context.get(numberArgument);
sender.sendMessage("You typed the number " + number);
}, numberArgument, ...more);

}
}

在埋头苦读上方源码之前, 不如看看官方文档是怎么解释的.

All auto-completable commands should extend Command, each command is composed of zero or multiple syntaxes, and each syntax is composed of arguments

If you find it confusing, here are a few examples:

/health 一条指令
/health set 50; 一条指令和他的语法
set 一小段字面量型的参数
~ ~ ~坐标参数

一条命令由零个或多个 语法 构成, 每个 语法 又由一个或多个参数构成. 如果感到无法理解, 不如这样想:

  • 所谓的 语法 就是命令的一个基本样子.
    比如: /effect xxx give xxx 是一条语法, 而 /effect xxx give xxx 30 24 因为后面多了两个参数就是一个新的语法.
  • 所以语法就规定了应该有哪些参数, 以及它们对应的类型.

上面命令补全回调触发的效果

更多内容, 请转向 官方文档 / Minestom Wiki

一些别的

虽然它还是高度实验性的服务端, 但是它很有潜力一举代替 Spigot 成为支撑 RPG, 小游戏服务器的主流服务端, 这也说明这真的不怪Java, 别再说什么 C++ 重写性能翻3000%了 因此, 现在开始学习如何使用是完全可取的, 因为本文所述的, 官方 Wiki 中包含的, 以及本身 API 架构不太可能再发生巨大变更.

Minestom 不仅提供了一个更加模块化的 Minecraft 服务端事件, 而且也兼顾了性能和 API 的良好设计, 开放程度远超 Spigot. 但是在使用 Minestom 开发你的实现之前, 你要花更多精力在维持好程序的良好架构上, 不然就会 go die.
有人今天用minestom写东西写的一团糟我不说是谁 所以, 我觉得如果不是很必要, 可以使用 Minestom 的 扩展(插件) API 和 高度实验性的脚本 API.
这样做或许更加有利于 Minestom 的生态发展, 而最坏的情况就是大家都喜欢自己 hold 一个 Minestom, 谁也不服谁, 就好像那帮 Mod Loader 一样.
但是 Mod Loader 也各自多多少少有一些 Mod, 要是世界上光有 Mod Loader 没有 Mod 就真的成灾难了…

End

感谢你的观看, 欢迎在评论区留言.

上手 Minestom: 新时代 Minecraft 服务端实现

https://blog.0w0.ing/2022/12/19/Getting-Started-With-Minestom/

作者

iceBear67

发布于

2022-12-19

更新于

2022-12-20

许可协议

评论