横版射击游戏是很多玩家的童年回忆,其中《魂斗罗》(Contra)系列更是经典中的经典。本文将以 JavaSwing 为基础,带你一步步了解如何从无到有实现一个简易的横版射击游戏,涵盖游戏开发中的核心思路和关键技术点。
一、项目背景与目标
JavaSwing 作为 Java 自带的 GUI 工具包,虽然并非专门为游戏开发设计,但足以实现小型 2D 游戏的核心功能。本项目旨在通过复刻《魂斗罗》的基础玩法,展示如何利用 Swing 的组件体系、事件监听和绘图机制构建游戏框架。
我们的目标是实现一个包含以下功能的简易游戏:
- 可控制的玩家角色(移动、跳跃、射击)
- 自动行动的敌方角色
- 滚动的游戏背景
- 子弹碰撞检测
- 角色生命值与战斗系统
- 基本的游戏循环与画面刷新
二、核心组件设计思路
1. 主窗口与分层面板
游戏的基础是一个可视化窗口,我们使用 JFrame 作为主容器。由于游戏中存在背景、角色、子弹等多个元素,需要通过分层面板(JLayeredPane)实现元素的层级显示 —— 背景在最底层,角色和子弹在上方,确保视觉上的遮挡关系正确。
窗口初始化时需设置固定尺寸、禁用缩放功能,并通过 WindowAdapter 监听窗口关闭事件,确保游戏线程能正常终止。
2. 背景绘制与滚动机制
横版游戏的一大特点是背景会随角色移动而滚动,营造出探索广阔地图的感觉。我们通过自定义 BackgroundPanel 实现这一功能:
- 加载大幅背景图片(超出窗口尺寸)
- 维护一个水平偏移量(offsetX),通过修改偏移量控制背景显示区域
- 当角色移动时,同步调整偏移量,使背景产生滚动效果
- 为避免背景滚动到边缘出现空白,采用循环绘制多张背景图片的方式实现无缝滚动
3. 角色系统设计
角色是游戏的核心元素,我们采用面向对象思想设计角色体系:
- 抽象出 Person 基类,封装所有角色的共同属性(坐标、生命值、图片资源)和行为(移动、跳跃、射击、绘制)
- 玩家角色(Hero)和敌方角色(Enemy)继承自 Person 类,分别实现各自的特有逻辑
- 角色移动与背景滚动联动,当角色向左右移动时,通过调整背景偏移量实现 "角色不动、场景移动" 的视觉效果,避免角色跑出屏幕
跳跃功能通过模拟重力实现:给角色一个初始向上速度,随时间逐渐减小(受重力影响),直到速度变为正(下落),最终回到初始高度。
4. 子弹系统与碰撞检测
射击是横版射击游戏的核心玩法,子弹系统设计需考虑:
- 子弹类(Bullet)包含位置、速度、伤害值、状态(飞行中 / 爆炸)等属性
- 实现子弹飞行逻辑:根据发射方向持续更新坐标
- 子弹与角色的碰撞检测:通过矩形区域重叠判断(比较子弹与角色的坐标和尺寸)
- 碰撞后处理:子弹爆炸、角色减血、播放特效(本案例通过图片切换模拟)
5. 游戏主循环
游戏的流畅运行依赖于稳定的主循环(Game Loop),我们通过单独的线程(ThreadContraL)实现:
- 控制帧率(本案例为 60 FPS),确保画面刷新稳定
- 每帧更新游戏状态:角色位置、子弹位置、背景偏移、AI 行为
- 处理碰撞检测逻辑
- 重绘界面组件,刷新视觉效果
- 检测游戏结束条件(角色生命值为 0)
6. 输入控制
玩家通过键盘与游戏交互,我们使用 KeyListener 实现输入响应:
- 监听方向键(A/D)控制背景滚动(角色移动)
- 监听跳跃键(W)触发角色跳跃逻辑
- 监听射击键(J/K)控制单发射击与连发射击
- 监听攻击键(I)触发近战攻击动作
7. 敌方 AI 设计
简单的敌方 AI 行为能提升游戏的可玩性:
- 移动逻辑:根据与玩家的距离随机选择前进、后退或静止
- 攻击逻辑:在有效范围内随机发射子弹
- 躲避行为:当玩家近战攻击时,有概率触发跳跃躲避
- 受击反应:被击中时产生击退效果
三、开发中的关键技术点
-
双缓冲机制:Swing 组件默认支持双缓冲,可减少画面闪烁,确保动画流畅
-
线程安全:游戏线程与 Swing 事件调度线程(EDT)分离,通过 SwingUtilities.invokeLater () 确保界面更新在 EDT 中执行
-
图片资源加载:使用 ImageIO 和 MediaTracker 确保图片加载完成后再进行绘制,避免空指针异常
-
状态管理:通过布尔变量(如 isFlying、isBurst、isEscaping)维护游戏元素的状态,控制行为逻辑切换
-
坐标计算:所有元素的位置计算需考虑背景偏移量,确保相对位置正确
四、总结与扩展方向
通过上述设计,我们实现了一个具备基础玩法的横版射击游戏框架。这个简易版本虽然功能有限,但包含了 2D 游戏开发的核心要素:场景管理、角色控制、碰撞检测、游戏循环和用户输入。
在此基础上,还可以扩展更多功能:
- 增加多种敌人类型和 Boss 战
- 实现道具系统(武器升级、生命值恢复)
- 添加关卡切换与进度保存
- 加入音效与背景音乐
- 优化 AI 逻辑,实现更复杂的敌方行为
JavaSwing 虽然不是游戏开发的首选工具,但通过这个案例可以看出,利用其基础组件和绘图能力,完全可以构建出具有可玩性的小型游戏。这个过程不仅能加深对 Swing 框架的理解,更能帮助开发者掌握游戏开发的基本思路和设计模式。
---------------------------------------------------------------------------------------------------------------------------------
二编:我们已经实现了音乐(BGM,击打音效)和敌人的肘击行为。
package contra.main;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
public class BackgroundPanel extends JPanel {
private Image backgroundImage;
private int imageWidth;
private int imageHeight;
protected static int offsetX = 0; // 水平偏移量
protected int speedX = 0; // 水平移动速度
private final int SCROLL_SPEED = 10; // 滚动速度
private int prevOffsetX = 0; // 上一帧的偏移量
public BackgroundPanel() {
}
public BackgroundPanel(String imagePath) throws IOException {
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
throw new IOException("图片文件不存在: " + imagePath);
}
backgroundImage = ImageIO.read(imageFile);
MediaTracker tracker = new MediaTracker(this);
tracker.addImage(backgroundImage, 0);
try {
tracker.waitForAll();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("图片加载被中断", e);
}
imageWidth = backgroundImage.getWidth(this);
imageHeight = backgroundImage.getHeight(this);
System.out.println("背景实际尺寸: " + imageWidth + "x" + imageHeight);
setLayout(null);
}
public void update() {
prevOffsetX = offsetX;
offsetX -= speedX;
if (offsetX < -imageWidth) {
offsetX += imageWidth;
} else if (offsetX >= imageWidth) {
offsetX -= imageWidth;
}
}
// 获取偏移量的变化
public int getOffsetXChange() {
return offsetX - prevOffsetX;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (backgroundImage != null) {
g.drawImage(backgroundImage, -offsetX, 0, this);
g.drawImage(backgroundImage, -offsetX + imageWidth, 0, this);
g.drawImage(backgroundImage, -offsetX + (2 * imageWidth), 0, this);
}
}
public void moveRight() {
speedX = -SCROLL_SPEED;
}
public void moveLeft() {
speedX = SCROLL_SPEED;
}
public void stopMoving() {
speedX = 0;
}
public boolean canMoveRight(Person hero, Person enemy) {
return (hero.getX() + hero.getWidth()) < enemy.getX();
}
}package contra.main;
import javax.swing.*;
public class Bullet {
protected int x, y;
protected int speedFly;
protected int hurt;
protected boolean isFlying = false;
protected ImageIcon imageIcon, imageBurstIcon;
protected boolean isBurst = false;
protected boolean needHurt = true;
protected int countBurst = 0;
public Bullet(int x, int y, int speedFly, int hurt, ImageIcon imageIcon, ImageIcon imageBurstIcon) {
this.x = x;
this.y = y;
this.speedFly = speedFly;
this.hurt = hurt;
this.imageIcon = imageIcon;
this.imageBurstIcon = imageBurstIcon;
}
public void fly() {
if (isBurst) {
countBurst++;
needHurt = false;
} else {
x += speedFly;
}
}
public void burstChangeImage() {
imageIcon = imageBurstIcon;
// System.out.println("BURST");
}
}
package contra.main;
import javax.xml.crypto.dsig.keyinfo.KeyValue;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class ContraLListener implements KeyListener {
private BackgroundPanel backgroundPanel;
private Person person;
private Enemy enemy;
public void setBackgroundPanel(BackgroundPanel backgroundPanel) {
this.backgroundPanel = backgroundPanel;
}
public void setPerson(Person person) {
this.person = person;
}
public void setEnemy(Enemy enemy) {
this.enemy = enemy;
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (backgroundPanel != null && person != null && enemy != null) {
if (keyCode == KeyEvent.VK_D) {
// 只有允许移动时才执行
if (backgroundPanel.canMoveRight(person, enemy)) {
backgroundPanel.moveRight();
} else {
backgroundPanel.stopMoving();
}
ThreadContraL.isRunningAndNeedToRun = true;
} else if (keyCode == KeyEvent.VK_A) {
backgroundPanel.moveLeft();
ThreadContraL.isRunningAndNeedToRun = true;
}
if (keyCode == KeyEvent.VK_W) {
ThreadContraL.isJumpingT = true;
}
if (keyCode == KeyEvent.VK_J) {
if (person.getPersonCondition() == 0) {
person.shootingSingleBullet();
}
}
if (keyCode == KeyEvent.VK_K) {
person.toggleContinuousShooting();
}
if (keyCode == KeyEvent.VK_I && !ThreadContraL.isJumpingT) {
person.hitRaiseTheGunAndAttackWithTheBarrel();
enemy.hitByBarrel(backgroundPanel.getOffsetXChange(), person);
}
}
}
@Override
public void keyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
if (backgroundPanel != null) {
if (keyCode == KeyEvent.VK_D || keyCode == KeyEvent.VK_A) {
backgroundPanel.stopMoving();
ThreadContraL.isRunningAndNeedToRun = false;
}
}
}
}package contra.main;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
public class ContraLUI {
public final static int JPANEL_WIDTH = 1920;
public final static int JPANEL_HEIGHT = 1075;
public final static int INIT_HERO_X = 450;
public final static int INIT_HERO_Y = 700;
public final static int BACKGROUND_WIDTH = 5760;
private ThreadContraL gameThread;
public void initBackground() {
try {
JFrame jf = new JFrame(" ");
jf.setSize(JPANEL_WIDTH, JPANEL_HEIGHT);
System.out.println("长度 + " + jf.getWidth() + " 宽度 +" + jf.getHeight());
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setLocationRelativeTo(null);
jf.setResizable(false);
JLayeredPane

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



