每日算法 — 使用java实现弓箭传说:Roguelike随机技能组合与抛物线弹道模拟

引言

弓箭传说(Archero)是一款风靡全球的Roguelike射箭手游。玩家操控一名弓箭手,在随机生成的关卡中面对潮水般的敌人,通过拾取随机技能来强化自身。本文将用Java实现弓箭传说的核心玩法,重点讲解抛物线弹道物理模拟、随机技能组合系统以及碰撞检测算法。

核心算法概述

1. 抛物线弹道模拟

弓箭的飞行轨迹遵循抛物线运动学。我们将位移分解为水平方向的匀速运动和竖直方向的匀加速运动:

  • 水平位移:x = v0 * cos(θ) * t
  • 竖直位移:y = v0 * sin(θ) * t - 0.5 * g * t²

其中 v0 为初速度,θ 为发射角度,g 为重力加速度,t 为飞行时间。

2. 随机技能组合系统

Roguelike的核心魅力在于随机性。我们设计一个技能池,每次升级时从池中随机抽取若干技能供玩家选择。技能之间存在协同效应,例如”弹射”+”穿透”可以造成连锁伤害。

3. AABB碰撞检测

箭矢与敌人、敌人与玩家之间的碰撞检测采用轴对齐包围盒(AABB)算法。该算法通过比较两个矩形在X轴和Y轴上的投影是否重叠来判断碰撞,时间复杂度为O(1)。

Java完整实现

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

/**
 * 弓箭传说核心模拟
 * 包含:抛物线弹道、随机技能系统、AABB碰撞检测
 */
public class ArcheroSimulation extends JPanel implements ActionListener, MouseListener {
    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;
    private static final double GRAVITY = 0.15; // 重力加速度
    private static final int FPS = 60;

    private Player player;
    private ArrayList<Arrow> arrows;
    private ArrayList<Enemy> enemies;
    private ArrayList<Skill> activeSkills;
    private SkillPool skillPool;
    private Random random;
    private Timer timer;
    private int score;
    private int level;

    public ArcheroSimulation() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setBackground(new Color(34, 45, 56));
        addMouseListener(this);

        random = new Random();
        player = new Player(100, HEIGHT - 150);
        arrows = new ArrayList<>();
        enemies = new ArrayList<>();
        activeSkills = new ArrayList<>();
        skillPool = new SkillPool();
        score = 0;
        level = 1;

        // 初始赠送一个技能
        activeSkills.add(skillPool.randomSkill());

        timer = new Timer(1000 / FPS, this);
        timer.start();

