前几天看了看Java Swing写了一个贪吃蛇小Demo。可以当作java swing学习的一个小实践。接下来我们看看如何写
结果展示:
该程序总共就涉及以下组件
Jrame窗口程序,主要作用是整个贪吃蛇的窗口。
面板,用来放置一些数据。此程序用来作为游戏布局面板、计分板。需要注意的是Jpanel不能直接add
进Jframe窗口,需要使用jframe.getContentPane().add(jPanel)
类似于前端的标签。其中可以放置文字内容或图片
图片组件
图片组件
网格布局,将此组件加入面板面板会根据该布局器来布局如:
GridLayOut gridLayout = new GridLayOut(50,50,0,0)
Jpanel jpanel = new Jpanel();
jpanel.setLayout(gridLayout);
gridLayout的四个参数分别表示行数,列数,行间距,列间距
设置之后jpanel的布局会按照类似于网格的形式布局。
这里还有其他的布局器:
float left
)边框布局,分为五个部分东,西,南,北,中五个部分
定时器,此程序用来做贪吃蛇的移动执行
键盘监听器,用来监听键盘的事件当按下A``W``S``D
以及方向箭头控制贪吃蛇的移动方向
多余的不说上代码,注释还是挺详细的
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
public GreedySnakeGame(){
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900,720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x,0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
}
}
运行结果:
现在窗口有了那么蛇应该在哪里行走呢?因此我们需要加一些东西让蛇有地方放。这里使用JPanel来作为容器面板
首先在GreedySnakeGame类中定义几个属性:
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
添加以下代码。含注释
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS,COLS,0,0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for(int i= 0;i < ROWS * COLS ; i++){
//创建一个面板
JPanel panel = new JPanel();
//设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
if((i + (i / COLS)) % 2 == 0){
panel.setBackground(Color.WHITE);
}else{
panel.setBackground(Color.BLACK);
}
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15,15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
完整代码:
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
public GreedySnakeGame(){
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS,COLS,0,0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for(int i= 0;i < ROWS * COLS ; i++){
//创建一个面板
JPanel panel = new JPanel();
//设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
if((i + (i / COLS)) % 2 == 0){
panel.setBackground(Color.WHITE);
}else{
panel.setBackground(Color.BLACK);
}
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15,15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900,720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x,0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
}
}
现在我们有了背板,我们需要创建一个蛇的身体。
创建GreedySnakeBody类并继承JLabel
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
* 蛇的身体
*/
public class GreedySnakeBody extends JLabel {
/**
* 创建坐标属性便于后面获取
*/
private int bodyX;
private int bodyY;
public GreedySnakeBody(int bodyX,int bodyY){
this.bodyX = bodyX;
this.bodyY = bodyY;
//设置JLabel的背景颜色为绿色
this.setBackground(Color.GREEN);
//设置JLabel的大小为15*15像素
this.setPreferredSize(new Dimension(15,15));
//设置身体不透明
this.setOpaque(true);
}
public int getBodyX() {
return bodyX;
}
public void setBodyX(int bodyX) {
this.bodyX = bodyX;
}
public int getBodyY() {
return bodyY;
}
public void setBodyY(int bodyY) {
this.bodyY = bodyY;
}
}
创建身体之后我们在背景上将身体部分显示出来。
在GreedySnakeGame中添加以下代码:
属性:
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
构造方法中:
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for(int i= 5 ; i >= 0;i--){
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS/2,i);
snakeBodies.add(greedySnakeBody);
}
创建一个方法用于将蛇画到背景上:
private void paintSnake(){
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody,BorderLayout.CENTER);
}
}
在之前初始化背景面板上加一个代码:
panel.setLayout(new BorderLayout());
设置背景面板的布局为边框布局。
并将之前设置的黑白交替背景注释或删除
完整代码:
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
public GreedySnakeGame(){
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS,COLS,0,0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for(int i= 0;i < ROWS * COLS ; i++){
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15,15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for(int i= 5 ; i >= 0;i--){
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS/2,i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900,720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x,0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
}
private void paintSnake(){
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody,BorderLayout.CENTER);
}
}
}
运行结果:
现在我们的窗口中已经有一条蛇了。现在我们想要让它动起来。那么我们来分析一下。蛇移动有几种情况。这几种情况有一个共性。当蛇移动时除了蛇头的位置发生了新的改变。其他的身体部位的坐标均是上一个身体的坐标。
向上移动
蛇头的Y坐标减1,X坐标不变,更新蛇的显示
向下移动
蛇头的Y坐标加1,X坐标不变,更新蛇的显示
向左移动
蛇头的X坐标减1,Y的坐标不变,更新蛇的显示
向右移动
蛇头的X坐标加1,Y的坐标不变,更新蛇的显示
根据上面的逻辑,我们先创建一个方法,该方法将耦合的步骤抽取出来
/**
* 根据蛇头坐标更新蛇的身体
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x,int y){
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for(int i = snakeBodies.size() - 1;i > 0;i--){
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0,greedySnakeBody);
}
添加移动方法:
向上移动
private void snakeMoveUp(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if(nextY < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
向下移动
private void snakeMoveDown(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if(nextY >= ROWS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
向左移动
private void snakeMoveLeft(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if(nextX < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
向右移动
private void snakeMoveRight(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if(nextX >= COLS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
现在我们有了蛇移动的方法。但是如何让他动起来呢?我们需要监听我们的键盘事件。创建一个内部类SnakeMoveListener继承KeyAdapter。此处可以实现KeyListener接口但是我们只需要里面的一个方法因此我们使用适配器。
keyListener中有三个方法:
keyTyped(KeyEvent e)
键入某个键时触发,即按下并释放某个键时触发
keyPressed(KeyEvent e)
按下键盘时触发
keyReleased(KeyEvent e)
释放键盘时触发
监听器:
class SnakeMoveListener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
//按下W或上键,向上移动
if(e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP){
snakeMoveUp();
}
//按下S或下键,向下移动
if(e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN){
snakeMoveDown();
}
//按下A或左键,向左移动
if(e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT){
snakeMoveLeft();
}
//按下D或右键,向右移动
if(e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT){
snakeMoveRight();
}
}
}
给当前窗口添加监听器:
this.addKeyListener(new SnakeMoveListener());
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
public GreedySnakeGame(){
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS,COLS,0,0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for(int i= 0;i < ROWS * COLS ; i++){
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15,15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for(int i= 5 ; i >= 0;i--){
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS/2,i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900,720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x,0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
}
private void paintSnake(){
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody,BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
//按下W或上键,向上移动
if(e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP){
snakeMoveUp();
}
//按下S或下键,向下移动
if(e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN){
snakeMoveDown();
}
//按下A或左键,向左移动
if(e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT){
snakeMoveLeft();
}
//按下D或右键,向右移动
if(e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT){
snakeMoveRight();
}
}
}
private void snakeMoveUp(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if(nextY < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if(nextY >= ROWS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if(nextX < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if(nextX >= COLS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x,int y){
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for(int i = snakeBodies.size() - 1;i > 0;i--){
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0,greedySnakeBody);
}
}
运行结果:
运行结果我们发现当我们撞墙或者撞到自己的身体时控制台会输出一些提示。
目前我们可以通过按下键盘控制蛇的移动。但是和我们玩的贪吃蛇不太一样。因此我们可以通过定时器来控制蛇的移动。修改键盘事件的逻辑为改变蛇的移动方向。
首先我们创建一个枚举类来列举蛇的移动方向。
/**
* @author APengG
*/
public enum GreedySnakeMoveDirection {
/**
* 蛇移动方向
*/
UP(1),
DOWN(-1),
LEFT(2),
RIGHT(-2),
/**
* 水平方向
* 后面我们用来做一些逻辑判断避免蛇的移动方向冲突
*/
HORIZONTAL(2),
/**
* 垂直方向
* 后面我们用来做一些逻辑判断避免蛇的移动方向冲突
*/
VERTICAL(1);
private final int var;
GreedySnakeMoveDirection(int var) {
this.var = var;
}
public int getVar() {
return var;
}
}
在GreedySnakeGame中添加两个属性
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
创建定时器方法:
private void loaderTimer(){
if(timer != null){
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if(currentDirection == GreedySnakeMoveDirection.UP){
snakeMoveUp();
}
if(currentDirection == GreedySnakeMoveDirection.DOWN){
snakeMoveDown();
}
if(currentDirection == GreedySnakeMoveDirection.LEFT){
snakeMoveLeft();
}
if(currentDirection == GreedySnakeMoveDirection.RIGHT){
snakeMoveRight();
}
});
//启动定时器
timer.start();
}
在GreedySnakeGame的构造方法中执行定时器
loaderTimer();
修改SnakeMoveListener中的代码:
class SnakeMoveListener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
//按下W或上键,向上移动
if(e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP){
currentDirection = GreedySnakeMoveDirection.UP;
}
//按下S或下键,向下移动
if(e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN){
currentDirection = GreedySnakeMoveDirection.DOWN;
}
//按下A或左键,向左移动
if(e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT){
currentDirection = GreedySnakeMoveDirection.LEFT;
}
//按下D或右键,向右移动
if(e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT){
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
}
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
public GreedySnakeGame(){
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS,COLS,0,0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for(int i= 0;i < ROWS * COLS ; i++){
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15,15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for(int i= 5 ; i >= 0;i--){
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS/2,i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900,720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x,0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
}
private void loaderTimer(){
if(timer != null){
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if(currentDirection == GreedySnakeMoveDirection.UP){
snakeMoveUp();
}
if(currentDirection == GreedySnakeMoveDirection.DOWN){
snakeMoveDown();
}
if(currentDirection == GreedySnakeMoveDirection.LEFT){
snakeMoveLeft();
}
if(currentDirection == GreedySnakeMoveDirection.RIGHT){
snakeMoveRight();
}
});
//启动定时器
timer.start();
}
private void paintSnake(){
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody,BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
//按下W或上键,向上移动
if(e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP){
currentDirection = GreedySnakeMoveDirection.UP;
}
//按下S或下键,向下移动
if(e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN){
currentDirection = GreedySnakeMoveDirection.DOWN;
}
//按下A或左键,向左移动
if(e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT){
currentDirection = GreedySnakeMoveDirection.LEFT;
}
//按下D或右键,向右移动
if(e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT){
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
}
private void snakeMoveUp(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if(nextY < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if(nextY >= ROWS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX,nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if(nextX < 0){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight(){
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if(nextX >= COLS){
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if(backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody){
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX,headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x,int y){
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for(int i = snakeBodies.size() - 1;i > 0;i--){
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0,greedySnakeBody);
}
}
运行结果:
运行代码我们发现当我们按下与当前方向相反的方向,蛇会停止移动并且控制台报“撞到了自己”。基于此处我们需要修改SnakeMoveListener的代码:
class SnakeMoveListener extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
//按下W或上键,向上移动
if(e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP){
if(Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()){
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if(e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN){
if(Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()){
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if(e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT){
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if(e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT){
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
}
}
我们玩着玩着又会发现一个问题,比如当蛇在向下移动,此时你迅速的先按下向左移动
再按下向上移动
你会发现控制台会打印撞到自己了。这是因为当我们按下向左移动
后定时器还没有执行结束即界面上的蛇其实还没有完成向左移动这个动作,但是接着来了一个向上移动
的命令,此时程序会发现向上的方向有蛇的身体便会报“撞到了自己”。
针对此种情况我们需要修改以下代码:
再GreedySnakeGame中添加一个属性:
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
修改SnakeMoveListener代码:
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
修改定时器代码:
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
问题解决!!!
现在蛇的移动我们已经基本解决。现在需要解决食物的问题
创建Food类继承JLabel
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
*/
public class Food extends JLabel {
/**
* 食物坐标
*/
private int bodyX;
private int bodyY;
public Food(int x,int y){
this.bodyX = x;
this.bodyY = y;
//设置食物大小
this.setSize(new Dimension(15,15));
//设置背景颜色为红色
this.setBackground(Color.RED);
//设置食物不透明
this.setOpaque(true);
}
public Food loadFood(int x,int y){
//食物我们可以复用只需要改变坐标即可
this.bodyX = x;
this.bodyY = y;
return this;
}
public int getBodyX() {
return bodyX;
}
public void setBodyX(int bodyX) {
this.bodyX = bodyX;
}
public int getBodyY() {
return bodyY;
}
public void setBodyY(int bodyY) {
this.bodyY = bodyY;
}
}
在GreedySnakeGame类中添加一个食物属性默认坐标为0,0
private Food food = new Food(0, 0);
在GreedySnakeGame类中创建随机生成食物的方法。
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
在构造方法中调用食物生成方法
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
private Food food = new Food(0, 0);
public GreedySnakeGame() {
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for (int i = 0; i < ROWS * COLS; i++) {
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15, 15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for (int i = 5; i >= 0; i--) {
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS / 2, i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900, 720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x, 0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
randomProduceFood();
}
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
private void paintSnake() {
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody, BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0, greedySnakeBody);
}
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
}
运行结果:
首先我们先来看蛇身体怎么增加,我们得按照当前状态来看总共会有以下几种状态
蛇身在同一列,蛇正向上移动
增加的身体的坐标为蛇尾坐标的X坐标不变,Y坐标减一
蛇身在同一列,蛇正向下移动
增加的身体的坐标为蛇尾坐标的X坐标不变,Y坐标加一
蛇身在同一行,蛇正向左移动
增加的身体的坐标为蛇尾坐标的Y坐标不变,X坐标减一
蛇身在同一行,蛇正向右移动
增加的身体的坐标为蛇尾坐标的Y坐标不变,X坐标加一
在GreedySnake中添加方法
private void addSnakeBody(){
//获取蛇尾
GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
//获取蛇尾的上一个身体,用于判断当前蛇的位置方向
GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);
int snakeTailX = snakeTail.getBodyX();
int snakeTailY = snakeTail.getBodyY();
int snakeSecondX = snakeSecond.getBodyX();
int snakeSecondY = snakeSecond.getBodyY();
//蛇在同一列
if(snakeTailX == snakeSecondX) {
if (snakeTailY > snakeSecondY) {
//蛇正向上移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
} else {
//蛇正向下移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
}
}
//蛇在同一行
if(snakeTailY == snakeSecondY) {
if (snakeTailX > snakeSecondX) {
//蛇正向左移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
} else {
//蛇正向右移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
}
}
}
添加身体的方法有了,现在我们再添加一个方法来判断蛇是否吃到了食物,如果吃到了食物那么就将身体变长
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
在每次移动后判断是否吃到食物,因此需要修改四个移动的方法
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
注意:将方法添加到更新蛇身之前执行,那么增加长度后刚好重画蛇身
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
private Food food = new Food(0, 0);
public GreedySnakeGame() {
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for (int i = 0; i < ROWS * COLS; i++) {
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15, 15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for (int i = 5; i >= 0; i--) {
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS / 2, i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900, 720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x, 0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
randomProduceFood();
}
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
private void paintSnake() {
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody, BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0, greedySnakeBody);
}
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
private void addSnakeBody(){
//获取蛇尾
GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
//获取蛇尾的上一个身体,用于判断当前蛇的位置方向
GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);
int snakeTailX = snakeTail.getBodyX();
int snakeTailY = snakeTail.getBodyY();
int snakeSecondX = snakeSecond.getBodyX();
int snakeSecondY = snakeSecond.getBodyY();
//蛇在同一列
if(snakeTailX == snakeSecondX) {
if (snakeTailY > snakeSecondY) {
//蛇正向上移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
} else {
//蛇正向下移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
}
}
//蛇在同一行
if(snakeTailY == snakeSecondY) {
if (snakeTailX > snakeSecondX) {
//蛇正向左移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
} else {
//蛇正向右移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
}
}
}
}
运行结果:
没有计分是如此的枯燥,因此我们添加一个计分功能
在GreedySnakeGame添加以下属性:
/**
* 分数
*/
private int scoreNum = 0;
/**
* 分数标签
*/
private JLabel score = new JLabel("分数:" + scoreNum);
/**
* 分数面板用于显示分数或者其他信息
*/
private JPanel scorePanel;
构造方法修改:
scorePanel = new JPanel();
scorePanel.setLayout(new GridLayout(1,3,0,0));
scorePanel.setBackground(Color.WHITE);
scorePanel.add(score);
this.setLayout(new BorderLayout());
//将计分面板添加到窗口中
this.getContentPane().add(scorePanel,BorderLayout.NORTH);
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel,BorderLayout.SOUTH);
当我们吃到食物时分数加10分因此需要修改是否吃到食物的方法:
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//分数增加
this.scoreNum += 10;
this.score.setText("分数:" + this.scoreNum);
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
private Food food = new Food(0, 0);
/**
* 分数
*/
private int scoreNum = 0;
/**
* 分数标签
*/
private JLabel score = new JLabel("分数:" + scoreNum);
/**
* 分数面板用于显示分数或者其他信息
*/
private JPanel scorePanel;
public GreedySnakeGame() {
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for (int i = 0; i < ROWS * COLS; i++) {
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15, 15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for (int i = 5; i >= 0; i--) {
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS / 2, i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
scorePanel = new JPanel();
scorePanel.setLayout(new GridLayout(1,3,0,0));
scorePanel.setBackground(Color.WHITE);
scorePanel.add(score);
this.setLayout(new BorderLayout());
//将计分面板添加到窗口中
this.getContentPane().add(scorePanel,BorderLayout.NORTH);
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel,BorderLayout.SOUTH);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900, 720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x, 0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
randomProduceFood();
}
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
private void paintSnake() {
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody, BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0, greedySnakeBody);
}
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//分数增加
this.scoreNum += 10;
this.score.setText("分数:" + this.scoreNum);
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
private void addSnakeBody(){
//获取蛇尾
GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
//获取蛇尾的上一个身体,用于判断当前蛇的位置方向
GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);
int snakeTailX = snakeTail.getBodyX();
int snakeTailY = snakeTail.getBodyY();
int snakeSecondX = snakeSecond.getBodyX();
int snakeSecondY = snakeSecond.getBodyY();
//蛇在同一列
if(snakeTailX == snakeSecondX) {
if (snakeTailY > snakeSecondY) {
//蛇正向上移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
} else {
//蛇正向下移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
}
}
//蛇在同一行
if(snakeTailY == snakeSecondY) {
if (snakeTailX > snakeSecondX) {
//蛇正向左移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
} else {
//蛇正向右移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
}
}
}
}
运行结果:
游戏结束有两种情况一种为撞墙种是撞自己我们将游戏结束封装到一个方法中。
首先创建一个枚举将两种情况列举出来:
/**
* @author APengG
*/
public enum GreedySnakeGameOverResult {
/**
* 游戏结束信息
*/
HIT_WALL("撞墙了"),
HIT_SELF("撞到自己了");
private String info;
GreedySnakeGameOverResult(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
在GreedySnakeGame中创建方法:
private void gameOverDeal(GreedySnakeGameOverResult result){
//停止计时器
timer.stop();
//弹出对话框
JOptionPane.showMessageDialog(this,
"游戏结束\r\n分数:" + this.scoreNum+"\r\n原因:"+result.getInfo());
//退出游戏
System.exit(0);
}
在之前打印游戏结束的地方修改代码,即四个移动方法:
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
private Food food = new Food(0, 0);
/**
* 分数
*/
private int scoreNum = 0;
/**
* 分数标签
*/
private JLabel score = new JLabel("分数:" + scoreNum);
/**
* 分数面板用于显示分数或者其他信息
*/
private JPanel scorePanel;
public GreedySnakeGame() {
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for (int i = 0; i < ROWS * COLS; i++) {
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15, 15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for (int i = 5; i >= 0; i--) {
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS / 2, i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
scorePanel = new JPanel();
scorePanel.setLayout(new GridLayout(1,3,0,0));
scorePanel.setBackground(Color.WHITE);
scorePanel.add(score);
this.setLayout(new BorderLayout());
//将计分面板添加到窗口中
this.getContentPane().add(scorePanel,BorderLayout.NORTH);
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel,BorderLayout.SOUTH);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900, 720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x, 0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
randomProduceFood();
}
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
private void paintSnake() {
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody, BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y);
snakeBodies.set(0, greedySnakeBody);
}
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//分数增加
this.scoreNum += 10;
this.score.setText("分数:" + this.scoreNum);
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
private void addSnakeBody(){
//获取蛇尾
GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
//获取蛇尾的上一个身体,用于判断当前蛇的位置方向
GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);
int snakeTailX = snakeTail.getBodyX();
int snakeTailY = snakeTail.getBodyY();
int snakeSecondX = snakeSecond.getBodyX();
int snakeSecondY = snakeSecond.getBodyY();
//蛇在同一列
if(snakeTailX == snakeSecondX) {
if (snakeTailY > snakeSecondY) {
//蛇正向上移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
} else {
//蛇正向下移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
}
}
//蛇在同一行
if(snakeTailY == snakeSecondY) {
if (snakeTailX > snakeSecondX) {
//蛇正向左移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
} else {
//蛇正向右移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
}
}
}
private void gameOverDeal(GreedySnakeGameOverResult result){
//停止计时器
timer.stop();
//弹出对话框
JOptionPane.showMessageDialog(this,
"游戏结束\r\n分数:" + this.scoreNum+"\r\n原因:"+result.getInfo());
//退出游戏
System.exit(0);
}
}
运行结果:
食物,首先将食物的图片放到resource文件夹下,我的路径是这样的:
修改食物类的代码:
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
*/
public class Food extends JLabel {
/**
* 食物坐标
*/
private int bodyX;
private int bodyY;
public Food(int x,int y){
this.bodyX = x;
this.bodyY = y;
//设置食物大小
this.setSize(new Dimension(15,15));
//设置背景颜色为红色
// this.setBackground(Color.RED);
//设置食物不透明
// this.setOpaque(true);
//设置背景图片,通过以下步骤才可以改变图片的大小
ImageIcon imageIcon = new ImageIcon(getClass().getResource("/snake/img/food.png"));
//设置图片大小
Image image = imageIcon.getImage();
Image scaledInstance = image.getScaledInstance(15, 15, Image.SCALE_DEFAULT);
imageIcon.setImage(scaledInstance);
this.setIcon(imageIcon);
}
public Food loadFood(int x,int y){
//食物我们可以复用只需要改变坐标即可
this.bodyX = x;
this.bodyY = y;
return this;
}
public int getBodyX() {
return bodyX;
}
public void setBodyX(int bodyX) {
this.bodyX = bodyX;
}
public int getBodyY() {
return bodyY;
}
public void setBodyY(int bodyY) {
this.bodyY = bodyY;
}
}
运行结果:
蛇的背景,因为我想要蛇头与其他地方不用同一个图片因此按照以下方法改造身体类:
import javax.swing.*;
import java.awt.*;
/**
* @author APengG
* 蛇的身体
*/
public class GreedySnakeBody extends JLabel {
/**
* 创建坐标属性便于后面获取
*/
private int bodyX;
private int bodyY;
public GreedySnakeBody(int bodyX,int bodyY){
this.bodyX = bodyX;
this.bodyY = bodyY;
//设置JLabel的背景颜色为绿色
// this.setBackground(Color.GREEN);
//设置JLabel的大小为15*15像素
this.setPreferredSize(new Dimension(15,15));
//设置身体不透明
// this.setOpaque(true);
//默认不为蛇头
loadIcon(false);
}
public GreedySnakeBody(int bodyX,int bodyY,boolean isHead){
this.bodyX = bodyX;
this.bodyY = bodyY;
//设置JLabel的大小为15*15像素
this.setPreferredSize(new Dimension(15,15));
loadIcon(isHead);
}
private void loadIcon(boolean isHead){
ImageIcon imageIcon;
if(isHead){
imageIcon = new ImageIcon(getClass().getResource("/snake/img/head.png"));
}else{
imageIcon = new ImageIcon(getClass().getResource("/snake/img/food.png"));
}
//设置图片大小
Image image = imageIcon.getImage();
Image scaledInstance = image.getScaledInstance(15, 15, Image.SCALE_DEFAULT);
imageIcon.setImage(scaledInstance);
this.setIcon(imageIcon);
}
public int getBodyX() {
return bodyX;
}
public void setBodyX(int bodyX) {
this.bodyX = bodyX;
}
public int getBodyY() {
return bodyY;
}
public void setBodyY(int bodyY) {
this.bodyY = bodyY;
}
}
修改更新蛇身体的方法:
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y,true);
snakeBodies.set(0, greedySnakeBody);
}
完整代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author APengG
* 该类直接继承JFrame,用于创建游戏窗口
* 使用该类时便可以直接创建一个游戏窗口
* 通过构造方法初始化窗口
*/
public class GreedySnakeGame extends JFrame {
public static void main(String[] args) {
//入口
new GreedySnakeGame();
}
/**
* 游戏面板,用于显示游戏内容
*/
private JPanel gamePanel;
/**
* 贪吃蛇背板行数和列数
*/
private final static int ROWS = 50;
private final static int COLS = 50;
/**
* 存储每一个格子,方便后面修改每一个格子的属性
*/
private JPanel[][] backPanels = new JPanel[ROWS][COLS];
private List<GreedySnakeBody> snakeBodies = new ArrayList<>();
/**
* 蛇移动方向
* 默认向下移动
*/
private GreedySnakeMoveDirection currentDirection = GreedySnakeMoveDirection.DOWN;
/**
* 定时器
*/
private Timer timer;
/**
* 是否可以改变方向
*/
private AtomicBoolean isPause = new AtomicBoolean(true);
private Food food = new Food(0, 0);
/**
* 分数
*/
private int scoreNum = 0;
/**
* 分数标签
*/
private JLabel score = new JLabel("分数:" + scoreNum);
/**
* 分数面板用于显示分数或者其他信息
*/
private JPanel scorePanel;
public GreedySnakeGame() {
//设置窗口标题等同于this.setTitle("贪吃蛇");
super("贪吃蛇");
//初始化游戏面板
gamePanel = new JPanel();
//设置游戏面板布局
GridLayout gridLayout = new GridLayout(ROWS, COLS, 0, 0);
gamePanel.setLayout(gridLayout);
//初始化游戏背板
for (int i = 0; i < ROWS * COLS; i++) {
//创建一个面板
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// //设置面板背景颜色,此处设置为黑白相间为了方便观察此时的布局样式后期删除即可
// if((i + (i / COLS)) % 2 == 0){
// panel.setBackground(Color.WHITE);
// }else{
// panel.setBackground(Color.BLACK);
// }
//设置每个格子的大小为长15像素,宽15像素
panel.setPreferredSize(new Dimension(15, 15));
//将面板添加到游戏面板中
gamePanel.add(panel);
//将面板添加到背板数组中
backPanels[i % ROWS][i / COLS] = panel;
}
//初始化贪吃蛇,蛇的初始长度为6。使用递减为了蛇头向下
for (int i = 5; i >= 0; i--) {
//蛇的坐标在背板中的位置,
GreedySnakeBody greedySnakeBody =
new GreedySnakeBody(ROWS / 2, i);
snakeBodies.add(greedySnakeBody);
}
paintSnake();
this.addKeyListener(new SnakeMoveListener());
loaderTimer();
scorePanel = new JPanel();
scorePanel.setLayout(new GridLayout(1,3,0,0));
scorePanel.setBackground(Color.WHITE);
scorePanel.add(score);
this.setLayout(new BorderLayout());
//将计分面板添加到窗口中
this.getContentPane().add(scorePanel,BorderLayout.NORTH);
//将游戏面板添加到窗口中
this.getContentPane().add(gamePanel,BorderLayout.SOUTH);
//先设置窗口大小下边才能获取窗口宽度
this.setSize(900, 720);
//获取当前屏幕大小
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
//等同于 (screenSize.width - this.getWidth()) / 2 目的为了让窗口横向居中
int x = screenSize.width - this.getWidth() >> 1;
this.setLocation(x, 0);
//设置窗口大小不可变,即使用鼠标无法拖动改变窗口大小
this.setResizable(false);
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
//设置窗口可见,不设置窗口不显示
this.setVisible(true);
//设置紧凑布局,即窗口大小为游戏面板大小
this.pack();
randomProduceFood();
}
private void loaderTimer() {
if (timer != null) {
timer.stop();
}
//初始化定时器
//500表示500ms执行一次方法
timer = new Timer(500, e -> {
if (currentDirection == GreedySnakeMoveDirection.UP) {
snakeMoveUp();
}
if (currentDirection == GreedySnakeMoveDirection.DOWN) {
snakeMoveDown();
}
if (currentDirection == GreedySnakeMoveDirection.LEFT) {
snakeMoveLeft();
}
if (currentDirection == GreedySnakeMoveDirection.RIGHT) {
snakeMoveRight();
}
isPause.set(true);
});
//启动定时器
timer.start();
}
private void paintSnake() {
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//加入面板并设置边框布局使蛇的身体填满面板
panel.add(snakeBody, BorderLayout.CENTER);
}
}
class SnakeMoveListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
//判断是否可以改变方向
if (isPause.get()) {
//按下W或上键,向上移动
if (e.getKeyCode() == KeyEvent.VK_W || e.getKeyCode() == KeyEvent.VK_UP) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.UP;
}
}
//按下S或下键,向下移动
if (e.getKeyCode() == KeyEvent.VK_S || e.getKeyCode() == KeyEvent.VK_DOWN) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.VERTICAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.DOWN;
}
}
//按下A或左键,向左移动
if (e.getKeyCode() == KeyEvent.VK_A || e.getKeyCode() == KeyEvent.VK_LEFT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.LEFT;
}
}
//按下D或右键,向右移动
if (e.getKeyCode() == KeyEvent.VK_D || e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (Math.abs(currentDirection.getVar()) != GreedySnakeMoveDirection.HORIZONTAL.getVar()) {
currentDirection = GreedySnakeMoveDirection.RIGHT;
}
}
//改变方向后设置为不可改变方向,必须定时器执行完毕后才能改变方向
isPause.set(false);
}
}
}
private void snakeMoveUp() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY - 1;
//判断是否撞墙
if (nextY < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveDown() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextY = headY + 1;
//判断是否撞墙
if (nextY >= ROWS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[headX][nextY].getComponentCount() > 0 &&
backPanels[headX][nextY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(headX, nextY);
//更新蛇的身体
updateSnakeBodyList(headX, nextY);
//重绘蛇
paintSnake();
}
private void snakeMoveLeft() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX - 1;
//判断是否撞墙
if (nextX < 0) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
private void snakeMoveRight() {
//获取当前蛇头
GreedySnakeBody snakeHead = snakeBodies.get(0);
//获取蛇头的坐标
int headX = snakeHead.getBodyX();
int headY = snakeHead.getBodyY();
//移动蛇头
int nextX = headX + 1;
//判断是否撞墙
if (nextX >= COLS) {
//撞墙了,游戏结束
System.out.println("撞墙了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_WALL);
return;
}
//判断是否撞到自己,背景中我们最多会放身体与食物并且他们不会同时存在。因此背景板有元素的话只有一个
if (backPanels[nextX][headY].getComponentCount() > 0 &&
backPanels[nextX][headY].getComponent(0) instanceof GreedySnakeBody) {
//撞到自己了,游戏结束
System.out.println("撞到自己了,游戏结束");
gameOverDeal(GreedySnakeGameOverResult.HIT_SELF);
return;
}
//判断是否吃到食物
ifEatFood(nextX, headY);
//更新蛇的身体
updateSnakeBodyList(nextX, headY);
//重绘蛇
paintSnake();
}
/**
* 根据蛇头坐标更新蛇的身体
*
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void updateSnakeBodyList(int x, int y) {
//移除原来面板中的蛇身体
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
//获取蛇应该在的面板位置
JPanel panel = backPanels[bodyX][bodyY];
//移除面板中的蛇身体
panel.remove(snakeBody);
//刷新面板
panel.revalidate();
panel.repaint();
}
//更新蛇的坐标
//此处保留了蛇头的位置,即第一个元素0不修改。蛇头的坐标单独修改
for (int i = snakeBodies.size() - 1; i > 0; i--) {
//蛇身体的坐标等于上一个蛇身体的坐标
GreedySnakeBody snakeBody = snakeBodies.get(i);
GreedySnakeBody snakeBody1 = snakeBodies.get(i - 1);
snakeBody.setBodyX(snakeBody1.getBodyX());
snakeBody.setBodyY(snakeBody1.getBodyY());
}
//更新蛇头
GreedySnakeBody greedySnakeBody = new GreedySnakeBody(x, y,true);
snakeBodies.set(0, greedySnakeBody);
}
private void randomProduceFood(){
int foodX = (int)(Math.random()*ROWS);
int foodY = (int)(Math.random()*COLS);
//判断生成的坐标是否与蛇重合
for (GreedySnakeBody snakeBody : snakeBodies) {
//获取蛇的坐标
int bodyX = snakeBody.getBodyX();
int bodyY = snakeBody.getBodyY();
if (foodX == bodyX && foodY == bodyY) {
//重合了,重新生成
randomProduceFood();
return;
}
}
//生成食物
food = food.loadFood(foodX, foodY);
//将食物添加到面板中
JPanel jPanel = backPanels[foodX][foodY];
//使食物填满面板
jPanel.add(food,BorderLayout.CENTER);
//刷新面板
jPanel.revalidate();
jPanel.repaint();
}
/**
* 当蛇移动后将蛇头的坐标与食物的坐标进行比较,如果相同则吃掉食物
* @param x 蛇头的x坐标
* @param y 蛇头的y坐标
*/
private void ifEatFood(int x,int y){
if(backPanels[x][y].getComponentCount() > 0 &&
backPanels[x][y].getComponent(0) instanceof Food
){
//吃到食物了,增加蛇身
addSnakeBody();
//分数增加
this.scoreNum += 10;
this.score.setText("分数:" + this.scoreNum);
//当前食物已经被吃掉,重新生成食物
randomProduceFood();
}
}
private void addSnakeBody(){
//获取蛇尾
GreedySnakeBody snakeTail = snakeBodies.get(snakeBodies.size() - 1);
//获取蛇尾的上一个身体,用于判断当前蛇的位置方向
GreedySnakeBody snakeSecond = snakeBodies.get(snakeBodies.size() - 2);
int snakeTailX = snakeTail.getBodyX();
int snakeTailY = snakeTail.getBodyY();
int snakeSecondX = snakeSecond.getBodyX();
int snakeSecondY = snakeSecond.getBodyY();
//蛇在同一列
if(snakeTailX == snakeSecondX) {
if (snakeTailY > snakeSecondY) {
//蛇正向上移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY - 1));
} else {
//蛇正向下移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX, snakeTailY + 1));
}
}
//蛇在同一行
if(snakeTailY == snakeSecondY) {
if (snakeTailX > snakeSecondX) {
//蛇正向左移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX - 1, snakeTailY));
} else {
//蛇正向右移动,添加身体
snakeBodies.add(new GreedySnakeBody(snakeTailX + 1, snakeTailY));
}
}
}
private void gameOverDeal(GreedySnakeGameOverResult result){
//停止计时器
timer.stop();
//弹出对话框
JOptionPane.showMessageDialog(this,
"游戏结束\r\n分数:" + this.scoreNum+"\r\n原因:"+result.getInfo());
//退出游戏
System.exit(0);
}
}
运行结果:
如果觉得蛇走得太慢可以修改定时器执行的时间。
代码中还有一个问题当我们点X关闭时发现程序仍然在运行,应为我们只把窗口关闭了。
修改构造方法中此行代码即可
//设置窗口关闭方式,即点击关闭按钮时只关闭当前窗口
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
修改为
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);