前言 自制的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 name: TeamGlow version: '1.0' main: cn.yunjic.teamglow.TeamGlowPlugin api-version: '1.20' author: Cloud7_c
TeamGlow TeamGlow
类主要负责实现队伍发光逻辑。因为有GlowingEntities
,所以只需要实现对每一队伍,调用发光函数使得成员之间互相看见发光即可。
为了使得插件共用一个TeamGlow
的实例化对象,使用了静态代码块实现的单例模式 。
TeamGlow
类持有GlowingEntities
类的实例化对象glowingEntities
,用于调用其提供的实体发光功能。
最主要的方法是enableTeamGlow
、disableTeamGlow
和refresh
,用于控制队伍发光效果的开启、停止和刷新。
另有一个辅助函数refreshTeamList
,用于生成队伍列表,每个队伍用一个Set
保存属于这一队伍的所有玩家,运行结果保存在TeamGlow
类的私有变量Map<Team, Set<Player>> teamListMap
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
的变量glowing
为true
,代表队伍发光效果已启用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
的变量glowing
为false
,代表队伍发光效果已取消。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 public void refresh () { disableTeamGlow(); if (glowing){ enableTeamGlow(); } }
GlowingEntities
类中有一个方法,作用是禁用发光功能,需要在插件卸载时调用。在TeamglowPlugin
的onDisable()
中添加:
1 2 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 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 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 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
,用于监听玩家加入游戏的事件,并调用TeamGlow
的refreshPlayer
方法,为该玩家刷新发光效果。
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 Bukkit.getPluginManager().registerEvents(new JoinEventListener (), this );
持久化配置 为了重启服务器后,玩家能保存发光状态,可以引入配置文件来保存当前队伍发光效果是否开启。
插件启用时自动加载保存的配置:
1 2 3 4 5 6 7 FileConfiguration config = this .getConfig();config.getDefaults(); TeamGlow teamGlow = TeamGlow.getInstance();teamGlow.setGlowing(config.getBoolean("glowing" )); teamGlow.refresh();
插件卸载时自动保存当前配置:
1 2 3 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; this .objective = Bukkit.getScoreboardManager().getMainScoreboard().getObjective(objectiveName); if (this .objective == null ) { this .objective = Bukkit.getScoreboardManager().getMainScoreboard().registerNewObjective(objectiveName, Criteria.DUMMY, "TeamGlow" ); } 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 );
这样,数据包使用时,只需要将计分板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/