Minecraft中ScoreBoard的底层实现与扩展应用

ScoreBoard计分板专题


本文章着重整理了Bukkit插件开发中计分板的底层实现与工作原理,是作者个人经验积累,之后会慢慢补充。

一、Bukkit对计分板的底层实现

1、概述:

(1)管理计分板

Bukkit中提供了一个用于管理计分板的类,总之要对计分板进行操作就要先获取这个类。

ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager();

(2)计分板分类

要对计分板进行操作,首先应该先了解一下底层是怎么对计分板进行架构的,其实很简单,服务器中的有两个“巨头”,一个是主计分板,一个是玩家的个人计分板。

主计分板

可以理解为一个全局共用的信息,一个服务器中所有人共用的信息就存在主计分板上,尤其是起床战争这种团队游戏就要经常和它打交道。

获取主计分板:

Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
个人计分板

就是针对每个玩家的统计信息,例如在一些PVP服务器中,每个玩家在侧边栏看到自己的击杀数都不一样,实际上就是因为每个玩家都有自己的一块计分板,是玩家“私有或者特有”的。

获取个人计分板,通常一个玩家拥有一个计分板即可
新建计分板如下:

Scoreboard scoreboard = scoreboardManager.getNewScoreboard();

获取已有计分板如下:

Scoreboard scoreboard = player.getScoreboard();

这里注意:个人计分板还可分为三个部分,分别是侧边栏、名字下方、玩家列表,这三个其实都是同属于一个计分板,只是他们的显示位置不同,后面我们会详细介绍。



2、个人计分板的编辑


在创建了个人计分板之后就需要对计分板进行一些修饰,本节介绍如何选择三个不同显示位置的计分板并修改他们显示的内容。

(1)Objective类

简单来讲,侧边栏、名字下方、玩家列表这三个其实就是同属于个人计分板下的三个不同的Objective类。

如果已有这些类,那就通过下面这条语句获取:

Objective objective = scoreboard.getObjective(<DisplaySlot>)

这里的DisplaySlot有三种,分别对应侧边栏、名字下方、玩家列表

所以我们一般<criteria>会填入"dummy",可以理解为空准则,它不会被玩家死亡或击杀变动。
如果没有事先创建,那么应该创建一个

