人间工作P 人间工作P
主站 (opens new window)
首页
  • Bukkit开发教程
投喂
关于
  • 分类
  • 标签
  • 时间线
  • 友情链接

人间工作P

我每天都好困… 最近在学习和进行 VOCALOID 创作
主站 (opens new window)
首页
  • Bukkit开发教程
投喂
关于
  • 分类
  • 标签
  • 时间线
  • 友情链接
  • 基础教程

    • 入门指南
      • Bukkit 是什么?
      • 准备环境
      • 新建项目
      • 配置依赖
      • 新建主类
      • 添加插件信息
      • 配置替换版本号
      • 构建插件
      • 正式开始了解Bukkit
      • 命令系统
      • 事件系统
      • 定时器系统
      • 配置系统
  • 高级教程

  • bukkit
  • 基础教程
MrXiaoM
2024-11-15
目录

入门指南

我们假设你是什么都不会的新手,手把手教你入门

完全编程新手的 Bukkit 插件开发入门教程,按照以下步骤操作,编译你的第一个插件吧!

只教开发,不教开服,本教程需要有开服技术基础,最起码要会编辑配置文件,对服务端目录熟悉。
NitWikit 笨蛋 MC 开服教程 (opens new window)

# Bukkit 是什么?

Bukkit (CraftBukkit) 是很早之前出现的一套 Minecraft Java Edition 服务端插件标准。现在几乎已经没有人使用纯血 Bukkit 了,Bukkit 已经成为了 SpigotMC 项目的一部分。现在我们更多使用的是 Spigot、Paper 等 Bukkit 衍生接口,我们依然叫它 Bukkit 插件只是因为它依然位于底层,并且我们已经习惯这么叫它了。

# 准备环境

先安装以下环境所需软件

  • (用于测试) Minecraft 客户端 (这里使用 1.20.4 来作为示例)
  • (用于测试) Spigot (opens new window) 或 Paper (opens new window) 服务端预构建版本
  • JDK (按你的游戏版本决定)
    • 1.16 及以前使用 Java 8
    • 1.17 到 1.20.4 用 Java 17
    • 1.20.5 及以上用 Java 21
  • IntellIJ IDEA Community Edition (opens new window)

安装并打开 IDEA 后,在主页点击 Plugins,在 Marketplace 搜索 中文语言包,安装第一个,安装完成后点击 Restart IDE。

然后参考这篇帖子 (opens new window)的教程,解决之后可能会出现的 IDEA 控制台乱码。

# 新建项目

点击新建项目,如图所示

01

org.example 是默认的示例组ID,仅用于教学,请勿用于实际,否则早晚出问题。

所谓「组ID」是用来辨别程序作者的,每个作者都必须要有一个唯一的组ID。为了唯一性,我们约定以下规则:

  • 如果你有域名,比如我的域名是 mrxiaom.top。那么将域名按.分隔,倒过来写就是组ID了。比如 top.mrxiaom。
  • 如果你没有域名,可以用邮箱,如果用邮箱,建议加个前缀 email。比如我的邮箱是 mrxiaom@qq.com,一样是倒过来写,那么组ID就是 com.qq.mrxiaom 或者 email.com.qq.mrxiaom。
  • 如果你有 Github 账号,可以使用 io.github.自己的全小写用户名 作为组ID,比如 io.github.mrxiaom,Gitee 同理,io.gitee.mrxiaom。

    并不是所有网站都可以这么做,可以用 io.github 和 io.gitee 是因为,这两家开源平台都有向用户提供网页托管服务,你只要有账号,xxx.github.io 之类的二级域名就是归你的,那么就可以适用于「你有域名」这种情况。

  • (不推荐) 如果上面几个你都不想用,那么请自由发挥,起得尽可能特殊,没人会跟你争就行。比如 kira.uwu.plugin。

如果你的组ID跟别人的组ID冲突,两个人的插件加到同一个服务器的时候可能会出问题。所以,一定要起一个能够确保唯一性的组ID。

如果你未来想将代码开源并发布到 Maven 中心仓库,请尽可能地使用域名作为组ID。

# 配置依赖

创建项目后,将来到这个界面

02

先打开 build.gradle.kts 待命,点击左侧的「构建」标签页,等待同步完成
03

然后在 repositories 下面添加开发所需仓库,添加完成后代码如下

repositories {
    mavenCentral()
    maven("https://repo.codemc.io/repository/maven-public/")
    maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
    maven("https://repo.rosewooddev.io/repository/public/")
}
1
2
3
4
5
6
  • codemc 提供一部分依赖插件
  • spigotmc 提供 spigot 接口
  • rosewooddev 提供 NMS 依赖,在之后的章节中将会详细说明 NMS 的用途。

