贪吃蛇
思路
-
首先构思游戏布局,计算合理的坐标系。
-
绘制静态数据(广告、初始小蛇、提示信息、棋盘)
-
添加键盘监听事件,改变游戏状态以及小蛇运动方向
-
添加定时器,让小蛇在一段时间内移动一定的距离
-
随机产生食物,并监听食物状态是否被吃
-
处理游戏结束事件
-
扩展相关游戏机制(积分、等级)
- 定义数据
- 绘制图像
- 事件监听
注意事项
-
导入文件资源时,通过类的相对路径获取
-
URL headURL = Data.class.getResource("header.png"); // 这是放在源代码同一个包下的文件
-
URL headURL = Data.class.getResource("/header.png"); // 这是放在项目根目录下的文件
-
-
键盘监听时,需要自动获取焦点。
- this.setFocusable(true); // 获取焦点
this.addKeyListener(new MyKeyListener());
- this.setFocusable(true); // 获取焦点
-
在修改数据之后,需要repaint重绘图形
-
小蛇运动时,需要注意边界问题
-
食物随机产生的坐标也要限制在游戏区域内
-
添加游戏的可玩性
具体实现
Data.java 存放所有图像数据
package snake;
import javax.swing.*;
import java.net.URL;
/**
* 数据中心
*/
public class Data {
// 相对路径
// 绝对路径 / 当前项目-->"GUI编程目录"
public static URL headURL = Data.class.getResource("header.png");
public static URL upURL = Data.class.getResource("up.png");
public static URL downURL = Data.class.getResource("down.png");
public static URL leftURL = Data.class.getResource("left.png");
public static URL rightURL = Data.class.getResource("right.png");
public static URL bodyURL = Data.class.getResource("body.png");
public static URL foodURL = Data.class.getResource("food.png");
public static URL foodURL2 = Data.class.getResource("food2.png");
public static ImageIcon header = new ImageIcon(headURL);
public static ImageIcon up = new ImageIcon(upURL);
public static ImageIcon down = new ImageIcon(downURL);
public static ImageIcon left = new ImageIcon(leftURL);
public static ImageIcon right = new ImageIcon(rightURL);
public static ImageIcon body = new ImageIcon(bodyURL);
public static ImageIcon food = new ImageIcon(foodURL);
public static ImageIcon food2 = new ImageIcon(foodURL2);
}
StartGame.java 游戏启动类
package snake;
import javax.swing.*;
import java.awt.*;
public class StartGame {
public static void main(String[] args) {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int screenWidth = (int) screenSize.getWidth();
int screenHeight = (int) screenSize.getHeight();
System.out.println("屏幕宽度:" + screenWidth + ",屏幕高度:" + screenHeight);
// 本游戏固定窗体大小(900,720)不可变
int x = (screenWidth - 900) / 2;
int y = (screenHeight - 720) / 2;
System.out.println("相对坐标x:" + x + ",y:" + y);
JFrame jFrame = new JFrame("贪吃蛇");
jFrame.setBounds(x, y, 900, 720);
jFrame.setResizable(false);
jFrame.add(new GamePanel());
jFrame.setVisible(true);
jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
}
GamePanel.java 处理游戏逻辑
package snake;
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 GamePanel extends JPanel {
// 蛇的数据结构
int length;
int[] snakeX = new int[600];
int[] snakeY = new int[500];
char direction;
// 食物的坐标
int foodx;
int foody;
Random random = new Random();
// 计数器
int count;
int score;
int worth;
boolean isStart;
boolean isFail;
// 定时器,每隔100毫秒执行一次参数的事件。
int interval = 100;
Timer timer = new Timer(interval, new MyActionLinstener());
public void init() {
// 默认游戏未开始
this.isStart = false;
this.isFail = false;
// 初始化速度
interval = 200;
timer.setDelay(interval);
// 初始化小蛇数据
this.length = 3;
this.score = 0;
this.snakeX[0] = 100;
this.snakeY[0] = 100;
this.snakeX[1] = 75;
this.snakeY[1] = 100;
this.snakeX[2] = 50;
this.snakeY[2] = 100;
this.direction = \'R\';
// 随机产生事物的坐标
this.foodx = 25 + 25 * random.nextInt(34);
this.foody = 75 + 25 * random.nextInt(24);
this.count = 1; // 统计食物数量
// 获得焦点和键盘监听事件
this.setFocusable(true);
// 添加键盘监听事件
this.addKeyListener(new MyKeyListener());
// 开启定时器
timer.start();
}
public GamePanel() {
init();
}
// 自动绘制面板
// paintComponent()是swing的一个方法,相当于图形版的main(),是会自执行的。
@Override
protected void paintComponent(Graphics g) {
//清屏
super.paintComponent(g);
this.setBackground(Color.WHITE);
//绘制静态面板
// 顶部广告
Data.header.paintIcon(this, g, 25, 11);
// 游戏区域
g.fillRect(25, 75, 850, 600);
// 画积分
g.setColor(Color.white);
g.setFont(new Font("微软雅黑", Font.BOLD, 18));
g.drawString("长度:" + length, 750, 30);
g.drawString("分数:" + score, 750, 56);
// 绘制食物
if (count % 5 != 0) {
Data.food.paintIcon(this, g, foodx, foody);
worth = 1;
} else {
Data.food2.paintIcon(this, g, foodx, foody);
worth = 3;
}
// 游戏状态 默认为暂停 需要提示信息
if (!isStart) {
g.setColor(Color.white);
g.setFont(new Font("微软雅黑", Font.BOLD, 40));
g.drawString("按下空格开始游戏", 300, 300);
}
// 画小蛇 初始化向右 长度为3
// 选择头部的方向
ImageIcon head = Data.right;
switch (this.direction) {
case \'U\':
head = Data.up;
break;
case \'D\':
head = Data.down;
break;
case \'L\':
head = Data.left;
break;
case \'R\':
head = Data.right;
break;
default:
break;
}
head.paintIcon(this, g, snakeX[0], snakeY[0]);
for (int i = 1; i < this.length; i++) {
Data.body.paintIcon(this, g, snakeX[i], snakeY[i]);
}
// 结束状态
if (isFail) {
g.setColor(Color.red);
g.setFont(new Font("微软雅黑", Font.BOLD, 100));
g.drawString("Game Over", 150, 200);
g.drawString("按空格重新开始", 100, 400);
}
}
/**
* 内部类监听键盘事件
*/
class MyKeyListener extends KeyAdapter {
// 只需要监听键盘按下
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// System.out.println(keyCode);
if (keyCode == KeyEvent.VK_SPACE) {
if (isFail) {
// 重新开始
init();
isStart = true;
} else {
isStart = !isStart;
}
repaint();
}
// 内层增加一个判断,180度转向 不生效
switch (keyCode) {
case KeyEvent.VK_UP:
if (direction != \'D\') {
direction = \'U\';
}
break;
case KeyEvent.VK_DOWN:
if (direction != \'U\') {
direction = \'D\';
}
break;
case KeyEvent.VK_LEFT:
if (direction != \'R\') {
direction = \'L\';
}
break;
case KeyEvent.VK_RIGHT:
if (direction != \'L\') {
direction = \'R\';
}
break;
default:
break;
}
// 调整速度 小键盘的 + -
if (keyCode == 107) {
interval = interval > 10 ? interval - 10 : 10;
timer.setDelay(interval);
System.out.println("加速" + interval);
} else if (keyCode == 109) {
interval += 20;
timer.setDelay(interval);
System.out.println("减速" + interval);
}
}
}
class MyActionLinstener implements ActionListener {
// 事件监听类
@Override
public void actionPerformed(ActionEvent e) {
// 通过事件刷新界面
// 如果游戏开始且未失败,则刷新界面
if (isStart && !isFail) {
// 吃食物 蛇头与食物重合
if (snakeX[0] == foodx && snakeY[0] == foody) {
length++; // 吃了边长
foodx = 25 + 25 * random.nextInt(34);
foody = 75 + 25 * random.nextInt(24);
count++; // 统计食物数量
score += worth;
// 这里可以设置积分到一定值,增加移动速度
if (score < 5) {
interval = 200;
timer.setDelay(interval);
System.out.println("一级速度");
} else if (score < 15) {
interval = 150;
timer.setDelay(interval);
System.out.println("二级速度");
} else if (score < 25) {
interval = 100;
timer.setDelay(interval);
System.out.println("三级速度");
} else if (score < 50) {
interval = 50;
timer.setDelay(interval);
System.out.println("四级速度");
}
}
for (int i = length - 1; i > 0; i--) {
// 从最后一节开始继承前一节的位置
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
// 头部找新的路,利用三元运算符判断边界重置
switch (direction) {
case \'U\':
snakeY[0] = snakeY[0] <= 75 ? 650 : snakeY[0] - 25;
break;
case \'D\':
snakeY[0] = snakeY[0] >= 650 ? 75 : snakeY[0] + 25;
break;
case \'L\':
snakeX[0] = snakeX[0] <= 25 ? 850 : snakeX[0] - 25;
break;
case \'R\':
snakeX[0] = snakeX[0] % 850 + 25;
break;
default:
break;
}
// 失败判定
for (int i = 1; i < length; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {
isFail = true;
}
}
repaint();
}
}
}
}
游戏效果
该版本已知bug
小蛇不能反向运动,玩家操作过快导致的意外死亡。
比如小蛇向↑运动过程中,玩家按 ↓ 是无效的,但如果按 ← 的瞬间按 ↓
由于定时器的刷新速度没有跟上玩家的操作,游戏帧尚未刷新就已经有了下一次操作。
误判为小蛇回头撞了自己,游戏结束。
第二个问题是
空格本应该具有暂停/继续/重玩的功能。
游戏局数为偶数时,空格键暂停功能失效。
打包游戏
首先通过IDEA生成Jar包,再通过exe4j将jar打包为exe
具体操作跳转至Java桌面应用程序打包。