引言
弓箭传说(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) | 每帧计算所有敌人到玩家的方向向量 |
关键知识点总结
- 抛物线运动分解:将斜抛运动分解为水平匀速和竖直匀加速两个独立分量,简化物理模拟。
- AABB碰撞检测:通过比较两个矩形在X/Y轴上的投影区间是否重叠来判断碰撞,避免复杂的距离计算。
- Roguelike技能设计:采用”技能池+随机抽取”机制,配合技能间的协同效应(如穿透+弹射),创造丰富的策略空间。
- HashSet去重:使用
hitEnemies集合记录每支箭已命中的敌人,防止单支箭对同一敌人造成多次伤害。