        // 定时生成敌人
        Timer spawnTimer = new Timer(2000, e -> spawnEnemy());
        spawnTimer.start();
    }

    /**
     * 玩家类:包含位置、生命值和移动逻辑
     */
    class Player {
        double x, y;
        int width = 30, height = 30;
        int hp = 100;
        int maxHp = 100;
        double speed = 2.5;

        Player(double x, double y) {
            this.x = x;
            this.y = y;
        }

        /**
         * 获取AABB包围盒,用于碰撞检测
         */
        Rectangle getBounds() {
            return new Rectangle((int)x, (int)y, width, height);
        }
    }

    /**
     * 箭矢类:抛物线运动的核心实现
     */
    class Arrow {
        double x, y;
        double vx, vy; // 水平/竖直速度分量
        int width = 8, height = 3;
        double damage = 25;
        int pierceCount = 0; // 穿透次数
        int bounceCount = 0; // 弹射次数
        HashSet<Enemy> hitEnemies = new HashSet<>(); // 已命中敌人,避免重复伤害
        boolean active = true;

        Arrow(double x, double y, double angle, double speed, ArrayList<Skill> skills) {
            this.x = x;
            this.y = y;
            // 速度分解为水平和竖直分量
            this.vx = speed * Math.cos(angle);
            this.vy = speed * Math.sin(angle);

            // 应用技能效果
            for (Skill s : skills) {
                switch (s.type) {
                    case PIERCE -> pierceCount += s.value; // 增加穿透次数
                    case BOUNCE -> bounceCount += s.value; // 增加弹射次数
                    case DAMAGE -> damage *= (1 + s.value * 0.2); // 增伤
                    case SPEED -> { // 加速
                        this.vx *= (1 + s.value * 0.15);
                        this.vy *= (1 + s.value * 0.15);
                    }
                }
            }
        }

        /**
         * 更新箭矢位置,核心物理模拟
         */
        void update() {
            x += vx;
            y += vy;
            vy += GRAVITY; // 竖直方向受重力影响

            // 边界检测与弹射逻辑
            if (x < 0 || x > WIDTH) {
                if (bounceCount > 0) {
                    vx = -vx;
                    bounceCount--;
                } else {
                    active = false;
                }
            }
            if (y > HEIGHT) {
                active = false;
            }
        }

        Rectangle getBounds() {
            return new Rectangle((int)x, (int)y, width, height);
        }

        /**
         * 计算箭矢当前飞行角度,用于绘制旋转
         */
        double getAngle() {
            return Math.atan2(vy, vx);
        }
    }

    /**
     * 敌人类:简单的AI移动逻辑
     */
    class Enemy {
        double x, y;
        int width = 25, height = 25;
        double speed = 0.8 + random.nextDouble();
        int hp = 30;
        int maxHp = 30;
        boolean active = true;

        Enemy(double x, double y) {
            this.x = x;
            this.y = y;
        }

        /**
         * 敌人AI:向玩家直线移动(简单寻路)
         */
        void update(Player player) {
            double dx = player.x - x;
            double dy = player.y - y;
            double dist = Math.sqrt(dx * dx + dy * dy);
            if (dist > 0) {
                x += (dx / dist) * speed;
                y += (dy / dist) * speed;
            }
        }

        Rectangle getBounds() {
            return new Rectangle((int)x, (int)y, width, height);
        }
    }

    /**
     * 技能枚举:定义所有可用的技能类型
     */
    enum SkillType {
        PIERCE,    // 穿透:箭矢可穿过敌人继续飞行
        BOUNCE,    // 弹射:箭矢碰到边界会反弹
        DAMAGE,    // 增伤:提升箭矢伤害
        SPEED,     // 加速:提升箭矢飞行速度
        MULTISHOT, // 多重射击:一次发射多支箭
        CRIT       // 暴击:概率造成双倍伤害
    }

    /**
     * 技能类:Roguelike随机技能的核心数据结构
     */
    class Skill {
        String name;
        SkillType type;
        int value; // 技能等级/数值
        String description;

        Skill(String name, SkillType type, int value, String description) {
            this.name = name;
            this.type = type;
            this.value = value;
            this.description = description;
        }
    }

    /**
     * 技能池:管理所有可随机获得的技能
     */
    class SkillPool {
        ArrayList<Skill> pool;

        SkillPool() {
            pool = new ArrayList<>();
            // 初始化技能池,每种技能有多个等级变体
            pool.add(new Skill("穿刺Ⅰ", SkillType.PIERCE, 1, "箭矢可穿透1个敌人"));
            pool.add(new Skill("穿刺Ⅱ", SkillType.PIERCE, 2, "箭矢可穿透2个敌人"));
            pool.add(new Skill("弹射Ⅰ", SkillType.BOUNCE, 1, "箭矢碰到边界反弹1次"));
            pool.add(new Skill("弹射Ⅱ", SkillType.BOUNCE, 2, "箭矢碰到边界反弹2次"));
            pool.add(new Skill("强力射击Ⅰ", SkillType.DAMAGE, 1, "伤害提升20%"));
            pool.add(new Skill("强力射击Ⅱ", SkillType.DAMAGE, 2, "伤害提升40%"));
            pool.add(new Skill("疾风Ⅰ", SkillType.SPEED, 1, "箭速提升15%"));
            pool.add(new Skill("疾风Ⅱ", SkillType.SPEED, 2, "箭速提升30%"));
            pool.add(new Skill("多重射击", SkillType.MULTISHOT, 1, "每次发射3支箭"));
            pool.add(new Skill("暴击", SkillType.CRIT, 1, "30%概率造成双倍伤害"));
        }

        /**
         * 从技能池中随机抽取一个技能
         * 实际游戏中会提供3选1,此处简化为直接随机
         */
        Skill randomSkill() {
            return pool.get(random.nextInt(pool.size()));
        }

        /**
         * 提供3个随机技能供玩家选择(Roguelike经典3选1)
         */
        ArrayList<Skill> offerChoices() {
            ArrayList<Skill> choices = new ArrayList<>();
            ArrayList<Skill> temp = new ArrayList<>(pool);
            Collections.shuffle(temp);
            for (int i = 0; i < Math.min(3, temp.size()); i++) {
                choices.add(temp.get(i));
            }
            return choices;
        }
    }

    /**
     * AABB碰撞检测:轴对齐包围盒判断
     * 时间复杂度O(1),适用于2D游戏中的矩形碰撞
     */
    boolean checkCollision(Rectangle a, Rectangle b) {
        return a.x < b.x + b.width &&
               a.x + a.width > b.x &&
               a.y < b.y + b.height &&
               a.y + a.height > b.y;
    }

    /**
     * 生成新敌人
     */
    void spawnEnemy() {
        int edge = random.nextInt(3);
        double ex, ey;
        switch (edge) {
            case 0 -> { ex = random.nextInt(WIDTH); ey = 0; }
            case 1 -> { ex = WIDTH; ey = random.nextInt(HEIGHT / 2); }
            default -> { ex = 0; ey = random.nextInt(HEIGHT / 2); }
        }
        enemies.add(new Enemy(ex, ey));
    }

    /**
     * 发射箭矢:根据鼠标位置计算发射角度
     */
    void shoot(double targetX, double targetY) {
        double dx = targetX - player.x;
        double dy = targetY - player.y;
        double angle = Math.atan2(dy, dx);
        double speed = 8.0;

        // 检查是否有多重射击技能
        int arrowCount = 1;
        for (Skill s : activeSkills) {
            if (s.type == SkillType.MULTISHOT) {
                arrowCount = 3;
                break;
            }
        }

        // 发射箭矢,多重射击时分散角度
        double spread = arrowCount > 1 ? 0.15 : 0;
        for (int i = 0; i < arrowCount; i++) {
            double a = angle + (i - arrowCount / 2.0 + 0.5) * spread;
            arrows.add(new Arrow(player.x + 15, player.y + 15, a, speed, activeSkills));
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // 更新箭矢
        for (Arrow arrow : arrows) {
            arrow.update();
        }
        arrows.removeIf(a -> !a.active);

        // 更新敌人
        for (Enemy enemy : enemies) {
            enemy.update(player);
        }

        // 箭矢与敌人碰撞检测
        for (Arrow arrow : arrows) {
            for (Enemy enemy : enemies) {
                if (!arrow.active || !enemy.active) continue;
                if (arrow.hitEnemies.contains(enemy)) continue;

                if (checkCollision(arrow.getBounds(), enemy.getBounds())) {
                    // 暴击判定
                    double finalDamage = arrow.damage;
                    for (Skill s : activeSkills) {
                        if (s.type == SkillType.CRIT && random.nextDouble() < 0.3) {
                            finalDamage *= 2;
                        }
                    }

                    enemy.hp -= finalDamage;
                    arrow.hitEnemies.add(enemy);

                    if (enemy.hp <= 0) {
                        enemy.active = false;
                        score += 10;
                        // 每50分升级,随机获得新技能
                        if (score % 50 == 0) {
                            level++;
                            Skill newSkill = skillPool.randomSkill();
                            activeSkills.add(newSkill);
                            System.out.println("升级!获得技能:" + newSkill.name);
                        }
                    }

                    // 穿透逻辑:如果没有穿透次数,箭矢消失
                    if (arrow.pierceCount <= 0) {
                        arrow.active = false;
                    } else {
                        arrow.pierceCount--;
                    }
                }
            }
        }
        enemies.removeIf(en -> !en.active);

        // 敌人与玩家碰撞
        for (Enemy enemy : enemies) {
            if (checkCollision(player.getBounds(), enemy.getBounds())) {
                player.hp -= 1;
                if (player.hp <= 0) {
                    System.out.println("游戏结束!得分:" + score);
                    timer.stop();
                }
            }
        }

        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制玩家
        g2d.setColor(Color.CYAN);
        g2d.fillOval((int)player.x, (int)player.y, player.width, player.height);

        // 绘制箭矢(根据速度方向旋转)
        g2d.setColor(Color.YELLOW);
        for (Arrow arrow : arrows) {
            g2d.translate(arrow.x + arrow.width / 2.0, arrow.y + arrow.height / 2.0);
            g2d.rotate(arrow.getAngle());
            g2d.fillRect(-arrow.width / 2, -arrow.height / 2, arrow.width, arrow.height);
            g2d.rotate(-arrow.getAngle());
            g2d.translate(-(arrow.x + arrow.width / 2.0), -(arrow.y + arrow.height / 2.0));
        }

        // 绘制敌人
        g2d.setColor(Color.RED);
        for (Enemy enemy : enemies) {
            g2d.fillRect((int)enemy.x, (int)enemy.y, enemy.width, enemy.height);
            // 血条
            g2d.setColor(Color.DARK_GRAY);
            g2d.fillRect((int)enemy.x, (int)enemy.y - 8, enemy.width, 4);
            g2d.setColor(Color.GREEN);
            g2d.fillRect((int)enemy.x, (int)enemy.y - 8, 
                        (int)(enemy.width * enemy.hp / enemy.maxHp), 4);
            g2d.setColor(Color.RED);
        }

        // 绘制UI
        g2d.setColor(Color.WHITE);
        g2d.setFont(new Font("Arial", Font.BOLD, 16));
        g2d.drawString("得分: " + score + "  等级: " + level, 10, 20);
        g2d.drawString("生命: " + player.hp + "/" + player.maxHp, 10, 40);

        // 绘制当前技能
        int sy = 60;
        g2d.drawString("技能:", 10, sy);
        g2d.setFont(new Font("Arial", Font.PLAIN, 12));
        for (Skill s : activeSkills) {
            sy += 15;
            g2d.drawString("• " + s.name + " - " + s.description, 10, sy);
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        shoot(e.getX(), e.getY());
    }
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}

    public static void main(String[] args) {
        JFrame frame = new JFrame("弓箭传说 - Java算法模拟");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new ArcheroSimulation());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

算法复杂度分析

模块 时间复杂度 空间复杂度 说明
抛物线更新 O(1) O(1) 每帧每支箭常数时间物理计算
AABB碰撞检测 O(n×m) O(1) n为箭矢数,m为敌人数
技能组合生成 O(1) O(k) k为技能池大小,随机抽取为常数时间
敌人AI移动 O(m) O(1) 每帧计算所有敌人到玩家的方向向量

关键知识点总结

  1. 抛物线运动分解:将斜抛运动分解为水平匀速和竖直匀加速两个独立分量,简化物理模拟。
  2. AABB碰撞检测:通过比较两个矩形在X/Y轴上的投影区间是否重叠来判断碰撞,避免复杂的距离计算。
  3. Roguelike技能设计:采用”技能池+随机抽取”机制,配合技能间的协同效应(如穿透+弹射),创造丰富的策略空间。
  4. HashSet去重:使用hitEnemies集合记录每支箭已命中的敌人,防止单支箭对同一敌人造成多次伤害。