添加完成后,我们开始添加依赖。

在 dependencies 下面添加开发所需依赖,添加完成后代码如下

dependencies {
    compileOnly("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT")
}
1
2
3

1.20.4 需要改成你自己的服务端版本。后面的 -R0.1 是固定的,代表 Bukkit 版本 (在MC高版本中,这个版本号从不更新)。后面的 -SNAPSHOT 是快照版本固定格式。
原本 dependencies 里面那两句 testImplementation 可以删除,用途不大。

点击「构建」->「同步」下面的「重新加载Gradle项目」
(为了方便,之后我会简单地用刷新项目指代这个操作),等待依赖下载完成。

04

# 新建主类

在「项目」下面依次展开 src、main,在 java 文件夹上右键,点击「新建」->「软件包」

05

在弹出的输入框填入包名。包名由 组ID 和 软件名(全小写) 组成,用 . 连接。
这里使用 org.example.exampleplugin,请改成你自己的包名。

06

紧接着,我们在刚刚新建的软件包里面新建一个主类。
先右键,点击「新建」->「Java类」,在弹出的输入框填入类名,主类名一般是插件名或者 Main、PluginMain、BukkitMain 等等,看个人喜好。
这里使用插件名 ExamplePlugin 作为主类名,新建好后如下所示。

07

