package com.ai.snake;
/**
* @author cruder
* @version 1.0
* @date 2025/2/28 9:00
*/
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
public class SnakeGame extends JPanel implements ActionListener {
private static final int SCREEN_WIDTH = 600;
private static final int SCREEN_HEIGHT = 600;
private static final int UNIT_SIZE = 25;
private static final int GAME_UNITS = (SCREEN_WIDTH * SCREEN_HEIGHT) / UNIT_SIZE;
private static final int DELAY = 75;
private final int[] x = new int[GAME_UNITS];
private final int[] y = new int[GAME_UNITS];
private int bodyParts = 6;
private int applesEaten;
private int appleX;
private int appleY;
private char direction = 'R';
private boolean running = false;
private Timer timer;
private Random random;
public SnakeGame() {
random = new Random();
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.setBackground(Color.black);
this.setFocusable(true);
this.addKeyListener(new MyKeyAdapter());
startGame();
}
public void startGame() {
newApple();
running = true;
timer = new Timer(DELAY, this);
timer.start();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
public void draw(Graphics g) {
if (running) {
g.setColor(Color.red);
g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);
for (int i = 0; i < bodyParts; i++) {
if (i == 0) {
g.setColor(Color.green);
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
} else {
g.setColor(new Color(45, 180, 0));
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
}
}
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 40));
FontMetrics metrics = getFontMetrics(g.getFont());
g.drawString("Score: " + applesEaten, (SCREEN_WIDTH - metrics.stringWidth("Score: " + applesEaten)) / 2, g.getFont().getSize());
} else {
gameOver(g);
}
}
public void newApple() {
appleX = random.nextInt((int) (SCREEN_WIDTH / UNIT_SIZE)) * UNIT_SIZE;
appleY = random.nextInt((int) (SCREEN_HEIGHT / UNIT_SIZE)) * UNIT_SIZE;
}
public void move() {
for (int i = bodyParts; i > 0; i--) {
x[i] = x[i - 1];
y[i] = y[i - 1];
}
switch (direction) {
case 'U':
y[0] = y[0] - UNIT_SIZE;
break;
case 'D':
y[0] = y[0] + UNIT_SIZE;
break;
case 'L':
x[0] = x[0] - UNIT_SIZE;
break;
case 'R':
x[0] = x[0] + UNIT_SIZE;
break;
}
if (x[0] == appleX && y[0] == appleY) {
bodyParts++;
applesEaten++;
newApple();
}
if (x[0] < 0 || x[0] >= SCREEN_WIDTH || y[0] < 0 || y[0] >= SCREEN_HEIGHT) {
running = false;
}
for (int i = bodyParts; i > 0; i--) {
if (x[0] == x[i] && y[0] == y[i]) {
running = false;
}
}
}
public void gameOver(Graphics g) {
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 75));
FontMetrics metrics1 = getFontMetrics(g.getFont());
g.drawString("Game Over", (SCREEN_WIDTH - metrics1.stringWidth("Game Over")) / 2, SCREEN_HEIGHT / 2);
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 40));
FontMetrics metrics2 = getFontMetrics(g.getFont());
g.drawString("Score: " + applesEaten, (SCREEN_WIDTH - metrics2.stringWidth("Score: " + applesEaten)) / 2, g.getFont().getSize());
}
public class MyKeyAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
if (direction != 'R') {
direction = 'L';
}
break;
case KeyEvent.VK_RIGHT:
if (direction != 'L') {
direction = 'R';
}
break;
case KeyEvent.VK_UP:
if (direction != 'D') {
direction = 'U';
}
break;
case KeyEvent.VK_DOWN:
if (direction != 'U') {
direction = 'D';
}
break;
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (running) {
move();
}
repaint();
}
public static void main(String[] args) {
JFrame frame = new JFrame();
SnakeGame snakeGame = new SnakeGame();
frame.add(snakeGame);
frame.setTitle("Snake Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.pack();
frame.setVisible(true);
frame.setLocationRelativeTo(null);
}
}
1. 基础结构
package com.ai.snake;
import javax.swing.*;
import java.awt.*;
... // 其他导入
package
声明代码所属的包(Java的代码组织单元)import
语句引入Swing GUI组件(JFrame
,JPanel
等)和AWT图形库
2. 类定义与常量
public class SnakeGame extends JPanel implements ActionListener {
private static final int SCREEN_WIDTH = 600; // 游戏窗口宽度
private static final int UNIT_SIZE = 25; // 每个游戏单位(蛇身/苹果)的像素大小
private static final int DELAY = 75; // 定时器间隔(控制游戏速度)
- 继承
JPanel
实现绘图功能 - 实现
ActionListener
接口处理定时器事件
3. 核心变量
private final int[] x = new int[GAME_UNITS]; // 蛇身X坐标数组
private final int[] y = new int[GAME_UNITS]; // 蛇身Y坐标数组
private int bodyParts = 6; // 初始蛇身长度
private char direction = 'R'; // 初始移动方向(右)
- 使用数组存储蛇身每个节点的坐标
bodyParts
记录当前蛇身长度direction
用U/D/L/R表示移动方向
4. 构造函数
public SnakeGame() {
random = new Random();
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.addKeyListener(new MyKeyAdapter()); // 注册键盘监听
startGame();
}
- 设置面板尺寸为600x600
- 添加自定义键盘事件监听器
- 调用
startGame()
初始化游戏
5. 游戏逻辑方法
移动逻辑:
void move() {
for (int i = bodyParts; i > 0; i--) { // 从尾部开始更新坐标
x[i] = x[i-1];
y[i] = y[i-1];
}
switch(direction) { // 根据方向更新头部坐标
case 'U': y -= UNIT_SIZE; break;
case 'D': y += UNIT_SIZE; break;
// ...其他方向类似
}
}
- 通过循环将每个身体节点的坐标更新为前一个节点的位置
- 根据方向键改变头部坐标
碰撞检测:
if (x < 0 || x >= SCREEN_WIDTH) running = false; // 撞墙检测
for (int i = bodyParts; i>0; i--) { // 自碰检测
if (x==x[i] && y==y[i]) running = false;
}
6. 图形绘制
public void draw(Graphics g) {
if (running) {
g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE); // 绘制苹果
for (int i=0; i<bodyParts; i++) { // 绘制蛇身
if(i==0) g.setColor(Color.GREEN); // 头部颜色
else g.setColor(new Color(45,180,0)); // 身体颜色
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
}
} else {
gameOver(g); // 显示结束画面
}
}
7. 事件处理
class MyKeyAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_LEFT:
if(direction != 'R') direction = 'L'; // 禁止反向移动
break;
// 其他方向处理类似
}
}
}
8. 游戏启动
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.add(new SnakeGame());
frame.setTitle("Snake Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack(); // 自动调整窗口大小
frame.setVisible(true);
}
关键机制说明:
- 定时器驱动:通过
Timer(DELAY, this)
每75ms触发一次actionPerformed
,实现游戏循环 - 双缓冲绘图:继承JPanel后重写
paintComponent
方法实现无闪烁绘图 - 坐标系统:所有坐标以
UNIT_SIZE
为基本单位,保证蛇和苹果对齐网格 - 方向控制:通过限制180度转向(如不能从右直接转左)保证游戏合理性
建议运行代码时注意:
- 游戏结束条件包括撞墙和自碰
- 每吃一个苹果(红色圆点)得分+1,蛇身长度+1
- 使用方向键控制移动方向
这个实现包含了Java GUI编程的核心要素:事件处理、图形绘制、动画定时器等,是学习游戏开发的经典案例。可以尝试修改UNIT_SIZE
或DELAY
值观察游戏变化,加深理解。
由小艺AI生成<xiaoyi.huawei.com>