每日算法 — 使用java实现拳击:物理碰撞模拟与决策树AI最优打击策略

引言

拳击是最古老的竞技运动之一,其核心在于出拳时机、角度选择与防守策略。本文将用Java实现一个简化版拳击模拟系统,涵盖物理碰撞检测、打击力计算与基于决策树的AI对手。读者将理解如何将刚体物理、AABB碰撞与决策树融合到游戏AI中。

一、物理模型设计

1.1 拳击手状态定义

拳击手在二维平面内移动,状态包括位置、速度、体力与姿态:

public class Boxer {
    // 位置与速度
    public double x, y;
    public double vx, vy;

    // 身体属性
    public double mass;        // 质量(kg)
    public double reach;       // 臂展(m)
    public double stamina;     // 体力 0-100
    public double health;      // 生命值 0-100

    // 姿态枚举
    public enum Stance { GUARD, JAB, HOOK, UPPERCUT, DODGE_LEFT, DODGE_RIGHT }
    public Stance currentStance = Stance.GUARD;

    // 攻击冷却
    public int attackCooldown = 0;

    public Boxer(double x, double y, double mass, double reach) {
        this.x = x; this.y = y;
        this.mass = mass;
        this.reach = reach;
        this.stamina = 100;
        this.health = 100;
    }
}

1.2 打击碰撞检测

每次出拳生成一个AABB碰撞框,检测是否与对手躯干或头部相交:

public class Punch {
    public double x, y, width, height;
    public double damage;      // 基础伤害
    public double forceX, forceY; // 冲击力向量
    public PunchType type;

    public enum PunchType { JAB, CROSS, HOOK, UPPERCUT }

    public boolean intersects(Boxer opponent) {
        // 对手受击区域: 以对手位置为中心的区域
        double oppLeft = opponent.x - 0.3;
        double oppRight = opponent.x + 0.3;
        double oppBottom = opponent.y;
        double oppTop = opponent.y + 1.8;

        return x < oppRight && x + width > oppLeft &&
               y < oppTop && y + height > oppBottom;
    }
}

1.3 冲量与动量守恒

击中时根据动量定理计算受击位移与伤害衰减:

public class PhysicsEngine {
    private static final double FIST_MASS = 0.5; // 拳头质量约0.5kg

    /**
     * 计算打击后的动量交换
     * @param punch 打击信息
     * @param target 被击中的拳击手
     * @return 实际造成的伤害值
     */
    public static double applyPunch(Punch punch, Boxer target) {
        // 打击速度 v = sqrt(2*E/m),假设单次打击能量与体力相关
        double punchVelocity = Math.sqrt(2 * punch.damage / FIST_MASS);

        // 动量 p = m*v
        double momentum = FIST_MASS * punchVelocity;

        // 受击后目标速度变化 delta_v = p / M_target
        double deltaVX = punch.forceX * momentum / target.mass;
        double deltaVY = punch.forceY * momentum / target.mass;

        target.vx += deltaVX * 0.1; // 衰减系数
        target.vy += deltaVY * 0.1;

        // 伤害计算: 考虑防守姿态减伤
        double damageMultiplier = getDamageMultiplier(target.currentStance, punch.type);
        double actualDamage = punch.damage * damageMultiplier;

        target.health -= actualDamage;
        target.stamina -= actualDamage * 0.3; // 受击消耗体力

        return actualDamage;
    }

    private static double getDamageMultiplier(Boxer.Stance stance, Punch.PunchType type) {
        switch (stance) {
            case GUARD: return 0.4; // 格挡大幅减伤
            case DODGE_LEFT:
            case DODGE_RIGHT:
                // 闪避对直拳效果好,对勾拳效果差
                if (type == Punch.PunchType.JAB || type == Punch.PunchType.CROSS) return 0.1;
                return 0.8;
            default: return 1.0;
        }
    }
}

二、决策树AI设计

2.1 决策树节点结构

