每日算法 — 使用java实现PUBG:安全区收缩算法与动态路径规划

一、毒圈机制:从随机圆到动态博弈

PUBG的核心缩圈机制将玩家从广阔地图驱向狭小决赛区。每轮安全区(Safe Zone)都是前一轮的子集,其中心与半径由算法决定。本文实现:

  • 同心圆收缩模型:基于概率分布的安全区生成
  • 动态规划路径规划:在毒圈压力与时间约束下的最优移动策略
  • 毒圈伤害模型:计算不同路径的累计生存代价

二、安全区生成算法

真实PUBG的安全区生成并非完全随机,而是倾向于覆盖仍有玩家存活的区域,保证对抗性。

import java.util.*;

/**
 * PUBG安全区生成器
 * 地图为二维平面,安全区用圆表示(实际游戏中为椭圆/不规则形,本文简化为圆)
 */
public class SafeZoneGenerator {

    static class Circle {
        double x, y, radius;
        public Circle(double x, double y, double r) { this.x = x; this.y = y; this.radius = r; }

        /** 判断点是否在圆内 */
        boolean contains(double px, double py) {
            return Math.hypot(px - x, py - y) <= radius;
        }

        /** 当前圆面积 */
        double area() { return Math.PI * radius * radius; }
    }

    static class Player {
        double x, y;
        boolean alive = true;
        public Player(double x, double y) { this.x = x; this.y = y; }
    }

    private final Random random;
    private final double mapSize = 8000; // 8km × 8km 地图
    private Circle currentZone;
    private int phase = 0;

    // 各阶段参数:目标面积比例、收缩时间、毒伤害/秒
    private final double[] targetAreaRatios = {0.5, 0.3, 0.15, 0.05, 0.02, 0.005};
    private final double[] shrinkTimes = {300, 240, 180, 120, 90, 60}; // 秒
    private final double[] damages = {0.5, 1.0, 2.0, 4.0, 8.0, 12.0}; // 每秒伤害

    public SafeZoneGenerator(int seed) {
        this.random = new Random(seed);
        // 初始安全区覆盖地图中心大部分区域
        this.currentZone = new Circle(mapSize/2, mapSize/2, mapSize * 0.45);
    }

    /**
     * 生成下一轮安全区
     * 策略:在当前安全区内,偏向存活玩家密集区域采样中心
     */
    public Circle nextZone(List<Player> alivePlayers) {
        if (phase >= targetAreaRatios.length) {
            // 最终圈:极小的随机圆
            return randomCircleIn(currentZone, currentZone.radius * 0.3);
        }

        phase++;
        double targetArea = currentZone.area() * targetAreaRatios[phase - 1];
        double targetRadius = Math.sqrt(targetArea / Math.PI);

        // 计算存活玩家的重心(加权中心)
        double centerX = 0, centerY = 0;
        int count = 0;
        for (Player p : alivePlayers) {
            if (p.alive && currentZone.contains(p.x, p.y)) {
                centerX += p.x;
                centerY += p.y;
                count++;
            }
        }

        if (count > 0) {
            centerX /= count;
            centerY /= count;
        } else {
            centerX = currentZone.x;
            centerY = currentZone.y;
        }

        // 在以玩家重心为中心的分布中采样新圆心
        // 使用截断正态分布:越靠近重心概率越高,但必须保证整个新圆在当前圆内
        Circle next = null;
        int attempts = 0;
        while (next == null && attempts < 1000) {
            attempts++;
            // 新圆心在当前圆内随机偏移,偏移幅度随阶段递减(后期更集中)
            double maxOffset = currentZone.radius * 0.5 / Math.sqrt(phase);
            double offsetAngle = random.nextDouble() * 2 * Math.PI;
            double offsetDist = random.nextDouble() * maxOffset;

            double nx = centerX + Math.cos(offsetAngle) * offsetDist;
            double ny = centerY + Math.sin(offsetAngle) * offsetDist;

            // 确保新圆完全包含在当前圆内
            double distToCenter = Math.hypot(nx - currentZone.x, ny - currentZone.y);
            if (distToCenter + targetRadius <= currentZone.radius) {
                next = new Circle(nx, ny, targetRadius);
            }
        }

        if (next == null) {
            //  fallback:直接在当前圆心处收缩
            next = new Circle(currentZone.x, currentZone.y, targetRadius);
        }

        currentZone = next;
        return next;
    }

