返回

PaperMC插件计数翻倍问题?两种方法轻松解决!

java

解决 PaperMC 插件中点击计数翻倍的问题

在开发 PaperMC 插件时,常常需要追踪玩家的行动,比如计算攻击次数以触发特殊技能。有时,一个看似简单的计数逻辑,可能在特定情况下出现翻倍计数的问题,这通常会让开发者困惑。这篇文章旨在探讨这一问题并提供有效的解决方案。

问题分析:双重计数的根源

当一个事件被触发时,你可能会认为它只会被调用一次。但是,当使用类似EntityDamageByEntityEvent这样的事件,情况可能比较复杂。在某些情况下,例如玩家使用带有攻击判定的道具(比如斧头)攻击生物,此事件会触发多次。主要原因可能在于服务端和客户端之间对于伤害计算的不同步。简而言之,服务器端检测一次,客户端也检测一次,如果你的代码仅简单地在 onEntityDamage 方法里做一次 hits++,就会出现计数翻倍现象。 仔细研究 PaperMC API的 EntityDamageByEntityEvent 能帮你更透彻地理解背后的机制。

解决方案 1:加入冷却时间 (Attack Cooldown) 判断

一个常见的解决办法是利用 Minecraft 内建的攻击冷却(attack cooldown)机制。每个玩家的武器攻击都有一个内置的冷却时间,通过 player.getAttackCooldown() 获取当前玩家的攻击冷却值(0.0-1.0, 1.0 表示攻击冷却完毕),这样就只有当玩家攻击冷却完毕(比如 >0.9 )时才会被计算。这个思路很关键,避免因为判定和客户端之间的差异重复计算。

代码示例:

@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
    if (!(event.getDamager() instanceof Player)) return;
    Player player = (Player) event.getDamager();
    ItemStack item = player.getInventory().getItemInMainHand();

    if (item == null || item.getType() != Material.NETHERITE_AXE || !item.getItemMeta().hasDisplayName() || !item.getItemMeta().getDisplayName().equals(WEAPON_NAME)) {
      return;
    }

   if(player.getAttackCooldown() < 0.9 ) return;

   // 继续后面的计数逻辑
     UUID playerId = player.getUniqueId();
        if (player.getInventory().getItemInMainHand() != null && player.getInventory().getItemInMainHand().getType() != Material.AIR) {
        int hits = hitCounter.getOrDefault(playerId, 0) + 1;

            // Zaktualizuj licznik hitów
           hitCounter.put(playerId, hits);
           updateActionBar(player, hits);
            if (hits >= 20 && !superAttackReady.getOrDefault(playerId, false)) {
              superAttackReady.put(playerId, true);
             player.sendMessage(slMessageCustom + ChatColor.GREEN + "ꜱᴜᴘᴇʀ ᴀᴛᴀᴋ ɢᴏᴛᴏᴡʏ! ᴋʟɪᴋɴɪᴊ ᴘᴘᴍ ᴀʙʏ ᴀᴋᴛʏᴡᴏᴡᴀᴄ.");
           }
           // Dodawanie efektu withera graczom podczas super ataku
          if (isSuperAttackActive(player) && event.getEntity() instanceof LivingEntity) {
             LivingEntity target = (LivingEntity) event.getEntity();
              target.addPotionEffect(new PotionEffect(PotionEffectType.WITHER, 80, 1));
           }
     }
}

操作步骤:

  1. 在你的插件代码中,找到 onEntityDamage 方法。
  2. 加入 if(player.getAttackCooldown() < 0.9) return; 确保只在玩家攻击冷却结束时才继续后续的逻辑。

此方法易于实现,并且能很好的避免快速点击带来的重复计算问题。

解决方案 2: 使用时间戳避免快速攻击的重复计算

另一种处理重复计数的方式是通过时间戳来限制攻击频率。为每个玩家保存最近一次成功计入攻击的时间。当再次触发 EntityDamageByEntityEvent 时,检查当前时间和最近攻击时间的时间差是否小于一个预定的时间间隔(HIT_INTERVAL),只有超过这个间隔的攻击才会计入。这样也能有效地过滤因为其他原因(如客户端行为导致事件触发)带来的重复计数。