AI根据当前局势选择最优动作。决策树采用行为树变体,每层评估不同维度:

public interface DecisionNode {
    Boxer.Stance evaluate(Boxer self, Boxer opponent, double distance);
}

/**
 * 复合节点: 按优先级顺序评估子节点
 */
public class PrioritySelector implements DecisionNode {
    private final List<DecisionNode> children = new ArrayList<>();

    public void addChild(DecisionNode child) { children.add(child); }

    @Override
    public Boxer.Stance evaluate(Boxer self, Boxer opponent, double distance) {
        for (DecisionNode child : children) {
            Boxer.Stance result = child.evaluate(self, opponent, distance);
            if (result != null) return result;
        }
        return Boxer.Stance.GUARD;
    }
}

2.2 具体决策节点实现

/**
 * 血量危急时优先防守
 */
public class HealthCheckNode implements DecisionNode {
    @Override
    public Boxer.Stance evaluate(Boxer self, Boxer opponent, double distance) {
        if (self.health < 30 && self.stamina > 20) {
            // 随机左右闪避
            return Math.random() > 0.5 ? Boxer.Stance.DODGE_LEFT : Boxer.Stance.DODGE_RIGHT;
        }
        return null; // 不满足条件,继续评估
    }
}

/**
 * 距离内出拳决策
 */
public class AttackNode implements DecisionNode {
    @Override
    public Boxer.Stance evaluate(Boxer self, Boxer opponent, double distance) {
        if (distance > self.reach * 1.2) return null; // 距离太远
        if (self.attackCooldown > 0) return null;      // 冷却中
        if (self.stamina < 15) return null;            // 体力不足

        double rand = Math.random();
        // 根据对手姿态选择最优打击方式
        if (opponent.currentStance == Boxer.Stance.GUARD) {
            // 对手格挡时,优先勾拳/上勾拳破防
            return rand > 0.5 ? Boxer.Stance.HOOK : Boxer.Stance.UPPERCUT;
        } else if (opponent.currentStance == Boxer.Stance.DODGE_LEFT) {
            // 对手左闪,打右侧勾拳
            return Boxer.Stance.HOOK;
        } else {
            // 直拳试探
            return Boxer.Stance.JAB;
        }
    }
}

/**
 * 拉开距离的移动策略
 */
public class MovementNode implements DecisionNode {
    @Override
    public Boxer.Stance evaluate(Boxer self, Boxer opponent, double distance) {
        if (self.stamina < 20) {
            // 体力不足,保持防守距离
            return Boxer.Stance.GUARD;
        }
        if (distance < 0.5) {
            // 太近,后撤
            self.vx = (self.x > opponent.x) ? 0.5 : -0.5;
        }
        return null;
    }
}

2.3 AI决策树组装

public class BoxingAI {
    private final DecisionNode root;

    public BoxingAI() {
        PrioritySelector selector = new PrioritySelector();
        selector.addChild(new HealthCheckNode());  // 优先级1: 保命
        selector.addChild(new AttackNode());       // 优先级2: 攻击
        selector.addChild(new MovementNode());     // 优先级3: 移动
        this.root = selector;
    }

    /**
     * 每帧调用,更新AI拳击手的姿态
     */
    public void update(Boxer ai, Boxer player) {
        double distance = Math.abs(ai.x - player.x);
        Boxer.Stance newStance = root.evaluate(ai, player, distance);
        ai.currentStance = newStance != null ? newStance : Boxer.Stance.GUARD;

        // 根据姿态生成打击
        if (isAttackStance(ai.currentStance) && ai.attackCooldown == 0) {
            createPunch(ai, player);
            ai.attackCooldown = getCooldown(ai.currentStance);
        }

        if (ai.attackCooldown > 0) ai.attackCooldown--;
    }

    private boolean isAttackStance(Boxer.Stance s) {
        return s == Boxer.Stance.JAB || s == Boxer.Stance.HOOK || s == Boxer.Stance.UPPERCUT;
    }