在左花括号{前面加 extends JavaPlugin,将鼠标悬停到 红色字(报错) 上面,点击导入类。

08

然后在花括号之间输入以下代码

    @Override
    public void onEnable() {
        getLogger().info("你好,Minecraft!");
    }
1
2
3
4

如下所示

09

# 添加插件信息

接下来我们要让服务端能够识别并加载这个插件。依然是在「项目」下面依次展开 src、main,在 resources 文件夹上右键,点击「新建」->「文件」,输入文件名称 plugin.yml,在新建的文件中输入以下代码(自行替换代码里面的中文为相关信息)

name: 插件名
version: 版本号
main: 主类路径,先空着,后面会说怎么填
authors: [ 作者名字 ]
api-version: 插件接口版本,只要填到大版本就行了,最低填写1.13,要兼容1.13以上的服务端必须要填写这一项
1
2
3
4
5

如下所示

10

主类路径可以通过右键主类,点击「复制路径/引用…」->「复制引用」来获取。

11

# 配置替换版本号

如果你想要 build.gradle.kts 里的 version 跟 plugin.yml 里的 version 保持一致,那么请先将 plugin.yml 里 version 的值改成 ${version},如下所示

version: ${version}
1

然后在 build.gradle.kts 末尾添加以下代码即可

tasks {
    processResources {
        duplicatesStrategy = DuplicatesStrategy.INCLUDE
        from(sourceSets.main.get().resources.srcDirs) {
            expand(mapOf("version" to project.version))
            include("plugin.yml")
        }
    }
}
1
2
3
4
5
6
7
8
9

# 构建插件

激动人心的时候到了,你几乎做完所有准备工作了,

在右侧点击「Gradle」,依次展开 Tasks、build,双击图中选中的 build 开始构建

12

左侧转到「运行」标签页,出现「成功」、「BUILD SUCCESSFUL in XX」就代表成功了。

13

构建出来的插件 jar 会出现在 build/libs 文件夹里,复制到你的服务器里测试吧!

14

15

插件启用后,如期在日志中输出了「你好,Minecraft!」

# 正式开始了解Bukkit

你成功地在 Minecraft 服务器上运行了你的第一个插件,小打小闹结束了。接下来,我们基于你刚刚配置好的项目,正式开始插件开发。

我们需要知道,Bukkit能干什么。翻阅 Bukkit 的 Javadoc (opens new window) (俗称 BukkitAPI) 可以知晓各个类的注释。不过全都是英文的,读起来想必非常枯燥,特别是对于没有 Java 基础的人来说,更是没有兴趣读下去。

简单总结一下,插件可以抽象成以下几种能力:

  • 监听游戏事件,作出自定义行为操作或者修改事件结果 (修改参数或者取消执行),
  • 通过命令主动执行操作。
  • 通过定时任务执行操作。
  • 对数据 (包括配置和插件数据等等) 的增删改查,不管是存到插件目录还是数据库。

你要做的,就是如何活用这几种能力,来实现你想要的具体功能。

接下来该轮到你发挥了,

提示

如果你从未接触过编程,查阅以下几条规则来帮你快速写出代码。

点击查看

我知道这很长,请认真读完。

学习一门新编程语言的宗旨是,先了解基本语法结构,遇到不会的东西就去网上查,去问人。代码抄得多了你就能够逐渐了解为什么要这么写了。
尽量少问 GPT,AI 或许能很快地给出答案,但答案的正确率不一定比人高,优先寻找人类编写的帖子。

最好加入一个技术交流群。只要不是太过于低级的、网上能查到的问题,程序员们通常乐于回复你的问题。

你作为初学者,我并不要求你可以做到理解面向对象、把方法玩出花来、使用泛型等等,这对你来说可能还太早了。

有时候你可能按照教程输入代码会报错,鼠标移到爆红的代码上面,如果有显示「导入类」就点击它。如果显示了多个结果,尽可能选择位于 org.bukkit、java.util 包的类。

在 Java 中,以 // 开头的是注释 (字体会显示为灰色),注释不参与代码编译,仅仅是给人阅读的一串说明,之后的代码会写一些注释以方便读者理解。

在 Java,代码要写在方法里,而监听游戏事件最好的方法是新建方法,需要监听事件的时候,直接抄下面的代码就好了。

    @EventHandler
    public void on(事件类型 e) {
        // 这里花括号包裹的区域叫作「方法体」
        // 在方法体里面,写事件触发之后执行的代码
    }
1
2
3
4
5

在 Java,我们用.来进入下一级,注意到上面代码中的 e 了吗?那就是触发事件时的数据(这样描述便于理解,但并不准确),
把它想象成一个「房间」,我们可以在它后面加一个.并加上门牌号来打开特定的「门」,进入它后面的房间。
比如 玩家进入服务器 这个事件,e.getPlayer() 的大致意思就是 事件数据.获取玩家()。
同理,e.getPlayer().getName() 的大致意思就是 事件数据.获取玩家().获取名字()。

在你输入 . 的那一刻,IDEA 会弹出一个跟输入法一样的「智能提示」框,它会告诉你,这个「房间」里面有什么「门」可以进。

在一个方法体里面,总体上来说,代码是自上而下,自左向右执行的。有一些特殊情况会从右往左、或者有优先级地执行,大多数情况下是自左向右执行的。每条完整语句的后面都需要有一个英文分号;结尾。

你肯定有注意到这里有括号,事物.方法(),这对括号就是用来执行相应的方法的,方法你可以理解为机器上的一个按钮,执行方法就会把这个按钮给按下去,产生相应的效果。
而这个按钮后面接的是什么东西,不需要你去考虑,你只要知道它能产生什么效果就可以了。

有一些方法的括号里面可以填写参数,用来影响产生的效果。比如 玩家在物品栏移动物品 这个事件,e.setCancelled(true); 的大致意思就是 事件数据.设置是否取消事件(是),执行这个方法,会使得玩家无法在物品栏移动物品。这也是做菜单的时候,防止玩家拿走菜单图标的最基本方法。

还有一些方法有返回值,就比如 e.getPlayer(),它返回的是事件的玩家数据,所以你可以再在它后面加 .getName() 获取玩家的名称。

Java 语法是区分大小写的,大小写不同的两段代码,含义完全不同。
这里顺便讲讲变量,变量可以把 我们之前比喻的「门」 的门牌号给记住,让你的代码更简洁一点, 这样就不用每次想要获取玩家数据的时候,都从事件数据出发了。如下所示,新建变量的格式是:

类型 变量名 = 值;
1

简化 e.getPlayer().getName() 和 e.getPlayer().getLevel() 的示例如下:

Player player = e.getPlayer();
String name = player.getName();
int level = player.getLevel();
1
2
3

此外,还有 if (条件) { 满足条件执行操作 } else { 不满足条件执行操作 }、return;(结束执行) 等等初学阶段比较常用的语法,不多赘述。

Java 的门道很多,本教程只能引你入门,教你所有的语法就太冗长了。更多的语法规则和用法,需要你之后去寻找通用的 Java 教程去补齐。

# 命令系统

Bukkit 的命令添加起来是很复杂的。由于这篇文章是入门教程,所以命令系统部分就只给出最简单的写法。命令系统详细教程之后等我有空可能会单独开一篇文章来写。

首先我们到 plugin.yml 结尾添加以下内容,以注册一个命令

commands:
  命令名1:
    description: '命令描述'
    # 可不写 aliases
    aliases: [ 命令别名 ]
  命令名2:
    description: '命令描述'
  # ...更多命令以此类推
1
2
3
4
5
6
7
8

这里我们添加一个 example 命令,你可以根据自己喜好起名。

16

然后在插件主类添加以下代码

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        // 将代码写在这里
        // 返回 true 就代表命令被处理了,不会出现在你预期之外的提示信息。
        // 一般来说返回 true 就行了。
        return true;
    }
1
2
3
4
5
6
7

然后在 onCommand 方法里面添加一些简单的代码