代码示例:

 private Map<UUID, Long> lastHitTime = new HashMap<>();
 private long HIT_INTERVAL = 100;  //例如,设置攻击间隔为100毫秒

@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
     if (!(event.getDamager() instanceof Player)) return;

    Player player = (Player) event.getDamager();
    ItemStack item = player.getInventory().getItemInMainHand();
   if (item == null || item.getType() != Material.NETHERITE_AXE || !item.getItemMeta().hasDisplayName() || !item.getItemMeta().getDisplayName().equals(WEAPON_NAME)) {
        return;
   }
   if(player.getAttackCooldown() < 0.9 ) return;
   UUID playerId = player.getUniqueId();

  if (player.getInventory().getItemInMainHand() != null && player.getInventory().getItemInMainHand().getType() != Material.AIR) {

          long currentTime = System.currentTimeMillis();
         long lastHit = lastHitTime.getOrDefault(playerId, 0L);
         if (currentTime - lastHit < HIT_INTERVAL) return; //如果间隔过短就跳过
         lastHitTime.put(playerId, currentTime);

        int hits = hitCounter.getOrDefault(playerId, 0) + 1;

            // Zaktualizuj licznik hitów
           hitCounter.put(playerId, hits);
           updateActionBar(player, hits);

            if (hits >= 20 && !superAttackReady.getOrDefault(playerId, false)) {
               superAttackReady.put(playerId, true);
              player.sendMessage(slMessageCustom + ChatColor.GREEN + "ꜱᴜᴘᴇʀ ᴀᴛᴀᴋ ɢᴏᴛᴏᴡʏ! ᴋʟɪᴋɴɪᴊ ᴘᴘᴍ ᴀʙʏ ᴀᴋᴛʏᴡᴏᴡᴀᴄ.");
           }
              if (isSuperAttackActive(player) && event.getEntity() instanceof LivingEntity) {
               LivingEntity target = (LivingEntity) event.getEntity();
                target.addPotionEffect(new PotionEffect(PotionEffectType.WITHER, 80, 1));
             }

     }
}

操作步骤:

  1. 在你的插件类中,定义一个 Map<UUID, Long> 来存储每个玩家最近的攻击时间,以及定义 HIT_INTERVAL 来设置攻击间隔,如 private Map<UUID, Long> lastHitTime = new HashMap<>(); private long HIT_INTERVAL = 100;
  2. onEntityDamage 方法中,增加一段逻辑判断当前时间与玩家上次攻击的时间间隔是否超过设定的时间 if (currentTime - lastHit < HIT_INTERVAL) return; 如果过短,就跳过本次计算,lastHitTime.put(playerId, currentTime); 在计数增加后记录这次时间。
  3. 可以依据实际情况调整 HIT_INTERVAL的值

时间戳法更精确,允许你更加细粒度的控制,特别适合需要精细控制频率的场景。

安全建议

  1. 仔细测试 : 在生产环境中使用前,仔细测试你的计数逻辑,确保没有其他的因素可能导致双重计数或者漏计数的情况。
  2. 参数化配置:HIT_INTERVAL 等类似的重要参数放到配置文件中,方便日后修改调整。
  3. 异常处理: 在重要的环节增加 try catch,避免因为其他插件或 Minecraft 本身机制错误导致插件运行出错。

这些方法都能帮你更有效地处理 EntityDamageByEntityEvent 导致的重复计数, 灵活选择并使用它们。

总结

通过应用这些方法,你可以解决 PaperMC 插件开发中因事件重复触发造成的计数问题。使用攻击冷却判断与使用时间戳是两个有效处理重复计数的方向,并且需要针对特定情况,选取合适的策略或者同时采用这些方法进行结合使用。开发中遇到类似情况,耐心分析,灵活应用不同的解决方案。