You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Bukkit PlayerMoveEvent如何实现玩家碰水时每20tick执行一次代码

问题原因

你的代码逻辑存在两个核心错误,直接导致执行频率不符合预期:

  • PlayerMoveEvent 会在玩家位置发生任何变化时触发,玩家在水中移动时,单游戏刻内可能触发十几次甚至几十次该事件。你每次触发事件只要检测到玩家在水里,就会新建一个20游戏刻后执行的延时任务,短时间内会堆积海量重复的延时任务。
  • 没有做任何任务去重、冷却校验、资源清理逻辑:20游戏刻后,之前堆积的所有延时任务会陆续触发,最终变成每游戏刻都执行业务代码,甚至玩家离开水、退出服务器后,残留的任务还会继续运行。
修正方案

优先推荐第一种方案,性能开销最低,不会产生额外定时任务,逻辑最简单可控。

方案1:基于游戏刻的冷却标记实现

核心逻辑:用哈希表存储每个玩家上次执行水中业务逻辑的服务器游戏刻,每次移动事件触发时先校验冷却时间,只有间隔满20游戏刻才执行业务,不会创建多余定时任务。
修正后的完整代码:

package me.pgk.Listeners;

import me.pgk.PGK;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.UUID;

public class WaterLavaDamage implements Listener {

    private final PGK plugin;
    // 存储玩家上次执行水中逻辑的游戏刻
    private final HashMap<UUID, Long> lastExecuteTickMap = new HashMap<>();
    private static final long INTERVAL_TICK = 20L;

    public WaterLavaDamage(PGK plugin) {
        this.plugin = plugin;
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
    }

    @EventHandler
    public void onWaterMove(PlayerMoveEvent e) {
        Player player = e.getPlayer();
        UUID uuid = player.getUniqueId();
        long currentTick = plugin.getServer().getCurrentTick();

        // 玩家不在水中,清理缓存直接返回
        if (player.getLocation().getBlock().getType() != Material.WATER) {
            lastExecuteTickMap.remove(uuid);
            return;
        }

        // 首次进入水中初始化上次执行时间,等待20刻后第一次触发
        long lastExecuteTick = lastExecuteTickMap.getOrDefault(uuid, currentTick);
        // 未到执行间隔直接跳过
        if (currentTick - lastExecuteTick < INTERVAL_TICK) {
            return;
        }

        // 间隔满足,执行业务逻辑
        player.sendMessage("Water Damage");
        // 更新上次执行时间
        lastExecuteTickMap.put(uuid, currentTick);
    }

    // 玩家退出时清理缓存,避免内存泄漏
    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent e) {
        lastExecuteTickMap.remove(e.getPlayer().getUniqueId());
    }
}

方案2:单例重复任务实现(适合重业务场景)

如果你的水中业务逻辑比较重,不适合绑定在高频触发的移动事件上,可以用单任务模式:

  • 玩家第一次进入水中时,先判断是否已经为该玩家创建了运行中的水中逻辑任务,没有则创建一个每20游戏刻执行一次的重复任务,将任务ID存入哈希表
  • 玩家离开水、退出服务器时,根据存储的任务ID取消对应定时任务,清理缓存

注意:该方案必须严格管理任务ID,避免任务残留造成内存泄漏和无效逻辑执行。

原代码避坑点
  • 禁止在高频触发的事件(如移动事件、伤害事件)中无节制创建定时任务,必须做去重判断
  • 涉及玩家的缓存数据必须在玩家离开触发场景、退出服务器时及时清理
  • 固定间隔触发的逻辑优先用时间/刻差值做冷却判断,比堆叠延时任务稳定性高很多

内容的提问来源于stack exchange,提问作者PGK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 23:51:29