    Circle randomCircleIn(Circle parent, double radius) {
        double angle = random.nextDouble() * 2 * Math.PI;
        double dist = random.nextDouble() * (parent.radius - radius);
        return new Circle(parent.x + Math.cos(angle) * dist, 
                         parent.y + Math.sin(angle) * dist, radius);
    }

    /** 获取当前阶段的毒圈伤害速率 */
    public double currentDamageRate() {
        int idx = Math.min(phase, damages.length - 1);
        return damages[idx];
    }

    /** 获取当前阶段收缩剩余时间 */
    public double currentShrinkTime() {
        int idx = Math.min(phase, shrinkTimes.length - 1);
        return shrinkTimes[idx];
    }
}

三、毒圈伤害积分与路径代价模型

玩家在圈外会受到持续伤害。路径规划的核心是:在移动速度约束下,最小化总伤害积分(damage over time)。

/**
 * 毒圈伤害模型与路径代价计算
 */
public class DamageModel {

    /**
     * 计算沿一条直线路径移动时受到的累计毒伤害
     * @param path 路径点序列
     * @param moveSpeed 玩家移动速度(单位/秒)
     * @param zones 各时刻的安全区圆(假设匀速收缩)
     * @param damageRate 当前毒伤害/秒
     * @return 累计伤害值
     */
    public static double calculatePathDamage(List<double[]> path, double moveSpeed,
                                             List<Circle> zones, double damageRate) {
        double totalDamage = 0;
        double currentTime = 0;

        for (int i = 0; i < path.size() - 1; i++) {
            double[] from = path.get(i);
            double[] to = path.get(i + 1);
            double dist = Math.hypot(to[0] - from[0], to[1] - from[1]);
            double duration = dist / moveSpeed;

            // 对路径做数值积分(细分小段近似)
            int segments = Math.max(1, (int)(duration * 2)); // 每0.5秒一个采样点
            for (int s = 0; s < segments; s++) {
                double t = currentTime + duration * s / segments;
                double ratio = (double)s / segments;
                double px = from[0] + (to[0] - from[0]) * ratio;
                double py = from[1] + (to[1] - from[1]) * ratio;

                // 获取t时刻的安全区
                Circle zoneAtT = getZoneAtTime(zones, t);
                if (!zoneAtT.contains(px, py)) {
                    double outsideDist = Math.hypot(px - zoneAtT.x, py - zoneAtT.y) - zoneAtT.radius;
                    // 伤害随距离增加:基础伤害 + 距离惩罚
                    double segmentDamage = (damageRate + outsideDist * 0.01) * (duration / segments);
                    totalDamage += segmentDamage;
                }
            }
            currentTime += duration;
        }

        return totalDamage;
    }

    /**
     * 获取t时刻的安全区(线性插值收缩)
     * 假设zones[i]在shrinkTime[i]秒内匀速收缩到zones[i+1]
     */
    static Circle getZoneAtTime(List<Circle> zones, double t) {
        // 简化:假设每个阶段固定时间,查找对应阶段
        double phaseTime = 300; // 每阶段5分钟
        int phase = Math.min((int)(t / phaseTime), zones.size() - 1);
        double phaseProgress = (t % phaseTime) / phaseTime;

        Circle from = zones.get(phase);
        Circle to = (phase + 1 < zones.size()) ? zones.get(phase + 1) : from;

        double cx = from.x + (to.x - from.x) * phaseProgress;
        double cy = from.y + (to.y - from.y) * phaseProgress;
        double cr = from.radius + (to.radius - from.radius) * phaseProgress;

        return new Circle(cx, cy, cr);
    }
}

四、动态规划最优路径求解

玩家在缩圈过程中需要不断决策:直行入圈?沿圈边移动?等待下一圈位置确认后再动?

本文用网格动态规划求解:将地图离散化为网格,每个状态为(grid_x, grid_y, phase),转移代价为移动伤害。

/**
 * 基于网格的动态规划路径规划器
 * 状态:(x, y, t) 表示t时刻位于网格(x,y)的最小累计伤害
 */
public class PathPlanner {

    private final double gridSize = 100; // 网格大小(米)
    private final double mapSize = 8000;
    private final int gridCount = (int)(mapSize / gridSize);
    private final double moveSpeed = 6.0; // 奔跑速度 6m/s
    private final double runSpeed = 6.0;
    private final double walkSpeed = 3.0;

