| 建立网络其实就是一个图遍历问题:将所有相邻的网络结点存放在同一个网络之中。 那么图遍历问题的一个经典做法就是广度优先遍历(BFS),这里我们也就用BFS来建立网络。
 看到这里的小伙伴们可能就会有疑问:onServerEndTick是用来干什么的呢?
 其实方块加载/卸载都是在一个TICK中的异步过程,而Minecraft框架提供的ServerTickEvents.END_SERVER_TICK则是在一个TICK结束时的同步过程。
 因此,我们用一个队列来存储异步过程中需要进行寻路的结点,在同步过程中进行寻路处理即可!
 声明一个处理队列,并在服务端启动时进行初始化:
 
 之后我们需要在方块加载/卸载时将需要处理的结点放入处理队列中:复制代码private Queue<NetworkNode> queue;
private void onServerStarting(MinecraftServer server) {
    this.queue = new ArrayDeque<>();
}
 接下来就是最重要的寻路过程了,下面先放张图让大家理解一下:复制代码private void onBlockEntityLoad(BlockEntity blockEntity, ServerWorld world) {
    if (blockEntity instanceof BaseBlockEntity BE) {
        NNetworkNode node = NetworkService.INSTANCE.createNetworkNode(BE);
        //新建的网络结点放入处理队列中
        queue.add(node);
    }
}
private void onBlockEntityUnload(BlockEntity blockEntity, ServerWorld world) {
    if (blockEntity instanceof BaseBlockEntity BE) {
        NNetwork network = BE.getNetworkNode().getNetwork();
        NetworkService.INSTANCE.removeNetworkNode(BE.getNetworkNode());
        if (network != null) {
            //网络已损坏,需要将所拥有的全部结点重新处理,放入处理队列中
            queue.addAll(network.getNodes());
            NetworkService.INSTANCE.removeNetwork(network);
        }
    }
}
 
  
 每次对从队列中拿出来的正在处理的结点具体处理步骤如下:由于方块实体一加载就全部放入队列中,因此没有不在队列中的结点;图中黑点不一定是同一轮加入到队列中的结点,可能是之前就已经处理完构建好网络的结点;每轮都需要保证队列所有结点均需处理完。
 
 具体代码如下:判断结点所在位置是否加载,如果未加载则直接跳过(World.getBlockEntity会强制加载所在位置方块实体,需要先判断是否加载,否则会导致加载和卸载事件反复进行!);由于队列中的结点不一定存在所属网络,所以如果结点所属网络为空则创建一个所属网络;对于每个可能的连接方向:
 判断对应方向的位置是否加载,如果未加载则跳过;判断对应方向的位置的方块实体是否是网络方块实体,不是则跳过;获取对应网络方块实体的网络结点,判断其所属网络:
 如果没有所属网络,则将其加入正在处理的结点的网络,并将其加入到处理队列中;如果有所属网络,则将这两个网络合并(后文会阐述如何合并)。
 
 复制代码private void onServerEndTick(MinecraftServer server) {
    boolean flag = false;
    while (!queue.isEmpty()) {
        flag = true;
        NetworkNode cur = queue.remove();
        World world = cur.getBlockEntity().getWorld();
        //如果未加载则直接跳过
        if (!world.isPosLoaded(cur.getPos().getX(), cur.getPos().getZ())) continue;
        if (cur.getNetwork() == null) {
            //如果没有所属网络则创建一个
            Network network = NetworkService.INSTANCE.createNetwork();
            NetworkService.INSTANCE.addNodeToNetwork(cur, network);
        }
        Network curNetwork = cur.getNetwork();
        for (Direction direction : cur.getPossibleConnection()) {
            BlockPos pos = cur.getPos().offset(direction);
            if (world.isPosLoaded(pos.getX(), pos.getZ()) && world.getBlockEntity(pos) instanceof BaseBlockEntity BE) {
                NNetworkNode next = BE.getNetworkNode();
                NNetwork nextNetwork = next.getNetwork();
                if (nextNetwork == null) {
                    //将结点加入到网络中
                    NetworkService.INSTANCE.addNodeToNetwork(next, curNetwork);
                    queue.add(next);
                } else if (curNetwork != nextNetwork) {
                    //合并网络
                    NetworkService.INSTANCE.mergeNetwork(curNetwork, nextNetwork);
                    //记得更新正在处理的所属网络!
                    curNetwork = cur.getNetwork();
                }
            }
        }
    }
    //打印调试信息
    if (flag) {
        LOGGER.info("Path finding finish");
        for (Network network : NetworkService.INSTANCE.getNetworks()) {
            LOGGER.info("Network#{}:", network.getId());
            for (NetworkNode node : network.getNodes()) {
                BlockPos pos = node.getPos();
                LOGGER.info("Node#{}: {}, {}, {}", node.getId(), pos.getX(), pos.getY(), pos.getZ());
            }
        }
    }
}
这段代码中有两个没有出现过的函数,一个是getPossibleConnection,另一个是mergeNetwork。方块加载事件处理保证了每个网络方块实体都拥有一个网络节点;所有需要处理的结点都在处理队列中保证了合并网络后无需更新处理队列。
 我们先关注getPossibleConnection,即所有可能的连接。
 在BaseBlockEntity中添加:
 
 具体子类的处理可以重载getPossibleConnection函数来自定义所有可能的连接。复制代码public List<Direction> getPossibleConnection() {
    return Arrays.asList(Direction.values());
}
public boolean canConnect(Direction direction) {
    return getPossibleConnection().contains(direction);
}
在NetworkNode中添加:
 
 这样就定义完了所有可能的连接。复制代码public List<Direction> getPossibleConnection() {
    return blockEntity.getPossibleConnection();
}
最后是mergeNetwork,即合并两个网络,这里采用一种启发式的方法来高效合并两个网络,即将小的网络合并到大的网络。
 在Network中添加:
 
 在NetworkService中添加:复制代码//调用时保证n1大小大于n2
private static Network mergeInPrior(Network n1, Network n2) {
    //将n2的所有结点的所属网络设置为n1
    for (NetworkNode node : n2.nodes) {
        node.setNetwork(n1);
    }
    //将n2的所有节点加入到n1中
    n1.nodes.addAll(n2.nodes);
    //清空n2
    n2.nodes.clear();
    return n1;
}
//按照大小合并网络
public static Network merge(Network n1, Network n2) {
    if (n1.nodes.size() >= n2.nodes.size()) return mergeInPrior(n1, n2);
    else return mergeInPrior(n2, n1);
}
 至此,你就可以构建自己的网络了!如果对过程不是很理解可以看下图加深一下理解:复制代码public void mergeNetwork(Network n1, Network n2) {
    //n3为合并后的网络
    Network n3 = Network.merge(n1, n2);
    //将被合并的网络删除
    if (n3 == n1) removeNetwork(n2);
    else removeNetwork(n1);
}
 
  
 |