前言

自制的MC小游戏地图中需要用到同队玩家发光的功能来提升游戏体验,但是没有找到可以实现这一功能的插件,于是从零开始学习队伍发光插件的制作。文章记录初次开发插件过程和队伍发光的做法,如有不足之处,还请见谅。

准备工作

由于之前从未接触过MC插件开发,第一步自然是要学习插件开发的相关内容。这里可以直接参考Spogit插件官方指南进行学习,基本上是最简单的使用案例,可以直接上手。

Spigot 插件开发指南: https://www.spigotmc.org/wiki/spigot-plugin-development/

遗憾的是,学习Spigot插件开发的过程中发现Spigot API并没有提供所需要的发光功能,于是在Spigot论坛上搜索,查找如何实现队伍发光效果。

搜索结果里有现成的队伍发光插件,但是版本与我所需要的不符。还有一条求助帖子,里面有大佬回复给出了队伍发光的实现方法和样例代码。大概的意思是给客户端发送特定的数据包,就能让玩家看到其他实体发光。但是,有时玩家状态发生变化时会向服务端发包,进而由服务端转发到各个客户端,这些包会覆盖掉原本的发光效果(比如玩家疾跑状态切换时发的包)。因此,插件还要对这部分包进行处理,拦截后修改其中的内容,再发往其他客户端,以保证发光效果能够持续存在。

正当我准备动手实现的时候,我又发现了一个更方便的东西。官方指南给的优秀插件案例里有一个项目,功能是对指定玩家和实体,让玩家的客户端显示该实体发光,而且考虑了客户端发送的其他包对发光效果的影响,做得比较完善,而且还在持续维护,目前支持1.17-1.20.4。更重要的是,这一项目的包拦截是自己手写的,而没有使用ProtocolLib,也就是插件的运行不需要依赖ProtocolLib这一相比插件简单的功能来说十分重量级的包,这正是我想要的。项目地址:https://github.com/SkytAsul/GlowingEntities

有了这个,只需要套层队伍逻辑的壳就行了,具体实现之后再细看。

初始化

项目初始化

IDE使用IDEA,安装Minecraft Development插件之后,新建Minecraft Spigot插件项目,这里我使用的是Maven。

插件主类为TeamGlowPlugin。

引入GlowingEntities

使用Maven引入:

1
2
3
4
5
6
<dependency>
<groupId>io.github.skytasul</groupId>
<artifactId>glowingentities</artifactId>
<version>{VERSION}</version>
<scope>compile</scope>
</dependency>

但是,我这里Maven添加依赖一直无法识别,而作者很贴心地将功能全部集成在了一个文件中,于是直接将文件GlowingEntities.java复制一份到项目中,并添加需要的依赖。

1
2
3
4
5
6
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.68.Final</version>
<scope>provided</scope>
</dependency>

编写代码

插件主类 TeamGlowPlugin

TeamGlowPlugin继承了JavaPlugin,主要需要重写启用和禁用插件时执行的方法,进行初始化和清理工作。目前没有什么需要初始化载入和卸载时清理的东西,打条日志即可,代码略。

plugin.yml

编辑plugin.yml,填写插件相关信息。

1
2
3
4
5
6
# plugin.yml
name: TeamGlow
version: '1.0'
main: cn.yunjic.teamglow.TeamGlowPlugin
api-version: '1.20'
author: Cloud7_c

TeamGlow

TeamGlow类主要负责实现队伍发光逻辑。因为有GlowingEntities,所以只需要实现对每一队伍,调用发光函数使得成员之间互相看见发光即可。

为了使得插件共用一个TeamGlow的实例化对象,使用了静态代码块实现的单例模式

TeamGlow类持有GlowingEntities类的实例化对象glowingEntities,用于调用其提供的实体发光功能。

最主要的方法是enableTeamGlowdisableTeamGlowrefresh,用于控制队伍发光效果的开启、停止和刷新。

另有一个辅助函数refreshTeamList,用于生成队伍列表,每个队伍用一个Set保存属于这一队伍的所有玩家,运行结果保存在TeamGlow类的私有变量Map<Team, Set<Player>> teamListMap中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Refresh team list. Result saved in teamPlayersList.
*/
private void refreshTeamList(){
teamListMap.clear();
Collection<? extends Player> players = Bukkit.getServer().getOnlinePlayers();
for(Player player : players){
Team team = player.getScoreboard().getEntryTeam(player.getDisplayName());
if(team != null){
Set<Player> teamPlayersList = teamListMap.getOrDefault(team, new HashSet<>());
teamPlayersList.add(player);
teamListMap.put(team, teamPlayersList);
}
}
}

