每日算法 — 使用java实现大鱼吃小鱼:贪心增长策略与AABB碰撞检测

引言

大鱼吃小鱼是经典的休闲益智游戏,玩家控制一条小鱼在海洋中不断吞噬比自己小的鱼类来成长,同时躲避比自己大的捕食者。本文将用Java实现一个带AI决策的大鱼吃小鱼游戏,重点讲解贪心最优目标选择算法与AABB轴对齐包围盒碰撞检测的核心逻辑,并引入动态难度机制提升游戏挑战性。

核心算法设计

1. 贪心最优目标选择

AI小鱼需要在视野范围内实时评估所有潜在目标,选择”收益最大、风险最小”的猎物。贪心策略的核心是定义一个综合评分函数:

score(目标) = 体型收益 × 距离衰减系数 - 被捕食风险 ×  proximityPenalty
  • 体型收益:目标与自身的体积差,越大则成长收益越高
  • 距离衰减系数:距离越近,追捕成本越低
  • 被捕食风险:视野内比自己大的鱼与目标的距离

2. AABB碰撞检测

轴对齐包围盒(Axis-Aligned Bounding Box)是游戏碰撞检测的高效方案。由于所有鱼的碰撞体为圆形,我们用外接正方形作为AABB初筛,再通过圆心距离做精确检测:

  • 粗检测:判断两个AABB是否相交
  • 精检测:计算两圆圆心距离,若小于半径之和则发生碰撞

3. 动态难度调整

随着玩家成长,系统动态调整AI鱼类的数量、体型分布和追击激进程度,保持游戏平衡性。

完整Java实现

import java.util.*;

/**
 * 大鱼吃小鱼 - 核心算法实现
 * 包含贪心目标选择、AABB碰撞检测与动态难度系统
 */
public class BigFishGame {

    // 游戏常量
    private static final double VIEW_RADIUS = 200.0;      // AI视野半径
    private static final double SAFE_MARGIN = 50.0;       // 安全边界
    private static final double GROWTH_RATE = 0.15;       // 吞噬后的成长比例

    /**
     * 鱼类实体类
     */
    public static class Fish {
        int id;
        double x, y;          // 位置坐标
        double radius;        // 体型半径(决定等级)
        double speed;         // 移动速度
        boolean isPlayer;     // 是否玩家控制
        boolean alive = true; // 存活状态

        // AABB包围盒(实时更新)
        double minX, minY, maxX, maxY;

        public Fish(int id, double x, double y, double radius, boolean isPlayer) {
            this.id = id;
            this.x = x;
            this.y = y;
            this.radius = radius;
            this.isPlayer = isPlayer;
            this.speed = Math.max(1.0, 10.0 / (radius * 0.3 + 1)); // 小鱼更灵活
            updateAABB();
        }

        /** 更新AABB包围盒 */
        void updateAABB() {
            this.minX = x - radius;
            this.minY = y - radius;
            this.maxX = x + radius;
            this.maxY = y + radius;
        }

        /** 移动到新位置 */
        void move(double dx, double dy, double boundsW, double boundsH) {
            this.x = Math.max(radius, Math.min(boundsW - radius, x + dx));
            this.y = Math.max(radius, Math.min(boundsH - radius, y + dy));
            updateAABB();
        }

        /** 吞噬目标后成长 */
        void grow(Fish prey) {
            double area = Math.PI * radius * radius + Math.PI * prey.radius * prey.radius * GROWTH_RATE;
            this.radius = Math.sqrt(area / Math.PI);
            this.speed = Math.max(1.0, 10.0 / (radius * 0.3 + 1));
            prey.alive = false;
        }
    }

    /**
     * AABB粗检测:判断两个包围盒是否相交
     * 时间复杂度 O(1),用于快速排除不可能碰撞的目标
     */
    public static boolean aabbIntersect(Fish a, Fish b) {
        return a.maxX >= b.minX && a.minX <= b.maxX
            && a.maxY >= b.minY && a.minY <= b.maxY;
    }

    /**
     * 精确圆形碰撞检测
     * 在AABB粗检测通过后执行,确保准确性
     */
    public static boolean circleCollide(Fish a, Fish b) {
        double dx = a.x - b.x;
        double dy = a.y - b.y;
        double distSq = dx * dx + dy * dy;
        double radiusSum = a.radius + b.radius;
        return distSq <= radiusSum * radiusSum;
    }

