钓鱼游戏是休闲游戏中经久不衰的题材,玩家通过控制抛竿角度与力度,将鱼钩抛入水中,待鱼咬钩后通过收线操作将鱼钓起。本文用Java实现钓鱼游戏的核心物理引擎与AI策略,重点讲解抛体运动轨迹计算、浮漂物理模拟、鱼群行为建模以及基于概率的最优收线决策算法。
一、核心算法模块
钓鱼游戏可拆解为以下算法问题:
- 抛体运动:根据抛竿角度与力度计算鱼钩飞行轨迹
- 浮漂物理:水面的阻尼振荡与波浪影响
- 鱼群AI:鱼的游动路径规划与咬钩判定
- 收线策略:张力控制与鱼体力消耗的最优决策
二、抛体运动学模型
鱼钩抛出后做斜抛运动,受重力与空气阻力影响。简化模型下:
$$x(t) = v_0 \cos(\theta) \cdot t$$
$$y(t) = v_0 \sin(\theta) \cdot t – \frac{1}{2} g t^2$$
其中 $v_0$ 为初速度,$\theta$ 为抛射角,$g$ 为重力加速度。
入水时刻 $t_{water}$ 满足 $y(t_{water}) = 0$:
$$t_{water} = \frac{2 v_0 \sin(\theta)}{g}$$
入水后的水平距离:
$$x_{water} = \frac{v_0^2 \sin(2\theta)}{g}$$
三、Java完整实现
import java.util.*;
/**
* 钓鱼游戏核心算法实现
* 包含抛体运动、浮漂物理、鱼群AI与最优收线策略
*/
public class FishingGame {
// 物理常量
private static final double GRAVITY = 9.8; // 重力加速度 m/s^2
private static final double WATER_LEVEL = 0; // 水面Y坐标
private static final double AIR_DENSITY = 1.225; // 空气密度
private static final double WATER_DENSITY = 1000; // 水密度
private static final double DRAG_COEFF = 0.47; // 球体阻力系数
// 鱼钩属性
private static final double HOOK_MASS = 0.01; // 鱼钩质量 kg
private static final double HOOK_RADIUS = 0.005; // 鱼钩半径 m
private static final double LINE_TENSION_MAX = 50; // 鱼线最大张力 N
// 游戏场景
private static final double POND_WIDTH = 50; // 池塘宽度 m
private static final double POND_DEPTH = 5; // 池塘深度 m
private Random random = new Random();
private List<Fish> fishes = new ArrayList<>();
private Hook hook;
private double gameTime = 0;
/**
* 向量工具类
*/
static class Vector2D {
double x, y;
Vector2D(double x, double y) { this.x = x; this.y = y; }
Vector2D add(Vector2D v) { return new Vector2D(x + v.x, y + v.y); }
Vector2D mul(double s) { return new Vector2D(x * s, y * s); }
double length() { return Math.sqrt(x * x + y * y); }
Vector2D normalize() {
double len = length();
return len > 0 ? new Vector2D(x / len, y / len) : new Vector2D(0, 0);
}
@Override
public String toString() { return String.format("(%.3f, %.3f)", x, y); }
}
/**
* 鱼钩实体
*/
static class Hook {
Vector2D pos; // 位置
Vector2D vel; // 速度
boolean inWater; // 是否在水中
double lineLength; // 放出线长
double bobberPhase; // 浮漂振荡相位
Fish hookedFish; // 当前钩住的鱼
Hook() {
this.pos = new Vector2D(0, 1.5); // 竿尖高度1.5m
this.vel = new Vector2D(0, 0);
this.inWater = false;
this.lineLength = 0;
this.bobberPhase = 0;
this.hookedFish = null;
}
}
/**
* 鱼类实体
*/
static class Fish {
String species; // 鱼种
Vector2D pos; // 位置
Vector2D vel; // 速度
double stamina; // 体力值 0-100
double weight; // 重量 kg
double strength; // 力量系数
double awareness; // 警觉度 0-1
boolean biting; // 是否正在咬钩
double biteTimer; // 咬钩计时器
Vector2D targetPos; // 目标游动位置
Fish(String species, double x, double y, double weight) {
this.species = species;
this.pos = new Vector2D(x, y);
this.vel = new Vector2D(0, 0);
this.stamina = 100;
this.weight = weight;
this.strength = weight * 2;
this.awareness = 0.3 + random.nextDouble() * 0.5;
this.biting = false;
this.biteTimer = 0;
this.targetPos = new Vector2D(x, y);
}
/**
* 鱼的自然游动AI:随机目标点 + 避障
*/
void update(double dt, Vector2D hookPos, boolean baitInWater) {
// 1. 选择目标点
if (random.nextDouble() < 0.02 || pos.add(targetPos.mul(-1)).length() < 0.5) {
// 随机新目标,但鱼倾向于在底层活动
double tx = random.nextDouble() * POND_WIDTH;
double ty = -random.nextDouble() * POND_DEPTH * 0.8;
// 若鱼饵在水中且鱼不太警觉,有一定概率被吸引
if (baitInWater && random.nextDouble() > awareness) {
double attractRange = 5.0;
if (pos.add(hookPos.mul(-1)).length() < attractRange) {
tx = hookPos.x + (random.nextDouble() - 0.5) * 2;
ty = hookPos.y + (random.nextDouble() - 0.5) * 2;
}
}
targetPos = new Vector2D(tx, ty);
}
// 2. 向目标点游动
Vector2D dir = targetPos.add(pos.mul(-1)).normalize();
double speed = 0.5 + strength * 0.1; // 基础游速
if (stamina < 30) speed *= 0.5; // 体力低时减速
vel = vel.add(dir.mul(speed * dt)).mul(0.95); // 加阻尼
pos = pos.add(vel.mul(dt));
// 3. 边界约束
pos.x = Math.max(0, Math.min(POND_WIDTH, pos.x));
pos.y = Math.max(-POND_DEPTH, Math.min(WATER_LEVEL - 0.1, pos.y));
// 4. 体力自然恢复(当不激烈挣扎时)
if (!biting && stamina < 100) {
stamina += dt * 2;
}
}
}
FishingGame() {
this.hook = new Hook();
// 初始化鱼群
fishes.add(new Fish("鲫鱼", 10, -1, 0.3));
fishes.add(new Fish("鲤鱼", 25, -2, 1.2));
fishes.add(new Fish("草鱼", 35, -1.5, 2.5));
fishes.add(new Fish("青鱼", 15, -3, 4.0));
fishes.add(new Fish("鲈鱼", 30, -0.8, 0.8));
}
/**
* 抛竿操作:计算鱼钩初速度并抛出
*/
void cast(double angleDeg, double power) {
double angle = Math.toRadians(angleDeg);
double v0 = power * 15; // 力度映射到初速度
hook.vel = new Vector2D(
v0 * Math.cos(angle),
v0 * Math.sin(angle)
);
hook.inWater = false;
hook.hookedFish = null;
hook.lineLength = 0;
System.out.printf("抛竿:角度%.1f°,力度%.1f,初速度%s%n",
angleDeg, power, hook.vel);
}
/**
* 更新物理状态
*/
void update(double dt) {
gameTime += dt;
// 1. 更新鱼钩物理
updateHookPhysics(dt);
// 2. 更新鱼群
for (Fish fish : fishes) {
fish.update(dt, hook.pos, hook.inWater);
}
// 3. 咬钩检测
checkBite();
// 4. 收线张力计算
if (hook.hookedFish != null) {
updateTension(dt);
}
}
/**
* 鱼钩物理更新
*/
void updateHookPhysics(double dt) {
if (!hook.inWater) {
// 空气中:受重力与空气阻力
double v = hook.vel.length();
double dragForce = 0.5 * AIR_DENSITY * v * v * DRAG_COEFF *
Math.PI * HOOK_RADIUS * HOOK_RADIUS;
Vector2D drag = hook.vel.normalize().mul(-dragForce / HOOK_MASS * dt);
hook.vel.y -= GRAVITY * dt;
hook.vel = hook.vel.add(drag);
hook.pos = hook.pos.add(hook.vel.mul(dt));
// 入水检测
if (hook.pos.y <= WATER_LEVEL) {
hook.inWater = true;
hook.pos.y = WATER_LEVEL;
// 入水速度衰减
hook.vel = hook.vel.mul(0.3);
System.out.printf("入水!位置:%.2f, 入水速度:%.2f m/s%n",
hook.pos.x, v);
}
} else {
// 水中:受浮力、水阻与线拉力
updateHookInWater(dt);
}
// 更新线长
double distFromOrigin = hook.pos.length();
if (distFromOrigin > hook.lineLength) {
hook.lineLength = distFromOrigin;
}
}
/**
* 水中鱼钩物理:浮漂效果与下沉
*/
void updateHookInWater(double dt) {
// 浮力 = 排水体积 * 水密度 * g
double buoyancy = (4.0/3.0) * Math.PI * HOOK_RADIUS * HOOK_RADIUS * HOOK_RADIUS
* WATER_DENSITY * GRAVITY;
double netForceY = buoyancy - HOOK_MASS * GRAVITY;
// 水阻(与速度成正比)
double dragX = -hook.vel.x * 5.0;
double dragY = -hook.vel.y * 5.0;
hook.vel.x += (dragX / HOOK_MASS) * dt;
hook.vel.y += ((netForceY + dragY) / HOOK_MASS) * dt;
// 浮漂振荡:正弦波模拟水面波动
hook.bobberPhase += dt * 3;
double waveY = Math.sin(hook.bobberPhase) * 0.05;
hook.pos = hook.pos.add(hook.vel.mul(dt));
hook.pos.y = Math.min(WATER_LEVEL + waveY, hook.pos.y);
}
/**
* 检测是否有鱼咬钩
*/
void checkBite() {
if (!hook.inWater || hook.hookedFish != null) return;
for (Fish fish : fishes) {
if (fish.biting) continue;
double dist = fish.pos.add(hook.pos.mul(-1)).length();
double biteRange = 0.3; // 咬钩范围 30cm
if (dist < biteRange && random.nextDouble() < 0.05) {
fish.biting = true;
fish.biteTimer = gameTime;
hook.hookedFish = fish;
System.out.printf("🎣 %s咬钩了!重量%.1fkg%n", fish.species, fish.weight);
break;
}
}
}
/**
* 收线操作与张力计算
*/
void reel(double amount) {
if (hook.hookedFish == null) {
// 无鱼时简单收线
hook.lineLength = Math.max(0, hook.lineLength - amount);
return;
}
Fish fish = hook.hookedFish;
Vector2D toHook = hook.pos.add(fish.pos.mul(-1));
double dist = toHook.length();
// 收线拉近鱼
double newDist = Math.max(0.5, dist - amount);
Vector2D direction = toHook.normalize();
fish.pos = hook.pos.add(direction.mul(-newDist));
// 鱼的反抗:向远离钩的方向游
Vector2D escapeDir = direction.mul(-1);
fish.vel = escapeDir.mul(fish.strength * 0.5);
fish.stamina -= amount * fish.strength;
System.out.printf("收线%.2fm,鱼体力%.1f,距离%.2fm%n",
amount, fish.stamina, newDist);
}
/**
* 更新鱼线张力
*/
void updateTension(double dt) {
Fish fish = hook.hookedFish;
if (fish.stamina <= 0) {
System.out.printf("✅ %s体力耗尽,成功钓起!%n", fish.species);
fishes.remove(fish);
hook.hookedFish = null;
return;
}
// 若鱼线张力超过极限则断线
Vector2D diff = hook.pos.add(fish.pos.mul(-1));
double tension = diff.length() * fish.strength;
if (tension > LINE_TENSION_MAX) {
System.out.printf("💔 鱼线断裂!%s逃脱了%n", fish.species);
fish.biting = false;
hook.hookedFish = null;
}
}
/**
* 最优收线策略:根据鱼的状态决定收线节奏
* 策略:鱼体力高时保守,体力低时激进
*/
double calculateOptimalReelAmount(Fish fish) {
if (fish == null) return 0.5;
// 基础收线量
double baseAmount = 0.3;
// 根据体力调整:体力越低,收线越激进
double staminaFactor = (100 - fish.stamina) / 100.0;
double amount = baseAmount + staminaFactor * 0.7;
// 安全边界:确保不会导致断线
double safeLimit = LINE_TENSION_MAX / (fish.strength * 2);
return Math.min(amount, safeLimit);
}
/**
* 运行模拟
*/
void simulate() {
System.out.println("=== 钓鱼游戏模拟开始 ===\n");
// 抛竿
cast(45, 0.8);
// 模拟飞行与入水
for (int i = 0; i < 500; i++) {
update(0.02);
if (i % 50 == 0 && !hook.inWater) {
System.out.printf("飞行中:位置%s,速度%.2f m/s%n",
hook.pos, hook.vel.length());
}
}
// 等待咬钩
System.out.println("\n等待鱼儿上钩...");
for (int i = 0; i < 1000 && hook.hookedFish == null; i++) {
update(0.02);
}
// 收线战斗
if (hook.hookedFish != null) {
System.out.println("\n开始收线!");
Fish fish = hook.hookedFish;
for (int i = 0; i < 200 && hook.hookedFish != null; i++) {
update(0.02);
double amount = calculateOptimalReelAmount(fish);
if (i % 10 == 0) {
reel(amount);
}
}
}
System.out.println("\n=== 模拟结束 ===");
}
public static void main(String[] args) {
FishingGame game = new FishingGame();
game.simulate();
}
}
四、抛体运动的最优角度
对于固定初速度 $v_0$,最大射程对应的角度为 $45°$(忽略空气阻力)。考虑空气阻力时,最优角度略小于 $45°$,可通过数值方法求解:
double findOptimalAngle(double v0, double targetDistance) {
double bestAngle = 45;
double minError = Double.MAX_VALUE;
for (double angle = 20; angle <= 60; angle += 0.5) {
double distance = simulateFlight(v0, angle);
double error = Math.abs(distance - targetDistance);
if (error < minError) {
minError = error;
bestAngle = angle;
}
}
return bestAngle;
}
五、收线策略的概率模型
将收线过程建模为双人零和博弈:玩家选择收线力度,鱼选择挣扎强度。最优策略可通过求解纳什均衡获得。简化模型下:
$$\text{收益} = \text{收线进度} – \lambda \cdot \text{断线风险}$$
其中 $\lambda$ 为风险厌恶系数。贪心最优策略即每一步选择使期望收益最大化的收线力度。
六、总结
本文完整实现了钓鱼游戏的物理引擎与AI系统。从抛体运动到鱼群行为,每个模块都体现了游戏开发中算法的重要性。核心收获包括:
- 斜抛运动公式与入水点预测
- 水中物体的浮力、阻尼与波浪模拟
- 基于状态机的鱼群AI:巡逻 → 被吸引 → 咬钩 → 挣扎
- 最优收线策略:体力消耗与断线风险的动态平衡