enableTeamGlow首先调用refreshTeamList函数刷新队伍列表,再遍历队伍列表,对每个队伍内的所有玩家,使得玩家之间互相能看见发光。同时设置TeamGlow的变量glowingtrue,代表队伍发光效果已启用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Make players in the same team see each other glowing.
*/
public void enableTeamGlow(){
refreshTeamList();
Collection<Set<Player>> teams = teamListMap.values();
for(Set<Player> players : teams){
for(Player receiver : players){
for(Player player : players){
try {
glowingEntities.setGlowing(player, receiver);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
}
setGlowing(true);
}

enableTeamGlow遍历在线玩家列表,对每个玩家调用GlowingEntities里的unsetGlowing(player, receiver)方法,取消发光效果显示。同时设置TeamGlow的变量glowingfalse,代表队伍发光效果已取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Disable glowing for every player.
*/
public void disableTeamGlow(){
Collection<? extends Player> onlinePlayers = Bukkit.getServer().getOnlinePlayers();
for(Player player : onlinePlayers){
for (Player receiver : onlinePlayers){
try {
glowingEntities.unsetGlowing(player, receiver);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}

setGlowing(false);
}

刷新队伍发光效果,特别是当玩家更换队伍时,发光效果不会自动更新,这时就需要调用这一函数。

1
2
3
4
5
6
7
8
9
/**
* Refresh team glowing effect, especially for when a player changes teams.
*/
public void refresh(){
disableTeamGlow();
if(glowing){
enableTeamGlow();
}
}

GlowingEntities类中有一个方法,作用是禁用发光功能,需要在插件卸载时调用。在TeamglowPluginonDisable()中添加:

1
2
// TeamglowPlugin.onDisable()
TeamGlow.getInstance().glowingEntities.disable();

TeamglowCommand

TeamglowCommand类,实现TabExecutor接口,实现/teamglow命令和命令自动补全功能。

命令包含3个子命令:enable、disable、refresh,分别表示启用、禁用和刷新队伍发光效果。

命令执行:

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
@Override
public boolean onCommand(CommandSender commandSender, Command command, String label, String[] args) {
if(args.length == 0) return false;

TeamGlow teamGlow = TeamGlow.getInstance();
String senderName = commandSender.getName();

switch (args[0]) {
case "true":
if (!teamGlow.isGlowing()) {
teamGlow.enableTeamGlow();
TeamGlowPlugin.log("Team glow has been enabled by " + senderName);
commandSender.sendMessage("§aTeam glow has been enabled.");
}
else {
TeamGlowPlugin.log("Team glow is already enabled.");
commandSender.sendMessage("§cTeam glow is already enabled.");
}
break;
case "false":
if (teamGlow.isGlowing()) {
teamGlow.disableTeamGlow();
TeamGlowPlugin.log("Team glow has been disabled by " + senderName);
commandSender.sendMessage("§aTeam glow has been disabled.");
}
else {
TeamGlowPlugin.log("Team glow is already disabled.");
commandSender.sendMessage("§cTeam glow is already disabled.");
}
break;
case "refresh":
TeamGlowPlugin.log("Refreshing team glow by " + senderName);
commandSender.sendMessage("§aRefreshing team glow.");
teamGlow.refresh();
break;
default:
return false;
}
return true;
}

命令自动补全:

1
2
3
4
5
6
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) {
if(args.length == 0 || args.length == 1) return Arrays.asList("enable", "disable", "refresh");
return null;
}

在插件主类中注册命令。TeamGlowPlugin.onEnable()中添加:

1
2
3
4
// TeamGlowPlugin.onEnable()
// register command "teamglow"
this.getCommand("teamglow").setExecutor(new TeamGlowCommand());
this.getCommand("teamglow").setTabCompleter(new TeamGlowCommand());

plugin.yml中添加命令:

1
2
3
4
5
6
7
8
9
10
11
# plugin.yml中添加内容
commands:
teamglow:
description: set team glowing effect.
usage: "Usage: /<command> [enable|disable|refresh]"
permission: teamglow.op
permission-message: You don't have permission!
permissions:
teamglow.op:
description: allow teamglow
default: op

考虑玩家的退出重进

玩家退出会自动取消发光,再重新加入时需要考虑重新为玩家加上发光效果,包括同队玩家对该玩家发光以及该玩家对同队玩家发光。

TeamGlow类中添加函数refreshPlayer,为单个玩家刷新发光效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Refresh glowing effect for one player.
*
* @param receiver the player to be refreshed
*/
public void refreshPlayer(Player receiver) {
if(!glowing) return;

refreshTeamList();
Team team = receiver.getScoreboard().getEntryTeam(receiver.getDisplayName());
Set<Player> teamPlayers = teamListMap.getOrDefault(team, new HashSet<>());
if (team != null){
for (Player player : teamPlayers) {
try {
glowingEntities.setGlowing(player, receiver);
glowingEntities.setGlowing(receiver, player);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
}

添加一个类JoinEventListener,用于监听玩家加入游戏的事件,并调用TeamGlowrefreshPlayer方法,为该玩家刷新发光效果。

1
2
3
4
5
6
public class JoinEventListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event){
TeamGlow.getInstance().refreshPlayer(event.getPlayer());
}
}

插件主类TeamGlowPlugin中注册监听事件。TeamGlowPlugin.onEnable()中添加:

1
2
3
// TeamGlowPlugin.onEnable()
// register join event
Bukkit.getPluginManager().registerEvents(new JoinEventListener(), this);

持久化配置

为了重启服务器后,玩家能保存发光状态,可以引入配置文件来保存当前队伍发光效果是否开启。

1
2
# config.yml
glowing: false

插件启用时自动加载保存的配置:

1
2
3
4
5
6
7
// TeamGlowPlugin.onEnable()
FileConfiguration config = this.getConfig();
config.getDefaults();

TeamGlow teamGlow = TeamGlow.getInstance();
teamGlow.setGlowing(config.getBoolean("glowing"));
teamGlow.refresh();

插件卸载时自动保存当前配置:

1
2
3
// TeamGlowPlugin.onDisable()
this.getConfig().set("glowing", TeamGlow.getInstance().isGlowing());
this.saveConfig();

结合数据包使用

使用Spigot API实现的命令在数据包中是无法识别的,而我制作的小游戏地图的游戏逻辑是通过数据包控制的,因此需要将接口暴露到数据包能够控制的地方。这里我选择了将接口暴露到计分板上。

TeamGlow中编写定时任务TeamGlowTask类,继承BukkitRunnable类,实现定时任务的功能,定时查询计分板team_glow的计分项$glowing的变化,向数据包暴露队伍发光插件接口,以供数据包调用插件功能。

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
private class TeamGlowTask extends BukkitRunnable {
private final JavaPlugin plugin;
private Objective objective;
private Score score;
private int previousScore;
private final String objectiveName = "team_glow";
private final String scoreName = "$glowing";

public TeamGlowTask(JavaPlugin plugin) {
this.plugin = plugin;

// 获取目标计分板(Objective)实例
this.objective = Bukkit.getScoreboardManager().getMainScoreboard().getObjective(objectiveName);

// 如果计分项 "team_glow" 不存在,则创建该计分项
if (this.objective == null) {
this.objective = Bukkit.getScoreboardManager().getMainScoreboard().registerNewObjective(objectiveName, Criteria.DUMMY, "TeamGlow");
}

// 获取或创建分数记录 "$glowing"
score = objective.getScore(scoreName);
previousScore = glowing ? 1 : 0;
score.setScore(previousScore);
}

public void setScore(int newScore){
score.setScore(newScore);
previousScore = newScore;
}

@Override
public void run() {
int newScore = score.getScore();

// 检查分数是否发生变化
if (newScore != previousScore) {
if(newScore == 0){
disableTeamGlow();
}
else if (newScore == 1){
enableTeamGlow();
}
else if(newScore == 2){
refresh();
score.setScore(previousScore);
}
else score.setScore(previousScore);
}
}
}

TeamGlow初始化时调用:

1
2
3
JavaPlugin plugin = TeamGlowPlugin.getInstance();
teamGlowTask = new TeamGlowTask(plugin);
teamGlowTask.runTaskTimer(plugin, 0, 10); // run task every 0.5 seconds

这样,数据包使用时,只需要将计分板team_glow的计分项$glowing的值设置为1、0、2,就可以分别触发启用、禁用、刷新队伍发光功能了。

我不知道这种插件搭配数据包使用的办法是否合理,因为没有在网上找到其他相关的内容。倒是有一个插件可以实现让数据包能够调用插件自定义的命令,但是我觉得用另一种格式重新配置命令太麻烦了,而且对于队伍发光这样一个非常轻量级的插件,随便引入一个包都要比本体重的多了,没必要。

效果测试

自动更新插件到服务端 //todo

效果 //todo

不足

切换队伍不能自动刷新发光效果。

TeamGlowTask只是供搭配数据包使用的,但是没有设置选择性启用的接口,在插件加载期间将持续运行。

发光效果的实现原理

// todo

参考

Spigot 插件开发指南:https://www.spigotmc.org/wiki/spigot-plugin-development/

GlowingEntities:https://github.com/SkytAsul/GlowingEntities

实体数据包格式:https://wiki.vg/Entity_metadata

发包实现发光:https://www.spigotmc.org/threads/glow-packets-per-player-help.257884/