    /**
     * 贪心最优目标选择算法
     * 为AI鱼在视野内选择评分最高的目标
     * @param predator 捕食者自身
     * @param allFish  场景中所有鱼类
     * @return 最优目标方向向量 [dx, dy]
     */
    public static double[] greedyTargetSelect(Fish predator, List<Fish> allFish) {
        Fish bestTarget = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Fish target : allFish) {
            if (target == predator || !target.alive) continue;

            double dist = Math.hypot(target.x - predator.x, target.y - predator.y);
            if (dist > VIEW_RADIUS) continue; // 超出视野范围

            // 只能吃比自己小的
            if (target.radius >= predator.radius * 0.95) continue;

            // 计算距离衰减:越近越好(使用反比衰减)
            double distanceDecay = 1.0 / (1.0 + dist / 50.0);

            // 体型收益:体积差越大,收益越高
            double sizeBenefit = predator.radius - target.radius;

            // 被捕食风险:视野内是否存在比自己大的鱼靠近
            double risk = 0;
            for (Fish threat : allFish) {
                if (threat == predator || !threat.alive || threat.radius <= predator.radius) continue;
                double threatDist = Math.hypot(threat.x - predator.x, threat.y - predator.y);
                if (threatDist < VIEW_RADIUS) {
                    // 风险与威胁距离成反比,与体型差成正比
                    risk += (threat.radius - predator.radius) / (1.0 + threatDist / 30.0);
                }
            }

            // 综合评分:收益 × 距离衰减 - 风险
            double score = sizeBenefit * distanceDecay - risk * 0.5;

            // 逃跑机制:如果有大得多的威胁很近,优先逃跑
            if (risk > predator.radius * 2) {
                score -= risk * 10; // 大幅降权
            }

            if (score > bestScore) {
                bestScore = score;
                bestTarget = target;
            }
        }

        // 返回移动方向
        if (bestTarget != null) {
            double dx = bestTarget.x - predator.x;
            double dy = bestTarget.y - predator.y;
            double len = Math.hypot(dx, dy);
            if (len > 0) {
                return new double[]{dx / len * predator.speed, dy / len * predator.speed};
            }
        }