Objective objective = scoreboard.registerNewObjective(<name>,<criteria>");

<name>是标识名称,可以自己取,<criteria>是显示准则,这里详细讲一下:
<criteria>参数有这些,顾名思义,就不再解释每个参数的含义;

需要注意的是,这些参数只有当显示在名字下方时这些参数才生效。

objective.setDisplaySlot(DisplaySlot.BELOW_NAME);

此外还能设置计分板的标题栏内容

objective.setDisplayName(<标题内容>);

其他的有关objective的操作函数放在这里,都是顾名思义的

❓可见,bukkit似乎没有提供一个函数用来操作玩家列表计分板的footer,该内容有待探究(2024.5.25补充:考虑使用NMS)

这里要提到.isModifiable(),将其设置为false之后其他插件就无法对计分板进行修改,在后续讲到的TAB插件中,其生成的计分板就是不可被覆写的,这导致其他插件在TAB插件生效的情况下可能无法对计分板进行操作。如何将这些插件进行兼容,我们后续探讨。

(2)分数Score

分数项是对侧边栏(DisplaySlot.SIDEBAR)计分板有效,下面先讲讲分数显示原理。

计分板除标题以外每一行的内容为文本+分数,而决定每一行显示在哪是由分数决定的,计分板默认分数较高者排在最上面,即离标题最近的地方。

例如,有两行apple : 10 和orange : 1,那么明显apple的分数较大,它将显示在orange的上面
分数的范围为-2,147,483,648至2,147,483,647,没有小数。

利用这个原理,我们可以自定义每一行的分数从而来实现文本的显示顺序

通常为计分板添加积分项可以写一个封装函数,便于之后调用

private static void addScore(Objective objective, String text, int score) {
        Score scoreboardScore = objective.getScore(text);
        scoreboardScore.setScore(score);
    }

(3)计分板的推送与注销

  • 计分板的推送

在通过上面的步骤创建完计分板之后,玩家其实还是不能看见你为他们创建的计分板,所以还要调用计分板推送指令。

player.setScoreboard(scoreboard);
//这里的scoreboard就是定义好的个人计分板

注意:这里就不得不说到计分板刷新率问题,bukkit插件开发中,计分板上面的内容并不会自动刷新,也就是每调用上面的那条推送语句,玩家的计分板内容才被刷新。但是通常情况下,我们要实现的是计分板能够根据玩家的统计数据(如击杀数、破坏方块数量)来进行动态更新,这就要求我们在合适的时机调用推送指令。也有的插件如TAB插件干脆直接使用定时任务来周期性推送计分板,于是就有了计分板刷新率的说法。

虽然周期性刷新计分板可以实现一些炫酷的变色、闪烁等效果,但是过高的刷新率将会给服务器和数据库产生较高的压力,例如在计分板上面使用了一些需要读取数据库的内容(或者是解析了某个占位符),当刷新率较高时,占位符的解析(或者数据库的请求)任务将会变得很重,给服务器带来一些负担。

面对这种情况,还是要结合实际,选择合适的刷新办法(或者刷新率),对于计分板实时性没有太高要求的计分板,我们可以采用条件性的手动刷新;而对于实时性要求较高的计分板,我们可以设置缓存区,来减少数据库交互压力。


2024.5.24补充:

注意:该部分在研究“无闪计分板”时总结,读者可以结合自身情况,可以先跳过这部分,在学习Team等相关操作后再来阅读。

这部分重点仔细分析了计分板的三种不同的刷新办法以及他们的优缺点:

  1. 在编辑完scoreboard后执行player.setscoreboard()函数进行推送计分板

    这种办法操作简单、适用范围最广,通常使用这种办法即可满足大部分需求。但是如前文所说,如果计分板面对的是高刷新率、读写量高的情况,则需要对代码进行适当的优化。总体而言,这种办法适应性很强,除非刷新率极高,一般不会出现计分板闪烁的问题。

  2. 每一次刷新则调用scoreboard.resetScore()函数清空所有得分,然后重新编辑并推送计分板。

    我不知道这种方法怎么会有人用,在性能方面简直最差,多此一举。除非你要用计分板做一些行间内容变换,否则强烈不推荐使用该办法。

  3. 使用Team功能,仅修改前后缀来实现计分板内容变换。

    这是一个很好的思路,特别适合用于追求色彩变幻、高刷新率、数据读写量较少的动画计分板,很适合用来制作无闪计分板。但也有局限,先说结论,这种办法能创建的分数项受限于色彩字符的数量。

    下面讲讲怎么实现,直接上代码:

    Team team1 = scoreboard.registerNewTeam("1");
    //请注意,这里的addEntry是添加标记(入口),不仅可以是玩家名、也可是其他字符
    team1.addEntry("§a ");
    
    timer.setSuffix("suf");
    timer.setPrefix("pre");
    //巧妙的就在这里,由于team1的Entry也为"§a ",那么当下次修改team1的前缀和后缀,计分板上面的这个内容也会随之修改其前缀和后缀。
    addScore(objective,"§a ",1);
    
    /*
    其他实现
    */
    
    //添加计分项
    private static void addScore(Objective objective, String text, int score) {
     Score scoreboardScore = objective.getScore(text);
     scoreboardScore.setScore(score);
    }
    
    

这么一来,下一次修改计分板的内容时,只需要修改对应entry对应team的前缀与后缀即可,直接省略去了计分板的重置、推送问题,开发者可以单独对某一行的内容进行修改而不需要连同其他积分项一起重新再写一遍,大大提高了计分板的刷新性能与灵活性。

但为了美观,这里team的Entry通常选用可以不被显示出来的颜色代码,所以team的数量就受限于颜色代码数。

更详细的示例可以参考这篇文章的无闪计分板部分


  • 计分板的注销

在卸载插件的时候,为了安全,通常需要卸载掉本插件注册的计分板,以确保下次插件加载时不会重定义等错误。对于个人计分板,其注销方法较为简单:

objective.unregister();//选中要注销的objective计分板进行注销即可


3、主计分板

前面已经介绍过主计分板的获取方法与其应用的情景。这里再补充几点主机分板与个人计分板之间的区别,这些特点务必记住。

  • 主计分板全服玩家共有
  • 通常无显示界面
  • 常用于团队分配
  • 对于队伍变更信息,主计分板会自动推送变更

起床战争中不同队伍的玩家衣服的颜色、名字的颜色和前缀都不一样,而且同队伍玩家不能互相攻击,不同队伍的玩家可以互相攻击,这些内容其实就是通过主计分板的Team来实现,可以理解为主计分板就是一个存放全服队伍信息的容器。

由于上面的第四条特点,自适应队伍变更为我们编辑主计分板提供了很大的便利,通常只需要把玩家添加或移除某个队伍,就可以实现简单的分组

(1)分配团队 Team

先看主计分板有哪些有关于Team的操作

常用的就是这四个,其他大可不必关心(其实是懒得写,以后慢慢补充)
这四个分别就是获取所有团队、通过团队名获取团队、获取某个生物的团队、注册一个新的团队。

补充:

移除团队不在这里面,而是直接对team对象执行.remove()语句,后续会讲

team.unregister();
//通常卸载插件时也需要手动注销一下插件创建的团队,以免产生错误!!
//很重要,别问我怎么知道的

从团队移除玩家也是对team直接进行操作

team.removeEntry(player.getName());

看到这几个函数读者想必已经非常清楚,所以下面只给出一些常用的代码。

  • 队伍创建
  // 检查是否已存在该队伍,如果不存在则创建新的队伍
        Team team = scoreboard.getTeam(teamName);
        if (team == null) {
            team = scoreboard.registerNewTeam(teamName);
        }
  • 检查玩家是否拥有队伍
//在这个函数的基础上可以与Placeholder结合,注册出占位符
//进一步来说,甚至可以通过配置文件来进行动态管理占位符名称(不过也没啥必要)
    public boolean CheckIn(Player player){
        ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager();
        Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
        for (Team team : scoreboard.getTeams()){
            if(team.hasEntry(player.getName())){
                return true;
            }
        }
        return false;
    }

  • 统计某个队伍的人数
    (由于BedWar1058没有提供队伍人数变量,所以这里添加以下代码实现,以后可以添加插件补丁)
    public int NumberOfPeople(String teamName){
        int nop= 0;
        ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager();
        Scoreboard scoreboard = scoreboardManager.getMainScoreboard();
        Team team = scoreboard.getTeam(teamName);
        nop = team.getSize();
        return nop;
    }

(2)定义团队特征

前面已经实现了把玩家分配进队伍中,接下来肯定就是要为不同的队伍添加特色了

先来看Team的操作函数

常用函数就这几个,当玩家被加入某个队伍后,其名称也会立即呈现这个队伍的特征。

(3)其他操作函数的研究结果

team.addEntry()是用来向团队内添加一个成员,请注意,这个成员可以是一个已存在的实体名,也可是一个标记,这个标记并不一定要指向一个已存在的实体(具体为什么,可以返回参看【2.(3)计分板的推送】内容)。

team.setOption(Option,OptionStatus),用于设置团队内部属性

Option可选参数:

分别代表 碰撞体积、死亡消息、名称标签

OptionStatus可选参数:

分别代表 保持开启、对其他队伍关闭、对己方队伍关闭、总是关闭

team.setPrefix()给团队定制一个统一的前置标签,这个标签不仅会在玩家的头顶的名字生效,还会在玩家列表里直接生效。

team.setSuffix()给团队定制一个统一的后置标签,这个标签不仅会在玩家的头顶的名字生效,还会在玩家列表里直接生效。

team.setDisplayName()给团队定制一个统一的显示名,但经过测试,这个定制的名称并不会显示在任何地方个人觉得这个几乎没有什么作用。

team.setCanSeeFriendlyInvisibles(bool);

  • 这里总结几点有关起床战争团队模式下对隐身玩家处理办法的研究:
    正常情况下,当玩家隐身时,其身体和名字对任何人都不可见,如图所示

    而当上面方法的参数被设置为true时,在友军眼里是可以看到己方玩家的名字和虚化的身体轮廓,如图所示(不要在意那个“计数器测试”,之前忘记删掉了,无视他就行)

    这时候,还可以搭配team.setOption(Team.Option.NAME_TAG_VISIBILITY,Team.OptionStatus.NEVER)方法使用,可以实现对友军隐藏玩家名,如图所示

个人经验:本人在测试这个team.setOption(Team.Option.NAME_TAG_VISIBILITY,Team.OptionStatus.NEVER)函数的时候,发现其只生效在team.setCanSeeFriendlyInvisibles(bool);参数为true的情况下,也就是说,只有玩家有能力看到隐身玩家的轮廓时,才可能具有能力去看到隐身玩家的名字。

❓遗留问题:正如前面所说,如果我要实现一个机制:同队伍玩家能看到本队伍隐身队友的名字和轮廓(这个容易实现),但是不同队伍的玩家只能看到对方的名字,而看不到对方的身体轮廓,又该如何实现?

这里我也存在疑惑,可能的思路就是去修改服务端向客户端发送的数据包了,单纯的通过bukkit函数似乎做不到这一点。

二、扩展应用TAB插件及其兼容性问题

文档地址:TAB插件官方文档
待补充...

三、游戏内自带/scoreboard指令解读

待补充...

四、内容补充

待补充...

posted @ 2024-04-17 02:51  wyuu101  阅读(92)  评论(0编辑  收藏  举报