每日算法 — 使用java实现钓鱼游戏:抛体运动模拟与概率最优收线策略

钓鱼游戏是休闲游戏中经久不衰的题材,玩家通过控制抛竿角度与力度,将鱼钩抛入水中,待鱼咬钩后通过收线操作将鱼钓起。本文用Java实现钓鱼游戏的核心物理引擎与AI策略,重点讲解抛体运动轨迹计算、浮漂物理模拟、鱼群行为建模以及基于概率的最优收线决策算法。

一、核心算法模块

钓鱼游戏可拆解为以下算法问题:

  1. 抛体运动:根据抛竿角度与力度计算鱼钩飞行轨迹
  2. 浮漂物理:水面的阻尼振荡与波浪影响
  3. 鱼群AI:鱼的游动路径规划与咬钩判定
  4. 收线策略:张力控制与鱼体力消耗的最优决策

二、抛体运动学模型

鱼钩抛出后做斜抛运动,受重力与空气阻力影响。简化模型下:

$$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:巡逻 → 被吸引 → 咬钩 → 挣扎
  • 最优收线策略:体力消耗与断线风险的动态平衡