    /**
     * 动态规划求解最优路径
     * @param start 起始位置 [x, y]
     * @param zones 预知的所有安全区序列
     * @param damageRates 各阶段伤害率
     * @return 最优路径点序列与预计总伤害
     */
    public PathResult planOptimalPath(double[] start, List<Circle> zones, double[] damageRates) {
        // 将起点映射到网格
        int sx = (int)(start[0] / gridSize);
        int sy = (int)(start[1] / gridSize);

        // DP[phase][x][y] = 到达该状态的最小累计伤害
        int phases = zones.size();
        double[][][] dp = new double[phases][gridCount][gridCount];
        int[][][] prevX = new int[phases][gridCount][gridCount];
        int[][][] prevY = new int[phases][gridCount][gridCount];

        for (double[][] plane : dp) {
            for (double[] row : plane) {
                Arrays.fill(row, Double.POSITIVE_INFINITY);
            }
        }

        dp[0][sx][sy] = 0;

        // 按阶段递推
        for (int p = 0; p < phases - 1; p++) {
            double phaseDuration = 300; // 每阶段时长(秒)
            int timeSteps = (int)(phaseDuration / 5); // 每5秒一个决策点

            // 在每个阶段内,用BFS/SPFA扩展可达范围
            for (int step = 0; step < timeSteps; step++) {
                double t = p * 300 + step * 5;
                Circle zone = getInterpolatedZone(zones, p, (double)step / timeSteps);

                double[][] nextDp = new double[gridCount][gridCount];
                for (double[] row : nextDp) Arrays.fill(row, Double.POSITIVE_INFINITY);

                for (int x = 0; x < gridCount; x++) {
                    for (int y = 0; y < gridCount; y++) {
                        if (dp[p][x][y] == Double.POSITIVE_INFINITY) continue;

                        // 尝试向8个方向移动(或停留)
                        int[][] dirs = {{0,0},{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}};
                        for (int[] d : dirs) {
                            int nx = x + d[0], ny = y + d[1];
                            if (nx < 0 || nx >= gridCount || ny < 0 || ny >= gridCount) continue;

                            double dist = Math.hypot(d[0], d[1]) * gridSize;
                            double timeNeeded = dist / runSpeed;
                            if (timeNeeded > 5) continue; // 5秒内无法到达

                            // 计算移动过程中的伤害
                            double worldX = nx * gridSize, worldY = ny * gridSize;
                            double damage = 0;
                            if (!zone.contains(worldX, worldY)) {
                                double outside = Math.hypot(worldX - zone.x, worldY - zone.y) - zone.radius;
                                damage = (damageRates[p] + outside * 0.01) * timeNeeded;
                            }

                            double newCost = dp[p][x][y] + damage;
                            if (newCost < nextDp[nx][ny]) {
                                nextDp[nx][ny] = newCost;
                                prevX[p][nx][ny] = x;
                                prevY[p][nx][ny] = y;
                            }
                        }
                    }
                }

                // 将nextDp同步回dp[p]用于下一步扩展
                for (int x = 0; x < gridCount; x++) {
                    System.arraycopy(nextDp[x], 0, dp[p][x], 0, gridCount);
                }
            }

            // 阶段转移:当前阶段结束时的位置 = 下阶段的起始位置
            for (int x = 0; x < gridCount; x++) {
                for (int y = 0; y < gridCount; y++) {
                    dp[p + 1][x][y] = dp[p][x][y];
                }
            }
        }

        // 回溯最优路径
        return backtrack(dp, prevX, prevY, zones);
    }

    Circle getInterpolatedZone(List<Circle> zones, int phase, double progress) {
        Circle from = zones.get(phase);
        Circle to = (phase + 1 < zones.size()) ? zones.get(phase + 1) : from;
        return new Circle(
            from.x + (to.x - from.x) * progress,
            from.y + (to.y - from.y) * progress,
            from.radius + (to.radius - from.radius) * progress
        );
    }

