引言
大鱼吃小鱼是经典的休闲益智游戏,玩家控制一条小鱼在海洋中不断吞噬比自己小的鱼类来成长,同时躲避比自己大的捕食者。本文将用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进化系统等高级功能。