以每秒多次循环遍历数万到数十万个位置

Loop through tens to hundreds of thousands locations at multiples times per second

正在尝试在这里制作一个 minecraft 插件。我正在尝试制作自定义矿石生成器。发电机是立方体,外面没有外墙,中间是矿石。

当我创建一个生成器时,我不会保存每个位置对象,因为每个玩家都可以拥有多个生成器,这很快就会产生大量的块。我所做的是将边界和立方体内部的最小和最大位置保存在一个名为 CubeLocation 的对象中,该对象也存储 cubeId。

所以我将立方体保存在 HashMap 中 - (这不是玩家的 ID,但我在创建每个代码时生成一个随机的 uuid)。

而且我还将 Location 保存在 HashSet

现在解决我的问题。我需要检查损坏块的位置是否在立方体内部。我现在有的是这样的

public class LocationManager {
    
    private HashMap<UUID, PlayerCube> cubes;
    private HashSet<CubeLocation> cubeLocations;
    private HashMap<UUID, PlayerHold> playerHolds;
    private HashMap<UUID, Integer> tasks;
    private HashMap<UUID, UUID> lastKnownGenerator;
    
    public LocationManager() {
        cubes = new HashMap<>();
        cubeLocations = new HashSet<>();
        playerHolds = new HashMap<>();
        tasks = new HashMap<>();
        lastKnownGenerator = new HashMap<>();
    }
    
    public HashMap<UUID, PlayerCube> getCubes(){
        return cubes;
    }
    
    public HashSet<CubeLocation> getCubeLocations(){
        return cubeLocations;
    }
    
    public HashMap<UUID, PlayerHold> getPlayerHolds(){
        return playerHolds;
    }
    
    public HashMap<UUID, Integer> getTasks(){
        return tasks;
    }
    
    public HashMap<UUID, UUID> getLastKnownGenerator(){
        return lastKnownGenerator;
    }
    
    public String[] isLocationInsideCube(UUID player, Location loc) {
        UUID world = loc.getWorld().getUID();
        int x = loc.getBlockX();
        int y = loc.getBlockY();
        int z = loc.getBlockZ();
        String arr[] = {null, null};
        
        UUID last = lastKnownGenerator.get(player);
        
        if(last != null) {
            CubeLocation cubeLoc = cubes.get(last).getCubeLocation();
            lastKnownGenerator.remove(player);
            
            if(cubeLoc != null && cubeLoc.isInside(world, x, y, z)) {
                if(cubeLoc.isCubeLocation(world, x, y, z)) {
                    arr[1] = last.toString();
                    return arr;
                }
                
                if(arr[0] == null && cubeLoc.isBorderLocation(world, x, y, z)) {
                    arr[0] = last.toString();
                    return arr;
                }
            }
        }
        
        for(CubeLocation cubeLoc : cubeLocations) {
            if(!cubeLoc.isInside(world, x, y, z))
                continue;
            
            String id = cubeLoc.getCubeId().toString();
            
            if(arr[1] == null && cubeLoc.isCubeLocation(world, x, y, z)) {
                arr[1] = id;
                break;
            }
            
            if(arr[0] == null && cubeLoc.isBorderLocation(world, x, y, z)) {
                arr[0] = id;
                break;
            }

        }
        
        return arr;
    }

}

这是 CubeLocation

public class CubeLocation {
    
    private UUID world;
    
    private int minX;
    private int maxX;
    private int minY;
    private int maxY;
    private int minZ;
    private int maxZ;
    
    private int bMinX;
    private int bMaxX;
    private int bMinY;
    private int bMaxY;
    private int bMinZ;
    private int bMaxZ;
    
    private UUID cubeId;
    
    public CubeLocation(UUID world, UUID cubeId) {
        this.world = world;
        this.cubeId = cubeId;
    }
    
    public void setCoords(int x1, int x2, int y1, int y2, int z1, int z2) {
        //Border
        this.bMinX = x1;
        this.bMaxX = x2;
        this.bMinY = y1;
        this.bMaxY = y2;
        this.bMinZ = z1;
        this.bMaxZ = z2;
        
        //Cube
        this.minX = x1 + 1;
        this.maxX = x2 - 1;
        this.minY = y1 + 1;
        this.maxY = y2 - 1;
        this.minZ = z1 + 1;
        this.maxZ = z2 - 1;
    }
    
    public UUID getCubeId() {
        return cubeId;
    }
    
    public boolean isInside(UUID world, int x, int y, int z) {
        if(!world.equals(this.world))
            return false;
        
        return (x >= bMinX && x <= bMaxX) && (y >= bMinY && y <= bMaxY) && (z >= bMinZ && z <= bMaxZ);
    }
    
    public boolean isBorderLocation(UUID world, int x, int y, int z) {
        if(!world.equals(this.world))
            return false;
        
        if(y == bMinY || y == bMaxY) {
            return (x == bMinX && (z >= bMinZ || z <= bMaxZ)) || (x == bMaxX && (z >= bMinZ || z <= bMaxZ)) || (z == bMinZ && (x >= bMinX || x <= bMaxX)) || (z == bMaxZ && (x >= bMinX || x <= bMaxX));
            
        }
        return (x == bMinX && z == bMinZ) || (x == bMaxX && z == bMinZ) || (x == bMinX && z == bMaxZ) || (x == bMaxX && z == bMaxZ);
    }
    