    PathResult backtrack(double[][][] dp, int[][][] prevX, int[][][] prevY, List<Circle> zones) {
        // 在最后一阶段找到伤害最小的终点
        int lastPhase = zones.size() - 1;
        double minDamage = Double.POSITIVE_INFINITY;
        int bestX = 0, bestY = 0;

        for (int x = 0; x < gridCount; x++) {
            for (int y = 0; y < gridCount; y++) {
                if (dp[lastPhase][x][y] < minDamage) {
                    minDamage = dp[lastPhase][x][y];
                    bestX = x; bestY = y;
                }
            }
        }

        List<double[]> path = new ArrayList<>();
        int cx = bestX, cy = bestY;
        for (int p = lastPhase; p >= 0; p--) {
            path.add(new double[]{cx * gridSize, cy * gridSize});
            if (p > 0) {
                int px = prevX[p-1][cx][cy];
                int py = prevY[p-1][cx][cy];
                cx = px; cy = py;
            }
        }
        Collections.reverse(path);
        return new PathResult(path, minDamage);
    }

    static class PathResult {
        List<double[]> path;
        double totalDamage;
        PathResult(List<double[]> p, double d) { path = p; totalDamage = d; }
    }
}

五、实时决策:当安全区未知时

实际游戏中,下一阶段安全区在收缩完成后才公布。玩家必须在信息不完整的情况下决策。此时采用期望值最小化策略。

/**
 * 不确定安全区下的期望最优策略
 * 用蒙特卡洛模拟评估候选移动方向
 */
public class UncertainZonePlanner {

    private final Random random = new Random();
    private final SafeZoneGenerator zoneGen;

    public UncertainZonePlanner(int seed) {
        this.zoneGen = new SafeZoneGenerator(seed);
    }

    /**
     * 评估当前位置向某个方向移动的期望生存代价
     * 方法:在当前圈内采样大量可能的下一圈位置,计算平均路径伤害
     */
    public double expectedDamage(double[] from, double[] to, Circle currentZone,
                                  List<Player> alivePlayers, double damageRate) {
        int simulations = 100;
        double totalDamage = 0;

        for (int sim = 0; sim < simulations; sim++) {
            // 为每次模拟生成一个可能的下一圈
            Circle nextZone = zoneGen.nextZone(alivePlayers);

            // 简化:假设直接以最短路径前往目标点,再评估剩余时间内的伤害
            List<double[]> path = Arrays.asList(from, to);
            double damage = DamageModel.calculatePathDamage(path, 6.0, 
                Arrays.asList(currentZone, nextZone), damageRate);
            totalDamage += damage;
        }

        return totalDamage / simulations;
    }

    /**
     * 选择最优移动方向(离散角度采样)
     */
    public double[] chooseBestDirection(double[] pos, Circle currentZone,
                                        List<Player> alivePlayers, double damageRate) {
        double bestScore = Double.NEGATIVE_INFINITY;
        double[] bestDir = new double[]{0, 0};

        // 采样16个方向
        for (int i = 0; i < 16; i++) {
            double angle = 2 * Math.PI * i / 16;
            double dist = 200; // 向前移动200米
            double[] target = {pos[0] + Math.cos(angle) * dist, 
                              pos[1] + Math.sin(angle) * dist};

            // 若目标在圈外太远,跳过
            if (!currentZone.contains(target[0], target[1]) && 
                Math.hypot(target[0]-currentZone.x, target[1]-currentZone.y) > currentZone.radius * 1.5) {
                continue;
            }

            double expectedDmg = expectedDamage(pos, target, currentZone, alivePlayers, damageRate);

            // 综合评分:期望伤害越低越好,同时偏向圈中心(决赛圈优势)
            double centerBonus = 1.0 / (1.0 + Math.hypot(target[0] - currentZone.x, 
                                                          target[1] - currentZone.y) / 1000);
            double score = -expectedDmg + centerBonus * 50;

            if (score > bestScore) {
                bestScore = score;
                bestDir = new double[]{Math.cos(angle), Math.sin(angle)};
            }
        }

        return bestDir;
    }
}