// 在命令执行者的聊天栏,输出一条提示消息
sender.sendMessage("uwu");
1
2

如下所示。

17

我就不放实现效果啦,用之前的方法编译插件,然后在游戏里执行命令 /example 看看效果吧!

你可能会注意到,onCommand 在这里只用到了 sender,还有好几个参数没用到呢。

  • sender 是命令执行者,它可以被强制转换为 Player(玩家),但不要直接强转,否则控制台执行的时候必报错。
  • command 是主命令的信息,也就是我们在 plugin.yml 填的信息。
  • label 一般不使用,是 command.getLabel() 的值,但我们一般判断 command 里面的 getName()
  • args 的使用频率会很高,它是命令执行者输入的命令参数,按空格分隔成数组。

接下来我会讲解 args 的用法。

正如上方所说,args 是命令执行者(控制台/玩家)输入的命令参数,它的类型是 String[],也就是字符串数组,
你可以使用 args.length 获取它的长度,使用 args[下标] 来获取具体的值。

光讲概念可能有点抽象,接下来我讲一个实际案例。

玩家输入了命令 /example uwu owo awa,后面的 uwu owo awa 就是命令参数,它按空格分割后变成字符串数组 args 传进来。

  • args 的长度是 3
  • args[0] 是 uwu
  • args[1] 是 owo
  • args[2] 是 awa

在 Java 语言中,数组的下标从 0 开始,不能大于或等于它的长度,也不能小于0。如果你传入了不在合法范围内的下标,那么程序就会报错。

所以我们需要使用 if else 判断语句,提前判断 args 的长度,避免程序出现报错。

下面我们用一个简单的示例,让玩家通过执行 /example get [数量] 命令获得钻石,不输入数量时数量默认为 1。

// 如果有一个或以上参数
if (args.length >= 1) {
    // 如果第一个参数是 get
    if (args[0].equalsIgnoreCase("get")) {
        // 在 java 中,使用 instanceof 来判断变量类型,使用感叹号来做 逻辑取反 运算。
        // 所以这里,sender instanceof Player 的意思是,「sender的类型是否为玩家」。
        // 在前面加感叹号来取反,就变成了「sender的类型是否不为玩家」
        if (!(sender instanceof Player)) {
            sender.sendMessage("只有玩家才能执行该命令");
            // 通过 return 中止执行,不执行下面的代码
            return true;
        }
        // 前面做了判断,这里就可以放心将 sender 强制转换为玩家类型了
        Player player = (Player) sender;
        int count; // 定义整数变量 count
        // 如果有两个或以上参数
        if (args.length >= 2) {
            // 将第二个参数转换为整数,储存到 count 变量
            // 注意,由于本文章仅供入门,这里没做异常处理,
            // 如果玩家输入的不是一个整数,会出现报错。
            count = Integer.parseInt(args[1]);
            // 如果玩家输入的数量小于等于0,或者大于64,提醒玩家,中止执行
            if (count <= 0 || count > 64) {
                player.sendMessage("你输入的数量需要在1-64范围内");
                return true;
            }
        } else { // 如果没有两个或以上参数,count 设为 1
            count = 1;
        }
        // 获取玩家所在世界
        World world = player.getWorld();
        // 获取玩家当前坐标
        Location location = player.getLocation();
        // 新建一个物品,物品类型为钻石
        ItemStack item = new ItemStack(Material.DIAMOND);
        // 设置物品数量,不能超过最大堆叠数量
        item.setAmount(count);
        // 在指定坐标(刚刚获取的玩家坐标)掉落刚刚新建的物品
        // dropItem 只是其中一种间接给予玩家物品的方法
        // 除了这种方法以外,还可以使用
        // player.getInventory().addItem(item);
        // 直接给予玩家物品
        world.dropItem(location, item);
        // 提示玩家
        player.sendMessage("你获得了" + count + "个钻石");
        // 中止执行,以免往下执行了帮助命令提示
        return true;
    }
}
// 以上条件均不满足,还没有中止执行时提示帮助命令
sender.sendMessage("帮助命令: /example get [数量]");
return true;
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 事件系统

未完成

# 定时器系统

未完成

# 配置系统

未完成

编辑 (opens new window)
#编程#Gradle#Java
上次更新: 2025/04/19, 01:53:16
NMS调用

NMS调用→

最近更新
01
记一次从 Sonatype OSSRH 迁移到 Maven Central Publishing Portal
04-24
02
构建 Paper 衍生服务端(Purpur、Folia 等)时使用代理避免拉取失败
02-24
03
Gradle 杂交不同编译目标(Java)的模块到一个jar
01-29
更多文章>
Theme fork from Vdoing | Copyright © 2018-2025 人间工作P | 友情链接
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式