        // 无目标时随机游动
        double angle = Math.random() * Math.PI * 2;
        return new double[]{Math.cos(angle) * predator.speed * 0.3,
                           Math.sin(angle) * predator.speed * 0.3};
    }

    /**
     * 逃跑方向计算:远离最近的威胁
     */
    public static double[] escapeDirection(Fish self, List<Fish> allFish) {
        double escapeX = 0, escapeY = 0;
        for (Fish f : allFish) {
            if (f == self || !f.alive || f.radius <= self.radius * 1.05) continue;
            double dist = Math.hypot(f.x - self.x, f.y - self.y);
            if (dist < VIEW_RADIUS * 0.6) {
                // 反方向逃离,距离越近逃离意愿越强
                double weight = (VIEW_RADIUS * 0.6 - dist) / (VIEW_RADIUS * 0.6);
                escapeX -= (f.x - self.x) / dist * weight;
                escapeY -= (f.y - self.y) / dist * weight;
            }
        }
        double len = Math.hypot(escapeX, escapeY);
        if (len > 0) {
            return new double[]{escapeX / len * self.speed, escapeY / len * self.speed};
        }
        return new double[]{0, 0};
    }

    /**
     * 处理所有碰撞事件
     * 先AABB粗筛,再精确圆形检测,最后执行吞噬逻辑
     */
    public static void processCollisions(List<Fish> fishes) {
        int n = fishes.size();
        for (int i = 0; i < n; i++) {
            Fish a = fishes.get(i);
            if (!a.alive) continue;
            for (int j = i + 1; j < n; j++) {
                Fish b = fishes.get(j);
                if (!b.alive) continue;

                // 双层检测:AABB粗筛 + 圆形精检
                if (aabbIntersect(a, b) && circleCollide(a, b)) {
                    // 大的吃小的
                    if (a.radius > b.radius * 1.1) {
                        a.grow(b);
                    } else if (b.radius > a.radius * 1.1) {
                        b.grow(a);
                    }
                    // 体型接近则相互弹开(简化处理)
                }
            }
        }
    }

    /**
     * 动态难度调整系统
     * 根据玩家当前体型动态生成新鱼类的参数分布
     */
    public static class DifficultyManager {
        double playerRadius = 10.0;
        int spawnCount = 0;

        /** 根据玩家体型生成新鱼 */
        Fish spawnFish(Random rand, double boundsW, double boundsH) {
            double r = rand.nextDouble();
            double radius;
            if (r < 0.5) {
                // 50% 生成比玩家小很多的鱼(食物)
                radius = playerRadius * (0.2 + rand.nextDouble() * 0.4);
            } else if (r < 0.8) {
                // 30% 生成与玩家体型接近的鱼(竞争者)
                radius = playerRadius * (0.8 + rand.nextDouble() * 0.4);
            } else {
                // 20% 生成比玩家大的鱼(威胁)
                radius = playerRadius * (1.3 + rand.nextDouble() * 1.0);
            }
            double x = rand.nextDouble() * boundsW;
            double y = rand.nextDouble() * boundsH;
            return new Fish(++spawnCount, x, y, Math.max(3.0, radius), false);
        }

        void updatePlayerRadius(double radius) {
            this.playerRadius = radius;
        }
    }

    /**
     * 游戏主循环模拟(一帧的逻辑)
     */
    public static void simulateFrame(List<Fish> fishes, double boundsW, double boundsH,
                                     DifficultyManager diffMgr) {
        // 1. AI决策移动
        for (Fish f : fishes) {
            if (!f.alive || f.isPlayer) continue;

            double[] escape = escapeDirection(f, fishes);
            if (Math.hypot(escape[0], escape[1]) > 0.1) {
                // 优先逃跑
                f.move(escape[0], escape[1], boundsW, boundsH);
            } else {
                // 否则贪心追击
                double[] dir = greedyTargetSelect(f, fishes);
                f.move(dir[0], dir[1], boundsW, boundsH);
            }
        }

        // 2. 处理碰撞与吞噬
        processCollisions(fishes);

        // 3. 清理死亡对象并补充新鱼
        fishes.removeIf(f -> !f.alive);
        Random rand = new Random();
        while (fishes.size() < 15) {
            fishes.add(diffMgr.spawnFish(rand, boundsW, boundsH));
        }
    }

    // 演示入口
    public static void main(String[] args) {
        List<Fish> fishes = new ArrayList<>();
        Fish player = new Fish(1, 400, 300, 10.0, true);
        fishes.add(player);

        DifficultyManager diff = new DifficultyManager();
        Random rand = new Random();
        for (int i = 0; i < 14; i++) {
            fishes.add(diff.spawnFish(rand, 800, 600));
        }

        System.out.println("=== 大鱼吃小鱼 算法模拟开始 ===");
        for (int frame = 1; frame <= 20; frame++) {
            simulateFrame(fishes, 800, 600, diff);
            diff.updatePlayerRadius(player.radius);
            System.out.printf("Frame %d: 玩家半径=%.2f, 存活鱼数=%d%n",
                frame, player.radius, fishes.size());
        }
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
AABB粗检测 O(1) O(1) 仅需4次比较
圆形精确检测 O(1) O(1) 距离平方比较,避免开方
贪心目标选择 O(n²) O(1) n为视野内鱼数,含风险扫描
全局碰撞处理 O(n²) O(1) 双重循环+AABB快速排除
帧模拟 O(n²) O(n) n为场景中鱼的总数

算法亮点总结

  • 双层碰撞检测:AABB粗筛排除大部分不可能碰撞的对象,仅在必要时进行精确圆碰撞计算,兼顾效率与准确性。
  • 贪心策略+风险评估:不仅追求最大收益,还实时评估被捕食风险,使AI行为更智能、更像真实生物。
  • 动态难度平衡:spawn机制根据玩家体型自适应调整,确保游戏全程具有挑战性而不会过于简单或困难。

通过这套算法框架,可以进一步扩展多人对战模式、不同海域生态(体型上限差异)、以及基于遗传算法的AI进化系统等高级功能。