Bukkit插件教程篇之多线程

开始

虽然Java里也有多线程,但是Bukkit为我们写好的多线程更好用,我们可以直接使用BukkitRunnable来实现多线程!
他与Java提供的多线程有一点不同,BukkitRunnable与游戏时钟紧密相关。如果你的多线程不需要做到与游戏时间同步,那么你完全可以不使用BukkitRunnable!
这里先介绍一下:Minecraft中的时间用ticks计时,20ticks=1秒。

**↑不来一首BGM吗?**

本章要求

  • 学会使用BukkitRunnable

BukkitRunnable介绍

BukkitRunnable是Bukkit为开发者写的一套便于使用的多线程模板,查阅doc即可知道,这个类里有九个方法,其中一个是run,也就是开始运行这个多线程。但是我们并不推荐直接使用.run来启动这个线程。

BukkitRunnable方法介绍

其实这个有能力的同学可以自行翻阅doc查看,不过我这里还是介绍一下吧。

方法 参数 作用 返回值
cancel 停止该线程
getTaskId 获取该线程的ID int
runTask Plugin 在主线程运行一个线程实例 BukkitTask
runTaskAsynchronously Plugin 异步运行一个线程实例 BukkitTask
runTaskLater Plugin plugin, long delay delay个ticks后在主线程运行一个线程实例 BukkitTask
runTaskLaterAsynchronously Plugin plugin, long delay delay个ticks后异步运行一个线程实例 BukkitTask
runTaskTimer Plugin plugin, long delay, long period delay个ticks后每隔period个ticks在主线程运行一个线程实例 BukkitTask
runTaskTimerAsynchronously Plugin plugin, long delay, long period delay个ticks后每隔period个ticks异步运行一个线程实例 BukkitTask
run 直接运行run()方法