    public boolean isCubeLocation(UUID world, int x, int y, int z) {
        if(!world.equals(this.world))
            return false;
        
        return (x >= minX && x <= maxX) && (y >= minY && y <= maxY) && (z >= minZ && z <= maxZ);
    }

}

这也是我将边框与立方体分开保存的原因。因为我不希望玩家能够自己打破边界,但又不考虑墙壁(无论如何这不是问题,但我想解释一下)。

问题是每次一个块被破坏时,它都会检查每个 CubeLocation 以检查该位置是否是一个生成器。这意味着每次破坏方块时,它都会每次都进行此检查(这就是我添加 lastKnownGenerator 的原因,它会首先检查这个以查看玩家是否在同一个生成器中破坏方块,因此它没有再次循环)。

然而,如果玩家破坏了不在发电机内的方块(例如在矿井、房屋或 idk 中),这将无济于事,它将对每个破碎的方块进行循环。这还不是最糟糕的部分,最糟糕的是 BlockBreakEvent(当玩家破坏方块时触发的事件)可以被单个玩家每秒触发几十次(如果你的镐、铲子或斧头)不仅如此,它还可以同时被多个玩家触发。

想象一下,世界上总共放置了大约 30k 个立方体,大约有 30 名玩家拥有完整的附魔,每人每秒挖出 20 个方块。这意味着插件每秒检查 600k 坐标。这样不行,太浪费了

您也不能单独保存每个位置或用 block.setMetadata(key, FixedMetadataValue(plugin, value)); 标记一个块;因为你必须循环(假设我们的所有生成器都是 11x11x11,每个生成器 1331 个)30.000 * 1331,这是一个高达 39.930.000 个块,所以它比我正在做的更糟糕。

保存立方体并检查损坏的方块是否在立方体内部的合适方法是什么?我想做的事情完全错了吗?欢迎提出任何建议!

我建议你做多件事:

  1. 少用变量。它不会直接固定项目的数量,但会有助于提高性能。这也主要是一个建议,而不是使代码更快地更具可读性。例如,您可以添加一个中心的位置,然后存储大小。所以你只需要 5 个变量:
public int centerX, centerY, centerZ;
public int sizeBorder, sizeCube;

有了这些,如果你有 30k 个实例,你将节省 7*30000,即 210k 个变量。

  1. 使用块。这真的可以帮助你。 objective是按照chunk来存储cube。

首先,你需要获取块 X/Z :

public static getChunkX(Location loc) {
    rreturn floor(loc.getX()) >> 4;
}

/**
 * NumberConversions#floor(double), used by Location#getBlockX(), Location#getBlockZ()
 *
 * @param num the number to floor
 * @return the floored number
 */
public static int floor(double num) {
    int floor = (int) num;
    return floor == num ? floor : floor - (int) (Double.doubleToRawLongBits(num) >>> 63);
}

(Source)

现在,您应该声明将包含所有内容的变量:

HashMap<Integer, HashMap<Integer, CubeLocation>> cubePerChunk = new HashMap<>();

这是一个 table 的数组 table。想了解更多可以参考this

Location myLoc = ...;
List<CubeLocation> allCubesInChunk = cubePerChunk.get(getChunkX(myLoc)).get(getChunkZ(myLoc));

现在,您可以对所有多维数据集位置执行您所做的操作,但是您将只有该块中的项目,而不是 30k 项目的列表,因此少于 5 个。

要存储值,您应该获取 4 个点的块并尝试将其添加到所有列表中。如果所有点都在同一个块中,您将只存储一次项目,否则它将在它所在的所有块中可用。代码示例:

public void loadCube(CubeLocation loadedCube) {
   for(Location loc : loadedCube.getAllXZPoints()) {
      List<CubeLocation> allCubesInChunk = cubePerChunk.computeIfAbsent(getChunkX(loc), HashMap::new)
             .computeIfAbsent(getChunkZ(loc), ArrayList::new);
      if(!allCubesInChunk.contains(loadedCube)) // if not already added
         allCubesInChunk.add(loadedCube); // add it
   }
}

只是,关于getAllPoints()方法:

只是return所有点数不计入Y的东西,如:

public List<Location> getAllXZPoints() {
    // w is your world
    return Arrays.asList(new Location(w, minX, minY, minZ), new Location(w, maxX, minY, minZ),
    new Location(w, minX, minY, maxZ), new Location(w, minX, minY, maxZ));
}

再给你一些提示 dmf,当你在这里寻找性能时,你有两点可以做,那就是内存优化,基本上你会使用更少的内存,这基本上是 Elikill 已经做到的在这里告诉你^^。

第二件事是你使用 NMS,因为你正在设置(我相信,很多块),最好是你访问你可以访问的最深的 nms 方法,你可以在主龙头上找到一些教程关于如何快速放置方块的网站(或者你可以去看看 FAWE src)。

另一件事是,要检查损坏的方块是否在立方体内部,您所要做的就是获取立方体的最大点,获取立方体的最小点, 并检查块的 x 是否 <= max x, 和相同的过程,对于所有坐标,x y z.

请不要调用 .getAllPoints() 来检查块是否在立方体内部,那将是您可以编写的最糟糕的情况。