Java GUI 进阶:Swing 游戏开发实战 —— 时序乱斗俯视角射击 Demo
Java GUI 进阶:Swing 游戏开发实战 —— 时序乱斗俯视角射击 Demo
前言
最近自学 Java,想要巩固线程、集合、面向对象、GUI 相关知识,于是原生使用 Swing 开发了这款名为《时序乱斗》的 2D 俯视角射击 Demo。项目不引入任何第三方游戏引擎,移动、射击、碰撞等全部底层逻辑手动实现。
项目采用分阶段迭代开发,按照真实写代码的顺序逐步完善功能。
开发阶段划分(真实开发顺序)
阶段1:创建窗口,WASD控制蓝色方块移动
阶段2:增加窗口边界限制,新增墙体,玩家无法穿墙
阶段3:鼠标左键点击,向鼠标位置发射匀速飞行子弹(此时子弹不会自动销毁)
阶段4:实现子弹生命周期管理,子弹飞出窗口、撞击墙体时自动销毁
技术栈
Java SE、Swing GUI、固定FPS游戏循环、InputMap按键监听、AWT矩形碰撞、向量归一化、ArrayList实体管理
项目结构
zsq20260724
├─ GameMain.java // 程序入口,创建窗口、启动游戏线程
└─ GamePanel.java // 游戏画布,包含渲染、游戏循环、所有业务逻辑
GameMain.java 完整代码
package zsq20260724;
import javax.swing.*;
public class GameMain {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("时序乱斗");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
GamePanel gamePanel = new GamePanel();
frame.add(gamePanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// 开启独立子线程运行游戏循环,防止界面卡死
new Thread(gamePanel).start();
});
}
}
GamePanel.java 原始开发版本代码
package zsq20260724;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
public class GamePanel extends JPanel implements Runnable {
public static final int WIDTH = 900;
public static final int HEIGHT = 600;
private static final int FPS = 60;
private final long TIME_PER_FRAME = 1000000000 / FPS;
private final Map<Integer, Boolean> keyState = new HashMap<>();
private int playerX = 100;
private int playerY = 100;
private final int playerSize = 40;
private final int speed = 5;
//墙体
private final int wallX = 400;
private final int wallY = 200;
private final int wallW = 120;
private final int wallH = 220;
//存放所有子弹
private List<Bullet> bulletList = new ArrayList<>();
//子弹内部类
class Bullet{
double x,y;
double dx,dy;
int size = 10;
int speed = 7;
public Bullet(double startX,double startY,double targetX,double targetY){
x = startX;
y = startY;
double disX = targetX - startX;
double disY = targetY - startY;
double len = Math.sqrt(disX*disX + disY*disY);
dx = disX/len;
dy = disY/len;
}
public void update(){
x += dx*speed;
y += dy*speed;
}
}
// 按键常量
private static final int W = KeyEvent.VK_W;
private static final int S = KeyEvent.VK_S;
private static final int A = KeyEvent.VK_A;
private static final int D = KeyEvent.VK_D;
public GamePanel() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setBackground(Color.BLACK);
initKeyBind();
//左键发射子弹
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e){
double playerCenterX = playerX + playerSize/2.0;
double playerCenterY = playerY + playerSize/2.0;
Bullet b = new Bullet(playerCenterX,playerCenterY,e.getX(),e.getY());
bulletList.add(b);
}
});
}
// 绑定按键映射,摆脱焦点限制
private void initKeyBind() {
InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
bindKey(inputMap, actionMap, W, true);
bindKey(inputMap, actionMap, W, false);
bindKey(inputMap, actionMap, S, true);
bindKey(inputMap, actionMap, S, false);
bindKey(inputMap, actionMap, A, true);
bindKey(inputMap, actionMap, A, false);
bindKey(inputMap, actionMap, D, true);
bindKey(inputMap, actionMap, D, false);
}
private void bindKey(InputMap inputMap, ActionMap actionMap, int keyCode, boolean pressed) {
String name = keyCode + "_" + pressed;
inputMap.put(KeyStroke.getKeyStroke(keyCode, 0, !pressed), name);
actionMap.put(name, new AbstractAction() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
keyState.put(keyCode, pressed);
}
});
}
//碰撞判断工具方法
private boolean checkCollision(int x1,int y1,int w1,int h1,int x2,int y2,int w2,int h2){
Rectangle rect1 = new Rectangle(x1,y1,w1,h1);
Rectangle rect2 = new Rectangle(x2,y2,w2,h2);
return rect1.intersects(rect2);
}
private void updateGame() {
//先保存移动前坐标
int oldX = playerX;
int oldY = playerY;
//尝试移动
if (keyState.getOrDefault(W, false)) playerY -= speed;
if (keyState.getOrDefault(S, false)) playerY += speed;
if (keyState.getOrDefault(A, false)) playerX -= speed;
if (keyState.getOrDefault(D, false)) playerX += speed;
// 边界限制
playerX = Math.max(0, Math.min(playerX, WIDTH - playerSize));
playerY = Math.max(0, Math.min(playerY, HEIGHT - playerSize));
//检测和墙体碰撞,撞上就退回原来位置
if(checkCollision(playerX,playerY,playerSize,playerSize,wallX,wallY,wallW,wallH)){
playerX = oldX;
playerY = oldY;
}
for (int i = 0;i<bulletList.size();i++){
Bullet b = bulletList.get(i);
b.update();
//条件1:飞出画面
boolean outScreen = b.x<0 || b.y<0 || b.x>WIDTH || b.y>HEIGHT;
//条件2:撞到墙体
boolean hitWall = checkCollision((int)b.x,(int)b.y,b.size,b.size,wallX,wallY,wallW,wallH);
//满足删掉这颗子弹
if (outScreen || hitWall){
bulletList.remove(i);
i--;//删掉后下标对齐,防止跳过子弹
}
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
//灰色墙体
g.setColor(Color.GRAY);
g.fillRect(wallX,wallY,wallW,wallH);
//子弹
g.setColor(Color.YELLOW);
for (int i= 0;i<bulletList.size();i++){
Bullet b = bulletList.get(i);
g.fillRect((int)b.x,(int)b.y,b.size,b.size);
}
//玩家
g.setColor(new Color(60, 130, 255));
g.fillRect(playerX, playerY, playerSize, playerSize);
}
@Override
public void run() {
long lastTime = System.nanoTime();
long now;
while (true) {
now = System.nanoTime();
if (now - lastTime >= TIME_PER_FRAME) {
updateGame();
repaint();
lastTime = now;
}
}
}
}
分阶段开发讲解
阶段1:WASD玩家移动
搭建窗口、游戏循环,实现按键监听,玩家方块可以自由移动。
采用InputMap+ActionMap按键方案,区别老旧KeyListener,支持多键同时按下斜向移动。
此时:没有墙体、没有边界约束、没有子弹。
阶段2:窗口边界 + 墙体碰撞
新增墙体参数、坐标边界钳位代码、矩形碰撞工具方法。
实现效果:玩家不能走出屏幕;碰到墙体无法穿过。
逻辑思路:先移动,碰撞后回退备份坐标。
阶段3:鼠标点击发射子弹
新增Bullet内部类、鼠标监听器、向量归一化计算飞行方向。
向量归一化保证:无论鼠标远近,子弹匀速飞行。
阶段3完成遗留问题:子弹不会销毁,不断发射会持续占用内存。
阶段4:子弹自动销毁
遍历子弹集合,判断出界、撞击墙体,执行删除。
重点知识点:普通for循环删除集合元素,需要i--修正下标,避免循环漏掉子弹。
重点知识点拓展
1. GamePanel为什么继承JPanel
JPanel是Swing内置画布组件,继承后自动拥有绘图paintComponent、鼠标监听、repaint()刷新、尺寸设置等大量GUI能力。
Java只支持单继承,已经继承JPanel,因此无法再继承MouseAdapter、AbstractAction这类抽象类。
所以鼠标、按键事件只能使用匿名内部类实现。
2. new AbstractAction(){} 说明
属于匿名内部类。AbstractAction是抽象类,不是接口,不能使用Lambda表达式简写。
按键触发时,Swing自动回调actionPerformed方法,更新keyState里按键按下/松开状态。
3. 按键监听两种方案对比
简易替代方案:KeyListener
优点:代码简短,新手入门常见;
缺点:依赖组件焦点、多键同时按下容易失效,不适合游戏。
最终选用方案:InputMap + ActionMap
优点:窗口激活即可响应按键、多键输入稳定;
缺点:代码复杂,理解门槛偏高。
运行操作
WASD移动蓝色方块
鼠标左键点击,发射黄色子弹
后续拓展规划
下一阶段:新增 AI 敌人,实现敌人自动追踪玩家。后续还可以实现角色朝向跟随鼠标旋转
欢迎大家在评论区交流学习心得,如有疑问、代码优化思路都可以留言讨论
更多推荐

所有评论(0)