ticks是刻的意思,这里指1游戏刻。上面介绍过:在Minecraft中1游戏刻=0.05秒。也就是说20ticks = 1秒。
在主线程运行的意思是:服务器将停止其他工作,只运行你这个线程的代码直到结束。
异步运行的意思是:你的这个线程实例不会堵塞服务器主线程,服务器上发生的一切事情都与你的线程互不相关。一般情况下更推荐使用这个,一定程度上可以减轻插件对于服务器的卡顿。
但是有时候一些操作必须在bukkit主线程运行,比如说Bukkit.dispatchCommand(sender, commandLine);(以sender的身份执行一条命令)
此时你不能用runTaskAsynchronously,更别想着用Java的Thread或Runnable!否则服务端会抛出错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[19:44:07] [Thread-52/INFO]: An unknown error occurred while attempting to perform this command
[19:44:07] [Thread-52/WARN]: Unknown CommandBlock failed to handle command
java.lang.IllegalStateException: Asynchronous entity add!
at org.spigotmc.AsyncCatcher.catchOp(AsyncCatcher.java:14) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.World.addEntity(World.java:1017) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.WorldServer.addEntity(WorldServer.java:1112) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.World.addEntity(World.java:1013) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.EntityHuman.a(EntityHuman.java:566) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.EntityHuman.a(EntityHuman.java:551) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.EntityHuman.drop(EntityHuman.java:490) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at net.minecraft.server.v1_12_R1.CommandGive.execute(SourceFile:75) ~[spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper.dispatchVanillaCommand(VanillaCommandWrapper.java:109) [spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper.execute(VanillaCommandWrapper.java:38) [spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at org.bukkit.command.SimpleCommandMap.dispatch(SimpleCommandMap.java:141) [spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at org.bukkit.craftbukkit.v1_12_R1.CraftServer.dispatchCommand(CraftServer.java:648) [spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at org.bukkit.Bukkit.dispatchCommand(Bukkit.java:574) [spigot-1.12.2.jar:git-Spigot-b66ad9e-e1fb9cb]
at cn.viosin.roll.RollThread.end(RollThread.java:102) [Roll.jar:?]
at cn.viosin.roll.RollThread.run(RollThread.java:56) [Roll.jar:?]

例子:自动公告

如果我想让我的插件能够自动播放公告,并且是每隔一段时间广播一次,那么使用BukkitRunnable就是一个明智的选择。为了让插件有更高的自定义性,我们先在config中添加如下字段:

1
2
3
4
5
6
7
8
9
10
Broadcast:
Enable: true
#是否启用
Cooldown: 45
#冷却时间,以秒为单位
Prefix: "&a[公告]"
#公告前缀
Messages:
- "&c叁只仓鼠!"
- "&c欢迎来到Minecraft!"

然后,创建一个类并继承BukkitRunnable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BroadCastRunnable extends BukkitRunnable {
private List<String> message;
private int post;
//当前播放广播的位置
public BroadCastRunnable(List<String> message) {
this.message = message;
this.post = 0;
}
@Override
public void run(){
Bukkit.broadcastMessage(this.message.get(post));
//广播公告
post = (post + 1) % this.message.size();
//调整post位置为post+1,若post已指向最后一个则从0开始
}
}

然后我们在main类里先创建一个全局变量保存这个类的实例。

1
2
3
4
5
public class main extends JavaPlugin {
private FileConfiguration config;
private BroadCastRunnable broad = null;
//省略其他代码
}

然后我们在onEnable里开始写代码,注意的是,一定要先获取config实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void onEnable() {
if(this.config.getBoolean("Broadcast.Enable")) {
this.getLogger().info("公告模块已启动:");
long cooldown = this.config.getLong("Broadcast.Cooldown");
this.getLogger().info(" 公告播放频率:" + cooldown + "秒");
String prefix = this.config.getString("Broadcast.Prefix");
this.getLogger().info(" 公告前缀:" + prefix);
List<String> messages = this.config.getStringList("Broadcast.Messages");
for(int i=0;i<messages.size();i++){
this.getLogger().info(" 公告内容:" + messages.get(i));
messages.set(i, ( prefix + messages.get(i) ).replace("&", "§") );
//把每一个内容都加上公告前缀,并替换颜色字符
}
this.broad = new BroadCastRunnable(messages);
//实例化broad,把更改后的messages传递过去
this.broad.runTaskTimerAsynchronously(this, 0, cooldown*20);
//在0秒延迟后每隔20秒运行这个broad实例一次
}
else
this.getLogger().info("公告模块未启用!");
System.out.println("插件被启动了");
}

不需要多解释,看注释就行了。如果我们不想使用了这个broad实例,直接cancel就行了。这里我们在插件被关闭时取消这个broad实例,所以我们在onDisable里加上代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onDisable() {
//省略其他代码
if(this.broad != null) {
this.broad.cancel();
//如果broad已被实例化了,则取消这个线程
//因为上面的代码中我们实例化了broad之后就开启了这个线程
//所以我们只需要判断broad是否实例化,而不用判断它是否被开启了
this.getLogger().info("公告模块已关闭!");
}
System.out.println("插件被关闭了");
}

这样我们的插件就能实现自动公告了!
自动公告
(图中忘了修改plugin.yml中的version,不要在意~)

匿名内部类写法

首先在main类里创建一个全局变量BukkitTask用于存储BukkitRunnable调用启动线程的方法后返回的BukkitTask。

1
2
3
4
5
public class main extends JavaPlugin{
private FileConfiguration config = null;
private BukkitTask broad = null;
//省略其他代码
}

然后在onEnable里创建匿名内部类并使用runTaskTimerAsynchronously方法,将返回值赋值给broad变量。

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
@Override
public void onEnable() {
//其他代码,其中包含了获取config实例
if(this.config.getBoolean("Broadcast.Enable")) {
this.getLogger().info("公告模块已启动:");
long cooldown = this.config.getLong("Broadcast.Cooldown");
this.getLogger().info(" 公告播放频率:" + cooldown + "秒");
String prefix = this.config.getString("Broadcast.Prefix");
this.getLogger().info(" 公告前缀:" + prefix);
List<String> messages = this.config.getStringList("Broadcast.Messages");
for(int i=0;i<messages.size();i++){
this.getLogger().info(" 公告内容:" + messages.get(i));
messages.set(i, ( prefix + messages.get(i) ).replace("&", "§") );
//把每一个内容都加上公告前缀,并替换颜色字符
}
this.broad = new BukkitRunnable() {
private List<String> message = messages;
private int post = 0;
//当前播放广播的位置
@Override
public void run(){
Bukkit.broadcastMessage(this.message.get(post));
//广播公告
post = (post + 1) % this.message.size();
//调整post位置为post+1,若post已指向最后一个则从0开始
}
}.runTaskTimerAsynchronously(this, 0, cooldown*20);
}
else
this.getLogger().info("公告模块未启用!");
System.out.println("插件被启动了");
}

然后如果我们想要关闭这个线程,只要使用这个线程返回的BukkitTask里的cancel方法就可以关闭这个线程了。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onDisable() {
//其他代码
if(this.broad != null) {
this.broad.cancel();
//如果broad已被实例化了,则取消这个线程
//因为上面的代码中我们实例化了broad之后就开启了这个线程
//所以我们只需要判断broad是否实例化,而不用判断它是否被开启了
this.getLogger().info("公告模块已关闭!");
}
System.out.println("插件被关闭了");
}

这两样代码作用都是一样的,只是我不是很喜欢匿名内部类的写法,容易出错。

BukkitTask

BukkitRunnable里的所有启动线程方法都会返回一个BukkitTask(除了run()),这个BukkitTask里的cancel方法和BukkitRunnable里的cancel不太一样。
比如说你现在一个BukkitRunnable同时执行了两次创建线程的方法,那么他将返回两个不同的BukkitTask,使用这两个方法中的一个BukkitTask.cancel()只会停止其中一个线程,但是如果你使用了BukkitRunnable里的cancel()方法,两个线程都会被停止……请注意这一点。
在上面介绍的自动公告中,第一种写法里我们只创建了一个线程,所以使用BukkitRunnable里的cancel并无大碍,读者以后使用这个方法时应该提醒自己:是否需要关闭所有由该实例创建的线程!

本章完

点我返回目录

感谢各位的阅读!

人生不易,仓鼠断气