    private int getCooldown(Boxer.Stance s) {
        switch (s) {
            case JAB: return 10;
            case HOOK: return 20;
            case UPPERCUT: return 25;
            default: return 0;
        }
    }

    private void createPunch(Boxer ai, Boxer player) {
        Punch punch = new Punch();
        punch.x = ai.x + (ai.x < player.x ? ai.reach : -ai.reach);
        punch.y = ai.y + 1.2; // 击打高度
        punch.width = 0.3;
        punch.height = 0.3;
        punch.damage = 8 + Math.random() * 7; // 8-15点伤害
        punch.forceX = (player.x - ai.x) > 0 ? 1 : -1;
        punch.forceY = 0.2;

        if (punch.intersects(player)) {
            double actual = PhysicsEngine.applyPunch(punch, player);
            System.out.printf("AI击中! 造成 %.1f 点伤害,玩家剩余血量: %.1f%n", actual, player.health);
        }
    }
}

三、主循环与渲染框架

public class BoxingGame {
    private final Boxer player;
    private final Boxer ai;
    private final BoxingAI aiController;

    public BoxingGame() {
        player = new Boxer(2.0, 0, 85, 1.8);
        ai = new Boxer(8.0, 0, 90, 1.9);
        aiController = new BoxingAI();
    }

    /**
     * 单步模拟,可扩展为带UI的完整游戏
     */
    public void step() {
        // 更新位置 (简单的欧拉积分)
        player.x += player.vx * 0.016;
        player.y += player.vy * 0.016;
        ai.x += ai.vx * 0.016;
        ai.y += ai.vy * 0.016;

        // 速度衰减 (摩擦力)
        player.vx *= 0.9; player.vy *= 0.9;
        ai.vx *= 0.9; ai.vy *= 0.9;

        // 边界限制
        player.x = Math.max(0, Math.min(10, player.x));
        ai.x = Math.max(0, Math.min(10, ai.x));

        // AI决策
        aiController.update(ai, player);

        // 体力自然恢复
        player.stamina = Math.min(100, player.stamina + 0.1);
        ai.stamina = Math.min(100, ai.stamina + 0.1);
    }

    public boolean isOver() {
        return player.health <= 0 || ai.health <= 0;
    }

    public static void main(String[] args) {
        BoxingGame game = new BoxingGame();
        int round = 0;
        while (!game.isOver() && round < 10000) {
            game.step();
            round++;
            if (round % 100 == 0) {
                System.out.printf("Round %d: Player HP=%.1f Stamina=%.1f | AI HP=%.1f Stamina=%.1f%n",
                    round/100, game.player.health, game.player.stamina,
                    game.ai.health, game.ai.stamina);
            }
        }
        System.out.println(game.player.health > game.ai.health ? "玩家胜利!" : "AI胜利!");
    }
}

四、算法复杂度分析

模块 时间复杂度 空间复杂度 说明
碰撞检测 O(1) O(1) 单次AABB相交判断为常数时间
决策树评估 O(k) O(1) k为决策树节点数,通常<10
单帧模拟 O(1) O(1) 每帧固定计算量
整体对局 O(n) O(1) n为总帧数,与对局时长成正比

五、扩展方向

  1. 神经网络评估: 用多层感知机替代决策树,通过自对弈训练更智能的AI
  2. Monte Carlo模拟: 模拟未来多步打击结果,选择期望收益最大的动作
  3. 物理精细化: 引入旋转、重心偏移,实现更真实的击倒判定

总结

本文通过Java实现了一个完整的拳击物理模拟与AI决策系统。核心算法包括基于AABB的碰撞检测、动量守恒的打击力学计算,以及基于优先级决策树的AI策略选择。读者可以在此基础上扩展出完整的拳击游戏,并尝试用更高级的AI算法(如Minimax、MCTS)替代决策树,提升对手的智能水平。