六、可视化与策略对比

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class PUBGVisualizer extends JPanel {
    private final List<Circle> zones;
    private final List<double[]> optimalPath;
    private final List<double[]> naivePath;

    public PUBGVisualizer(List<Circle> zones, List<double[]> optPath, List<double[]> naivePath) {
        this.zones = zones;
        this.optimalPath = optPath;
        this.naivePath = naivePath;
        setPreferredSize(new Dimension(800, 800));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        double scale = 800.0 / 8000.0;

        // 绘制所有安全区(半透明同心圆)
        for (int i = 0; i < zones.size(); i++) {
            Circle z = zones.get(i);
            int r = (int)(z.radius * scale);
            int cx = (int)(z.x * scale);
            int cy = (int)(z.y * scale);
            g.setColor(new Color(0, 255, 0, 30 + i * 10));
            g.fillOval(cx - r, cy - r, r * 2, r * 2);
            g.setColor(Color.GREEN);
            g.drawOval(cx - r, cy - r, r * 2, r * 2);
        }

        // 绘制最优路径(蓝色)
        g.setColor(Color.BLUE);
        for (int i = 0; i < optimalPath.size() - 1; i++) {
            int x1 = (int)(optimalPath.get(i)[0] * scale);
            int y1 = (int)(optimalPath.get(i)[1] * scale);
            int x2 = (int)(optimalPath.get(i+1)[0] * scale);
            int y2 = (int)(optimalPath.get(i+1)[1] * scale);
            g.drawLine(x1, y1, x2, y2);
        }

        // 绘制 naive 直线路径(红色)
        g.setColor(Color.RED);
        for (int i = 0; i < naivePath.size() - 1; i++) {
            int x1 = (int)(naivePath.get(i)[0] * scale);
            int y1 = (int)(naivePath.get(i)[1] * scale);
            int x2 = (int)(naivePath.get(i+1)[0] * scale);
            int y2 = (int)(naivePath.get(i+1)[1] * scale);
            g.drawLine(x1, y1, x2, y2);
        }
    }

    public static void main(String[] args) {
        int seed = 42;
        SafeZoneGenerator gen = new SafeZoneGenerator(seed);

        // 模拟100个随机分布的玩家
        List<Player> players = new ArrayList<>();
        Random rnd = new Random(seed);
        for (int i = 0; i < 100; i++) {
            players.add(new Player(rnd.nextDouble() * 8000, rnd.nextDouble() * 8000));
        }

        // 预生成6轮安全区
        List<Circle> zones = new ArrayList<>();
        zones.add(gen.currentZone);
        for (int i = 0; i < 5; i++) {
            zones.add(gen.nextZone(players));
        }

        // 起点和终点
        double[] start = {1000, 1000};
        double[] goal = {zones.get(zones.size()-1).x, zones.get(zones.size()-1).y};

        // 最优路径规划
        PathPlanner planner = new PathPlanner();
        double[] damageRates = {0.5, 1.0, 2.0, 4.0, 8.0, 12.0};
        PathResult optResult = planner.planOptimalPath(start, zones, damageRates);

        // Naive路径:每轮直接朝圈中心走
        List<double[]> naivePath = new ArrayList<>();
        naivePath.add(start);
        for (Circle z : zones) {
            naivePath.add(new double[]{z.x, z.y});
        }

        // 对比伤害
        double optDamage = DamageModel.calculatePathDamage(optResult.path, 6.0, zones, 2.0);
        double naiveDamage = DamageModel.calculatePathDamage(naivePath, 6.0, zones, 2.0);

        System.out.println("Optimal path damage: " + optDamage);
        System.out.println("Naive path damage: " + naiveDamage);
        System.out.println("Improvement: " + (naiveDamage - optDamage) / naiveDamage * 100 + "%");

        // 显示
        JFrame frame = new JFrame("PUBG Zone Strategy");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new PUBGVisualizer(zones, optResult.path, naivePath));
        frame.pack();
        frame.setVisible(true);
    }
}

七、复杂度分析

模块 时间复杂度 空间复杂度 说明
安全区生成 O(P + attempts) O(1) P=存活玩家数
路径伤害积分 O(S·segments) O(1) S=路径段数
网格动态规划 O(phases·timesteps·G²·9) O(phases·G²) G=网格数(80)
蒙特卡洛期望 O(simulations·path_eval) O(1) sim=100
整体路径规划 O(phases·G²) ~ 毫秒级 O(phases·G²) 80×80网格极快

核心收获:
1. 安全区生成 = 有偏随机采样:偏向玩家密集区保证对抗,截断分布保证可行性
2. 毒圈路径 = 动态规划:离散化时空状态后,DP天然适合求解多阶段决策
3. 不确定信息 = 蒙特卡洛期望:当未来圈位置未知时,大量采样求期望最优
4. 毒伤害积分:数值积分将连续移动代价转化为可计算的DP转移

PUBG的缩圈机制用最简单的几何(同心圆)创造了最复杂的生存博弈——你永远不知道下一圈会逼你面对谁。